ADR 0515 §4, §5: the trusted setting modes names the accounts Work and Personal show, Everything shows every granted account; /read shows one mail as escaped text with its tasks, promises and attachments as downloads, records it opened, and offers its corrections (mark and group, not a task, not a promise, read later), each calling correct. Issue 1850: a sender with no display name is named by its address, on the list, the pane and All mail.
195 lines
7.9 KiB
Go
195 lines
7.9 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 json.RawMessage `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 || string(op.Default) != "[]" {
|
|
t.Fatalf("config.operators %+v, want a trusted list defaulting to nobody", op)
|
|
}
|
|
// issue 1859 (ADR 0515 §4): the trusted setting modes, an object of work and personal, empty by default.
|
|
if modes, ok := m.Config["modes"]; !ok || modes.Type != "object" || !modes.Trusted || string(modes.Default) != "{}" {
|
|
t.Fatalf("config.modes %+v, want a trusted object defaulting to none", modes)
|
|
}
|
|
// 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, "modes": 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", "MESH_MAIL_WEB_MODES": "modes"} {
|
|
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
|
|
}
|