190 lines
8.0 KiB
Go
190 lines
8.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The manifest says what the code does (novox/hq ADR 0359): the database and the bucket it requires (§3), the own
|
|
// secret family issued outside the mesh (§5, ADR 0283 §6), the two trusted settings (§4), the tools (§5), the events,
|
|
// its two long-running processes with their health (§6), and — ADR 0378 as amended by ADR 0379, issue 800 part A —
|
|
// the three mail seats it declares and the one, mail-store, it claims. It is assigned to no machine by itself.
|
|
|
|
type seat struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Serves []string `json:"serves"`
|
|
Needs []string `json:"needs"`
|
|
}
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Slug string `json:"slug"`
|
|
Seats []seat `json:"seats"`
|
|
Requires []string `json:"requires"`
|
|
Contributes map[string]map[string]any `json:"contributes"`
|
|
OwnSecrets map[string]map[string]string `json:"own-secrets"`
|
|
Tools []string `json:"tools"`
|
|
Emits []string `json:"emits"`
|
|
Streams []struct {
|
|
Name string `json:"name"`
|
|
Events []string `json:"events"`
|
|
MaxAgeDays int `json:"max-age-days"`
|
|
Grant struct {
|
|
Config string `json:"config"`
|
|
Events string `json:"events"`
|
|
} `json:"grant"`
|
|
} `json:"streams"`
|
|
Consumes []string `json:"consumes"`
|
|
Invokes []string `json:"invokes"`
|
|
Claims []seat `json:"claims"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []struct {
|
|
From string `json:"from"`
|
|
Binary string `json:"binary"`
|
|
Loads []string `json:"loads"`
|
|
Env map[string]string `json:"env"`
|
|
} `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
func readManifest(t *testing.T) (manifest, map[string]map[string]any) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
byID := map[string]map[string]any{}
|
|
for _, r := range m.Resources {
|
|
byID[r["id"].(string)] = r
|
|
}
|
|
return m, byID
|
|
}
|
|
|
|
func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
|
m, byID := readManifest(t)
|
|
if m.Module != Module || !reflect.DeepEqual(m.Requires, []string{"postgres-database", "s3-bucket"}) || m.Contributes["postgres-database"]["name"] != "mail" {
|
|
t.Fatalf("module %s, requires %v, contributes %v", m.Module, m.Requires, m.Contributes)
|
|
}
|
|
// mesh_<node>_<slug> fits the S3 access key's 20 characters on every node.
|
|
if len("mesh_shanks_"+m.Slug) > 20 {
|
|
t.Fatalf("the slug %q overflows an S3 access key", m.Slug)
|
|
}
|
|
fam := m.OwnSecrets[SecretFamily+"*"]
|
|
if fam["issued-by"] != "outside" || fam["path"] != "${dir:state}/"+SecretFamily+"*.secret" || len(m.OwnSecrets) != 1 {
|
|
t.Fatalf("own secrets %v", m.OwnSecrets)
|
|
}
|
|
var names []string
|
|
for _, tool := range (&Bundle{}).Tools() {
|
|
names = append(names, tool.Name)
|
|
}
|
|
wantTools := []string{
|
|
"mail_accounts", "mail_search", "mail_read", "mail_health", "mail_send", "mail_send_status",
|
|
"mail-store.accounts", "mail-store.search", "mail-store.read", "mail-store.send",
|
|
}
|
|
if !reflect.DeepEqual(m.Tools, names) || !reflect.DeepEqual(names, wantTools) {
|
|
t.Fatalf("tools %v, the code serves %v", m.Tools, names)
|
|
}
|
|
// Its events go to its own stream MAIL, kept a week, each account's part heard only by the consumers the trusted
|
|
// config value grants names for it (ADR 0359 §2, §4; ADR 0363): none on the shared EVENTS stream.
|
|
var events []string
|
|
for _, c := range Changes {
|
|
events = append(events, EventKey("*", c))
|
|
}
|
|
if len(m.Emits) != 0 || len(m.Streams) != 1 || m.Streams[0].Name != "mail" || !reflect.DeepEqual(m.Streams[0].Events, events) ||
|
|
m.Streams[0].MaxAgeDays != 7 || m.Streams[0].Grant.Config != "grants" || m.Streams[0].Grant.Events != "mail.{key}.**" {
|
|
t.Fatalf("emits %v, streams %+v; the code says %v", m.Emits, m.Streams, events)
|
|
}
|
|
// It calls nothing and consumes nothing: mesh-mail-notifier (issue 800 part B) is the one that invokes and
|
|
// consumes, not the store.
|
|
if len(m.Consumes)+len(m.Invokes) != 0 {
|
|
t.Fatalf("consumes %v, invokes %v", m.Consumes, m.Invokes)
|
|
}
|
|
// It declares exactly the three mail seats (ADR 0378 §1 as amended by ADR 0379 §4) — mail-store and
|
|
// mail-webclient each held once for the mesh, mail-notifier held once per machine and needing node-notifier
|
|
// there — and claims only mail-store, serving the same four verbs as its own tools (§2, §3).
|
|
wantSeats := []seat{
|
|
{Name: "mail-store", Scope: "mesh", Serves: []string{"accounts", "search", "read", "send"}},
|
|
{Name: "mail-webclient", Scope: "mesh", Serves: []string{"open", "status"}},
|
|
{Name: "mail-notifier", Scope: "node", Serves: []string{"status"}, Needs: []string{"node-notifier"}},
|
|
}
|
|
if !reflect.DeepEqual(m.Seats, wantSeats) {
|
|
t.Fatalf("seats %+v, want %+v", m.Seats, wantSeats)
|
|
}
|
|
wantClaims := []seat{
|
|
{Name: "mail-store", Scope: "mesh", Serves: []string{"accounts", "search", "read", "send"}},
|
|
}
|
|
if !reflect.DeepEqual(m.Claims, wantClaims) {
|
|
t.Fatalf("claims %+v, want %+v", m.Claims, wantClaims)
|
|
}
|
|
// The two settings are trusted, and their defaults are an empty account list and no grant.
|
|
for id, want := range map[string]string{"accounts": `{"accounts":{}}`, "grants": `{"grants":{}}`} {
|
|
r := byID[id]
|
|
if r == nil || r["trusted"] != true || r["merge"] != "json" || !reflect.DeepEqual(r["takes-config"], []any{id}) {
|
|
t.Fatalf("%s: %v", id, r)
|
|
}
|
|
// The file's text is kept once, in its template under files/ (novox/hq ADR 0349 §2).
|
|
raw, err := os.ReadFile(filepath.Join("../..", r["template"].(string)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(strings.Fields(string(raw)), "") != want {
|
|
t.Fatalf("%s's default is %q", id, raw)
|
|
}
|
|
if id == "accounts" {
|
|
if _, err := ParseAccounts(raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
a := m.Build.Artifacts
|
|
if len(a) != 2 || a[1].From != "files" || a[0].From != "cmd/mesh-mail" || a[0].Binary != Module || !reflect.DeepEqual(a[0].Loads, []string{Module}) {
|
|
t.Fatalf("build %+v", m.Build)
|
|
}
|
|
for k, v := range map[string]string{"MESH_MAIL_ACCOUNTS": "accounts", "MESH_MAIL_GRANTS": "grants", "MESH_MAIL_STORE": "store", "DATABASE_URL_FILE": "database-url"} {
|
|
if a[0].Env[k] != byID[v]["path"] {
|
|
t.Errorf("the bundle reads %s at %s, and the manifest writes %s at %v", k, a[0].Env[k], v, byID[v]["path"])
|
|
}
|
|
}
|
|
if a[0].Env["MESH_MAIL_SECRETS"] != "${dir:state}" {
|
|
t.Errorf("the secrets directory is %s", a[0].Env["MESH_MAIL_SECRETS"])
|
|
}
|
|
}
|
|
|
|
// Its processes: the preparation once, keep judged by its unit, watch by mail_health (ADR 0359 §6), the watch writing
|
|
// the verdict the bundle's tool reads, as the account the bundle runs as.
|
|
func TestItsProcessesAreThePreparationTheActAndTheJudge(t *testing.T) {
|
|
m, byID := readManifest(t)
|
|
prep, keep, watch := byID["prepare"], byID["keep"], byID["watch"]
|
|
if prep == nil || prep["run-once"] != true || prep["run"].([]any)[1] != "prepare" {
|
|
t.Fatalf("prepare %v", prep)
|
|
}
|
|
if keep == nil || keep["run"].([]any)[1] != "keep" || keep["health"].(map[string]any)["kind"] != "unit" {
|
|
t.Fatalf("keep %v", keep)
|
|
}
|
|
h, _ := watch["health"].(map[string]any)
|
|
if watch == nil || watch["run"].([]any)[1] != "watch" || h["kind"] != "tool" || h["tool"] != HealthTool || h["needs"] != "postgres-database" {
|
|
t.Fatalf("watch %v", watch)
|
|
}
|
|
if watch["user"] != "${machine:account}" || watch["env"].(map[string]any)["MESH_MAIL_STATUS"] != m.Build.Artifacts[0].Env["MESH_MAIL_STATUS"] {
|
|
t.Fatalf("watch writes another status than the tool reads, or as another account: %v", watch)
|
|
}
|
|
// keep runs as the machine's root, which the own secrets' files belong to; it starts again when its settings or
|
|
// its store change.
|
|
if _, as := keep["user"]; as {
|
|
t.Fatalf("keep runs as %v", keep["user"])
|
|
}
|
|
if !reflect.DeepEqual(keep["restart-on"], []any{"database-url", "store", "accounts"}) {
|
|
t.Fatalf("keep restarts on %v", keep["restart-on"])
|
|
}
|
|
}
|