mesh-mail: pin mesh-sdk go to release 2, declare the sender account, bus process and work-queue stream (ADR 0436 clauses 7, 8; issue 1172)
This commit is contained in:
@@ -30,11 +30,16 @@ type manifest struct {
|
||||
OwnSecrets map[string]map[string]string `json:"own-secrets"`
|
||||
Tools []string `json:"tools"`
|
||||
Emits []string `json:"emits"`
|
||||
State []string `json:"state"`
|
||||
Streams []struct {
|
||||
Name string `json:"name"`
|
||||
Events []string `json:"events"`
|
||||
MaxAgeDays int `json:"max-age-days"`
|
||||
Grant struct {
|
||||
Name string `json:"name"`
|
||||
Events []string `json:"events"`
|
||||
MaxAgeDays int `json:"max-age-days"`
|
||||
MaxMiB int `json:"max-mib"`
|
||||
Retention string `json:"retention"`
|
||||
MaxDeliveries int `json:"max-deliveries"`
|
||||
Duplicates int `json:"duplicates"`
|
||||
Grant struct {
|
||||
Config string `json:"config"`
|
||||
Events string `json:"events"`
|
||||
} `json:"grant"`
|
||||
@@ -95,13 +100,35 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
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.
|
||||
// config value grants names for it (ADR 0359 §2, §4; ADR 0363): none on the shared EVENTS stream. The module's
|
||||
// top-level emits is the sender's two outcomes (novox/hq ADR 0436 §7, §11, issue 1172): the module as a whole may
|
||||
// publish them, though only the process send actually does (its own bus.emits, checked in
|
||||
// TestItsManifestDeclaresTheSenderProcess).
|
||||
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}.**" {
|
||||
var mailStream *struct {
|
||||
Name string `json:"name"`
|
||||
Events []string `json:"events"`
|
||||
MaxAgeDays int `json:"max-age-days"`
|
||||
MaxMiB int `json:"max-mib"`
|
||||
Retention string `json:"retention"`
|
||||
MaxDeliveries int `json:"max-deliveries"`
|
||||
Duplicates int `json:"duplicates"`
|
||||
Grant struct {
|
||||
Config string `json:"config"`
|
||||
Events string `json:"events"`
|
||||
} `json:"grant"`
|
||||
}
|
||||
for i := range m.Streams {
|
||||
if m.Streams[i].Name == "mail" {
|
||||
mailStream = &m.Streams[i]
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(m.Emits, []string{"mail.send.sent", "mail.send.failed"}) || len(m.Streams) != 2 || mailStream == nil ||
|
||||
!reflect.DeepEqual(mailStream.Events, events) || mailStream.MaxAgeDays != 7 || mailStream.Grant.Config != "grants" ||
|
||||
mailStream.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
|
||||
@@ -187,3 +214,104 @@ func TestItsProcessesAreThePreparationTheActAndTheJudge(t *testing.T) {
|
||||
t.Fatalf("keep restarts on %v", keep["restart-on"])
|
||||
}
|
||||
}
|
||||
|
||||
// 1172/AC2: the manifest declares the sender's account, own-secrets owner, work-queue stream and process bus,
|
||||
// and widens the state directory and accounts.json so the sender (not the operator account) can read them,
|
||||
// mirroring mesh-controller's own fixtures (internal/catalogue/process_bus_test.go:14-31,
|
||||
// own_secret_owner_test.go:16-29, stream_work_queue_test.go:13-14).
|
||||
func TestItsManifestDeclaresTheSenderProcess(t *testing.T) {
|
||||
m, byID := readManifest(t)
|
||||
|
||||
account := byID["account"]
|
||||
if account == nil || account["type"] != "user" || account["name"] != "mesh-mail-send" {
|
||||
t.Fatalf("the sender's account resource: %v", account)
|
||||
}
|
||||
|
||||
fam := m.OwnSecrets[SecretFamily+"*"]
|
||||
if fam["owner"] != "mesh-mail-send" {
|
||||
t.Fatalf("mail-credential-*'s owner: %v; want mesh-mail-send", fam["owner"])
|
||||
}
|
||||
|
||||
var sendsStream *struct {
|
||||
Name string `json:"name"`
|
||||
Events []string `json:"events"`
|
||||
MaxAgeDays int `json:"max-age-days"`
|
||||
MaxMiB int `json:"max-mib"`
|
||||
Retention string `json:"retention"`
|
||||
MaxDeliveries int `json:"max-deliveries"`
|
||||
Duplicates int `json:"duplicates"`
|
||||
Grant struct {
|
||||
Config string `json:"config"`
|
||||
Events string `json:"events"`
|
||||
} `json:"grant"`
|
||||
}
|
||||
for i := range m.Streams {
|
||||
if m.Streams[i].Name == "mail-sends" {
|
||||
sendsStream = &m.Streams[i]
|
||||
}
|
||||
}
|
||||
if sendsStream == nil || !reflect.DeepEqual(sendsStream.Events, []string{"mail.send.approved"}) ||
|
||||
sendsStream.MaxAgeDays != 1 || sendsStream.MaxMiB != 64 || sendsStream.Retention != "work-queue" ||
|
||||
sendsStream.MaxDeliveries != 5 || sendsStream.Duplicates != 24 {
|
||||
t.Fatalf("the stream mail-sends: %+v", sendsStream)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(m.Emits, []string{"mail.send.sent", "mail.send.failed"}) {
|
||||
t.Fatalf("top-level emits: %v", m.Emits)
|
||||
}
|
||||
|
||||
send := byID["send"]
|
||||
if send == nil || send["type"] != "process" || send["user"] != "mesh-mail-send" {
|
||||
t.Fatalf("the process send: %v", send)
|
||||
}
|
||||
run, _ := send["run"].([]any)
|
||||
if len(run) != 2 || run[0] != "./mesh-mail" || run[1] != "send" {
|
||||
t.Fatalf("send's run: %v", send["run"])
|
||||
}
|
||||
env, _ := send["env"].(map[string]any)
|
||||
keepEnv, _ := byID["keep"]["env"].(map[string]any)
|
||||
for _, k := range []string{"MESH_MAIL_ACCOUNTS", "MESH_MAIL_SECRETS"} {
|
||||
if env[k] != keepEnv[k] {
|
||||
t.Fatalf("send's %s is %v, keep's is %v", k, env[k], keepEnv[k])
|
||||
}
|
||||
}
|
||||
h, _ := send["health"].(map[string]any)
|
||||
if h["kind"] != "unit" {
|
||||
t.Fatalf("send's health: %v", send["health"])
|
||||
}
|
||||
bus, _ := send["bus"].(map[string]any)
|
||||
if bus == nil {
|
||||
t.Fatalf("send declares no bus")
|
||||
}
|
||||
if !reflect.DeepEqual(toStrings(bus["emits"]), []string{"mail.send.sent", "mail.send.failed"}) ||
|
||||
!reflect.DeepEqual(toStrings(bus["queues"]), []string{"mail-sends"}) ||
|
||||
!reflect.DeepEqual(toStrings(bus["state"]), []string{"sends"}) || bus["asks"] != true ||
|
||||
!reflect.DeepEqual(toStrings(bus["invokes"]), []string{"seat:operator-channel.tell"}) {
|
||||
t.Fatalf("send's bus: %+v", bus)
|
||||
}
|
||||
|
||||
// On novox the operator account's state directory is 0700, so mesh-mail-send cannot reach its own 0600
|
||||
// files inside it: the state directory becomes 0711 (passable, not listable) and accounts.json (server
|
||||
// names and addresses, no password) becomes 0644, so the sender can read the outgoing server. No password
|
||||
// file's mode widens (the lead's decision, issue 1172).
|
||||
if byID["state"]["mode"] != "0711" {
|
||||
t.Fatalf("the state directory's mode: %v; want 0711", byID["state"]["mode"])
|
||||
}
|
||||
if byID["accounts"]["mode"] != "0644" {
|
||||
t.Fatalf("accounts.json's mode: %v; want 0644", byID["accounts"]["mode"])
|
||||
}
|
||||
if byID["grants"]["mode"] != "0600" {
|
||||
t.Fatalf("grants.json's mode widened to %v; it holds no password, but was not asked to widen", byID["grants"]["mode"])
|
||||
}
|
||||
}
|
||||
|
||||
func toStrings(v any) []string {
|
||||
list, _ := v.([]any)
|
||||
out := make([]string, 0, len(list))
|
||||
for _, item := range list {
|
||||
if s, ok := item.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
// The SDK pin a process's own bus connection needs (novox/hq ADR 0436 §4, issue 1172): mesh-mail's go.mod
|
||||
// requires mesh-sdk go at the release-2 commit (main 34392e7) and go 1.26.0, and go/process is importable.
|
||||
// The blank import below is what proves importability at compile time; go vet (run before every commit, per
|
||||
// the context file) already fails the build if it is not.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "git.novox.be/novox/mesh-sdk/go/process"
|
||||
)
|
||||
|
||||
// 1172/AC1: given mesh-mail's go.mod, when built, then it requires mesh-sdk go at
|
||||
// v0.1.14-0.20261019062050-34392e74916c and go 1.26.0, and go/process is importable (the import above).
|
||||
func TestGoModPinsTheSDKsProcessRelease(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../go.mod")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mod := string(raw)
|
||||
const wantSDK = "git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019062050-34392e74916c"
|
||||
if !strings.Contains(mod, wantSDK) {
|
||||
t.Fatalf("go.mod does not require %q:\n%s", wantSDK, mod)
|
||||
}
|
||||
if !strings.Contains(mod, "go 1.26.0") {
|
||||
t.Fatalf("go.mod does not declare go 1.26.0:\n%s", mod)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user