Files
mesh-catalog/modules/mesh-mail-web/internal/web/manifest_test.go
T
jochen ad4f312263 issue 1859: open the mail page on the operator's list (1859.1)
ADR 0515 §1, §5: / greets the operator with what the mail signal told today and when they last looked, then
shows today, this week, later, the promises by kind, good to know, read later, handled today and the scam
warnings, from mail-reading.list; it records the page opened. The classical list moves to /all, as All mail.
2026-10-22 20:03:21 +02:00

191 lines
7.5 KiB
Go

package web
// The manifest says what the code does (novox/hq ADR 0440 §1, §2, §3, §8; issue 1262).
//
// 1262/AC5: the repository's own way to run "the controller's manifest rules" is merge-check.sh, which shells
// out to the real controller binary ("$MESH_GATE" module check modules/*/module.json, merge-check.sh:127) — not
// something this package can call as a unit test, and this batch's brief refuses running merge-check.sh or any
// integration test. mesh-mail's and mesh-mail-notifier's own manifest_test.go files do not run the controller
// either: they assert the manifest's shape by hand, field by field, in Go. There is no unit-level way in this
// repository to run the controller's actual module-check rules, so TestTheManifestSaysWhatTheCodeDoes below is
// as far as the repo allows for AC5: it is the same hand-written shape assertion the sibling modules use, now
// covering every field AC1 names. The real module check still runs once, for real, through the lead's
// mesh-controller.try.
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
)
type seat struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
}
type listen struct {
Name string `json:"name"`
Port int `json:"port"`
Protocol string `json:"protocol"`
From string `json:"from"`
}
type configEntry struct {
Type string `json:"type"`
Trusted bool `json:"trusted"`
Default []string `json:"default"`
}
type manifest struct {
Module string `json:"module"`
RunsAs string `json:"runs-as"`
Claims []seat `json:"claims"`
Invokes []string `json:"invokes"`
Reads []string `json:"reads"`
Requires []string `json:"requires"`
Contributes map[string]map[string]any `json:"contributes"`
Binds map[string]string `json:"binds"`
Secrets map[string]string `json:"secrets"`
Listens []listen `json:"listens"`
Config map[string]configEntry `json:"config"`
Tools []string `json:"tools"`
State []any `json:"state"`
OwnSecrets map[string]string `json:"own-secrets"`
SecretsOwner string `json:"secrets-owner"`
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 {
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)
}
return m
}
// 1262/AC1 (and, as far as the repo allows, AC5 — see the file's own comment above).
func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
m := readManifest(t)
if m.Module != "mesh-mail-web" {
t.Fatalf("module %q", m.Module)
}
if m.RunsAs != "mesh-mail-web" {
t.Fatalf("runs-as %q, want its own account", m.RunsAs)
}
wantClaims := []seat{{Name: "mail-webclient", Scope: "mesh", Serves: []string{"open", "status"}}}
if !reflect.DeepEqual(m.Claims, wantClaims) {
t.Fatalf("claims %+v, want %+v", m.Claims, wantClaims)
}
// 1430/AC: the six seat:mail-reading verbs, beside the five mail-store ones.
wantInvokes := []string{
"seat:mail-store.accounts", "seat:mail-store.search", "seat:mail-store.read",
"seat:mail-store.send", "seat:mail-store.send-status",
"seat:mail-reading.entries", "seat:mail-reading.groups", "seat:mail-reading.status",
"seat:mail-reading.correct", "seat:mail-reading.lessons", "seat:mail-reading.undo",
// issue 1859: the operator's list and its acts.
"seat:mail-reading.list", "seat:mail-reading.handle", "seat:mail-reading.unhandle",
"seat:mail-reading.remind", "seat:mail-reading.record",
}
if !reflect.DeepEqual(m.Invokes, wantInvokes) {
t.Fatalf("invokes %v, want exactly %v", m.Invokes, wantInvokes)
}
if !reflect.DeepEqual(m.Reads, []string{"mesh-mail.accounts"}) {
t.Fatalf("reads %v, want [mesh-mail.accounts]", m.Reads)
}
if !contains(m.Requires, "route") || !contains(m.Requires, "oidc-client") {
t.Fatalf("requires %v, want route and oidc-client", m.Requires)
}
if len(m.Listens) != 1 || m.Listens[0].Name != "web" || m.Listens[0].From != "mesh" || m.Listens[0].Protocol != "tcp" {
t.Fatalf("listens %+v, want exactly one, named web, from the mesh only", m.Listens)
}
// "list" is the controller's own word for a trusted array value (novox/hq ADR 0348 §4); "array" is not
// one of its five types (number, string, boolean, object, list) and the controller refuses it.
op, ok := m.Config["operators"]
if !ok || op.Type != "list" || !op.Trusted || len(op.Default) != 0 {
t.Fatalf("config.operators %+v, want a trusted list defaulting to nobody", op)
}
// 1430/AC4: no database, no state bucket, no writable directory beyond the account's own state dir: only the
// account (a user resource, not writable), the state directory, and the two files rendered into it.
if len(m.State) != 0 {
t.Fatalf("state %v, want none", m.State)
}
for _, dbish := range []string{"postgres-database", "s3-bucket"} {
if contains(m.Requires, dbish) {
t.Fatalf("requires %v, want no database or object store", m.Requires)
}
}
wantResourceIDs := map[string]bool{"account": true, "state": true, "operators": true, "oidc-config": true}
if len(m.Resources) != len(wantResourceIDs) {
t.Fatalf("resources %v, want exactly %v", m.Resources, wantResourceIDs)
}
for _, r := range m.Resources {
id := r["id"].(string)
if !wantResourceIDs[id] {
t.Fatalf("an unexpected resource %q: no writable directory beyond the state dir is wanted", id)
}
if id != "account" && r["type"] == "directory" && id != "state" {
t.Fatalf("an extra writable directory %q", id)
}
}
// The manifest's own tools match the code's (mirroring mesh-mail's and mesh-mail-notifier's own check).
var names []string
for _, tool := range (&Bundle{}).Tools() {
names = append(names, tool.Name)
}
if !reflect.DeepEqual(m.Tools, names) {
t.Fatalf("tools %v, the code serves %v", m.Tools, names)
}
a := m.Build.Artifacts
if len(a) != 2 || a[0].From != "cmd/mesh-mail-web" || a[0].Binary != "mesh-mail-web" || !reflect.DeepEqual(a[0].Loads, []string{"mesh-mail-web"}) {
t.Fatalf("build %+v", m.Build)
}
for k, v := range map[string]string{"MESH_MAIL_WEB_OPERATORS": "operators", "MESH_MAIL_WEB_OIDC": "oidc-config"} {
found := false
for _, r := range m.Resources {
if r["id"] == v {
found = a[0].Env[k] == r["path"]
}
}
if !found {
t.Fatalf("the bundle reads %s at %s, which does not match resource %s's path", k, a[0].Env[k], v)
}
}
// A bundle's word is ${port:<the number a listens entry gives>}, never the endpoint's own name
// (novox/hq ADR 0192; mesh-controller internal/catalogue/port_into.go portsFilledInto matches it against
// listens[].Port, a plain number, not a label like "web"). len(m.Listens)==1 is already checked above.
wantListen := fmt.Sprintf("${port:%d}", m.Listens[0].Port)
if a[0].Env["MESH_MAIL_WEB_LISTEN"] != wantListen {
t.Fatalf("MESH_MAIL_WEB_LISTEN is %q, want %q (the listens entry's own port number)",
a[0].Env["MESH_MAIL_WEB_LISTEN"], wantListen)
}
}
func contains(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}