Files
mesh-catalog/modules/mesh-mail/cmd/mesh-mail/manifest_test.go
T

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"])
}
}