Files
mesh-sdk/go/seatdef/seatdef_test.go
T
jschoubben a410447ff5 SD-M1: a holder's event kept in its own stream is refused under its module's subjects
A family on a holder's event names the holder's own stream, so its subject is mesh.stream.<holder>.event.<event>;
operator-channel's offered and closed said the router's stream but mesh.mod subjects.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:31:16 +01:00

117 lines
6.6 KiB
Go

package seatdef
import (
"errors"
"strings"
"testing"
)
// TestADefinitionIsReadStrictlyNamingWhatIsWrong is the format's own table (design 26: a valid definition parses;
// each missing required field is refused naming it; one over 256 KiB is refused), with the parts and fixtures beside.
func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
const listGrain = `{"format":1,"seat":"node-hostname","scope":"node","grain":"seat-list","decision":"ADR 1",
"verbs":[{"name":"add","input":{"type":"object"}}]}`
const whole = `{"format":1,"seat":"mesh-broker","scope":"mesh","grain":"whole","decision":"ADR 1",
"verbs":[{"name":"list","input":{"type":"object"},"answer":{"type":"object"}}],
"events":[{"name":"created","body":{"type":"object"}}],"family":{"name":"broker.events","max-age-days":7}}`
cases := []struct {
name, raw string
refused string // a text the refusal names, empty when it parses
}{
{"the seat list's grain parses", listGrain, ""},
{"a whole definition parses", whole, ""},
{"no format", `{"seat":"a-b","scope":"mesh","grain":"seat-list","decision":"x"}`, "format: missing"},
{"a later format", `{"format":2,"seat":"a-b","scope":"mesh","grain":"seat-list","decision":"x"}`, "format 2"},
{"no seat", `{"format":1,"scope":"mesh","grain":"seat-list","decision":"x"}`, "seat: missing"},
{"no scope", `{"format":1,"seat":"a-b","grain":"seat-list","decision":"x"}`, "scope: missing"},
{"another scope", `{"format":1,"seat":"a-b","scope":"room","grain":"seat-list","decision":"x"}`, "scope"},
{"no grain", `{"format":1,"seat":"a-b","scope":"mesh","decision":"x"}`, "grain: missing"},
{"no decision", `{"format":1,"seat":"a-b","scope":"mesh","grain":"seat-list"}`, "decision: missing"},
{"an unknown field", `{"format":1,"seat":"a-b","scope":"mesh","grain":"seat-list","decision":"x","sevres":[]}`,
"sevres: a field the format does not know"},
{"a whole verb with no answer", strings.Replace(whole, `,"answer":{"type":"object"}`, "", 1),
"verbs[0].answer: missing"},
{"a whole verb with no input", strings.Replace(whole, `"input":{"type":"object"},`, "", 1),
"verbs[0].input: missing"},
{"a whole event with no body", strings.Replace(whole, `"name":"created","body":{"type":"object"}`,
`"name":"created"`, 1), "events[0].body: missing"},
{"whole events kept nowhere", strings.Replace(whole, `,"family":{"name":"broker.events","max-age-days":7}`,
"", 1), "family: missing"},
{"a family kept longer than the shared events stream", strings.Replace(whole, `"max-age-days":7`,
`"max-age-days":30`, 1), "family.max-age-days: missing, or not 7"},
{"a family with a cap of its own", strings.Replace(whole, `"max-age-days":7`, `"max-age-days":7,"max-mib":256`, 1),
"max-mib: a field the format does not know"},
{"a holder's event on its own stream", strings.Replace(whole, `"family":`, `"holder-events":[{"name":"verdict",`+
`"subject":"mesh.stream.<holder>.event.verdict","family":"checker.asks"}],"family":`, 1), ""},
{"a holder's event on its own stream with no family", strings.Replace(whole, `"family":`, `"holder-events":[{"name":"verdict",`+
`"subject":"mesh.stream.<holder>.event.verdict"}],"family":`, 1), "no family names that stream"},
{"a holder's event kept in its own stream under its module's subjects", strings.Replace(whole, `"family":`, `"holder-events":[{"name":"verdict",`+
`"subject":"mesh.mod.<holder>.event.verdict","family":"checker.asks"}],"family":`, 1), "a family names the holder's own stream"},
{"a holder's event on a subject of no module", strings.Replace(whole, `"family":`, `"holder-events":[{"name":"verdict",`+
`"subject":"mesh.event.verdict"}],"family":`, 1), "holder-events[0].subject: not mesh.mod.<holder>.event.verdict"},
{"a verb twice", strings.Replace(listGrain, `]}`, `,{"name":"add"}]}`, 1), `"add" twice`},
{"records at the seat list's grain", strings.Replace(listGrain, `"decision":"ADR 1",`,
`"decision":"ADR 1","records":[{"name":"asks"}],`, 1), "grain: seat-list holds"},
{"over 256 KiB", listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) + `"}`,
ErrTooLarge.Error()},
}
for _, c := range cases {
_, err := Parse([]byte(c.raw))
switch {
case c.refused == "" && err != nil:
t.Errorf("%s: refused: %v", c.name, err)
case c.refused != "" && err == nil:
t.Errorf("%s: parsed, want refused naming %q", c.name, c.refused)
case c.refused != "" && !strings.Contains(err.Error(), c.refused):
t.Errorf("%s: refused with %q, want it to name %q", c.name, err, c.refused)
}
}
if _, err := Parse([]byte(listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) +
`"}`)); !errors.Is(err, ErrTooLarge) {
t.Errorf("over 256 KiB is ErrTooLarge, got %v", err)
}
// A part is taken in whole, its verbs before the definition's own; a part not given, or a type defined twice,
// is refused.
part, err := ParsePart([]byte(`{"format":1,"part":"registry","verbs":[{"name":"publish","input":{"type":"object"},
"answer":{"type":"object"}}],"events":[{"name":"published","body":{"type":"object"}}],
"defs":{"Ref":{"type":"string"}}}`))
if err != nil {
t.Fatal(err)
}
d, err := Parse([]byte(strings.Replace(whole, `"decision":"ADR 1",`, `"decision":"ADR 1","includes":["registry"],`, 1)))
if err != nil {
t.Fatal(err)
}
got, err := Resolve(d, map[string]Part{"registry": part})
if err != nil {
t.Fatal(err)
}
if v := VerbNames(got.Verbs); strings.Join(v, ",") != "publish,list" {
t.Errorf("resolved verbs %v, want the part's first", v)
}
if m := MessageNames(got.Events); strings.Join(m, ",") != "published,created" || got.Defs["Ref"] == nil {
t.Errorf("resolved events %v, defs %v", m, got.Defs)
}
if _, err := Resolve(d, nil); err == nil || !strings.Contains(err.Error(), `part "registry" is not given`) {
t.Errorf("a part not given: %v", err)
}
d.Defs = map[string]map[string]any{"Ref": {"type": "string"}}
if _, err := Resolve(d, map[string]Part{"registry": part}); err == nil || !strings.Contains(err.Error(), "defs.Ref") {
t.Errorf("a type defined twice: %v", err)
}
// Fixtures name what they are an instance of.
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list.input","given":{}},{"name":"b","of":"event:created",
"subject":"mesh.seat.mesh-broker.event.created","given":{}}]`)); err != nil {
t.Errorf("fixtures: %v", err)
}
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list","given":{}}]`)); err == nil ||
!strings.Contains(err.Error(), "fixtures[0].of") {
t.Errorf("a fixture of nothing known: %v", err)
}
if _, err := ParseIndex([]byte(`{"format":1,"seats":["a-b","a-b"]}`)); err == nil {
t.Error("an index naming a seat twice parsed")
}
}