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
117 lines
6.6 KiB
Go
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")
|
|
}
|
|
}
|