Files
mesh-controller/internal/broker/log_test.go
T
jschoubben 09d2cc5056
mesh/delivery merged: source.main moved to novox/mesh-controller@350f8ebdfe5f
mesh/delivery-group group feat/1945-user-list-switch merged: every member is merged
mesh/check checking 09d2cc5056ef on 47e64f0284a9 | novox/mesh-controller#470 in group feat/1945-user-list-switch
Broker: the bus node may follow the grants part's sealed user list and the grants part may write it, the operator's second one-grant exception (issue 1945, ADR 0547 clauses 7-8)
2026-10-26 13:03:15 +01:00

251 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package broker
import (
"encoding/json"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/nats-io/nats.go/jetstream"
)
// **The stream a log is, field by field** (novox/hq ADR 0297 §2, the shared contract): a file stream
// kept by limits, no maximum age, no cap per key, the declared cap, a message of at most 260 KiB (a
// value of 256 KiB and 4 KiB of headers),
// refusing what comes next when full, read directly, refusing a delete or a purge, one replica, and
// a description that says whose it is and why.
func TestALogsStreamIsAsTheDecisionFixesIt(t *testing.T) {
got := Log{Module: "mesh-issues", Name: "changes"}.Config()
want := jetstream.StreamConfig{
Name: "LOG_mesh-issues_changes",
Description: got.Description,
Subjects: []string{"mesh.log.mesh-issues.changes.>"},
Storage: jetstream.FileStorage,
Retention: jetstream.LimitsPolicy,
MaxAge: 0,
MaxMsgs: -1,
MaxMsgsPerSubject: -1,
MaxBytes: 1024 * 1024 * 1024,
MaxMsgSize: 260 * 1024,
Discard: jetstream.DiscardNew,
AllowDirect: true,
DenyDelete: true,
DenyPurge: true,
Replicas: 1,
}
a, _ := json.Marshal(got)
b, _ := json.Marshal(want)
if string(a) != string(b) {
t.Fatalf("the log's stream is\n %s\nwant\n %s", a, b)
}
if !strings.Contains(got.Description, "mesh-issues") || !strings.Contains(got.Description, "ADR 0297") {
t.Fatalf("the description does not say whose the log is and why: %q", got.Description)
}
if c := (Log{Module: "m", Name: "n", MaxMiB: 8192}).Config(); c.MaxBytes != 8192*1024*1024 {
t.Fatalf("a cap of 8192 MiB is %d bytes", c.MaxBytes)
}
}
// **The membership names each of the owner's logs** with exactly the field names the runtime reads:
// `logs`, and in each `name`, `stream`, `subject`, `writes`. A module with no log is issued none, and
// the field is absent.
func TestAMembershipListsItsModulesLogs(t *testing.T) {
m := MembershipFor("one", Declared{Module: "mesh-issues",
Logs: []Log{{Module: "mesh-issues", Name: "changes"}}}, Placements{})
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
var back map[string]json.RawMessage
if err := json.Unmarshal(raw, &back); err != nil {
t.Fatal(err)
}
if got := string(back["logs"]); got !=
`[{"name":"changes","stream":"LOG_mesh-issues_changes","subject":"mesh.log.mesh-issues.changes","writes":true}]` {
t.Fatalf("the membership's logs are %s", got)
}
none, _ := json.Marshal(MembershipFor("one", Declared{Module: "audit"}, Placements{}))
if strings.Contains(string(none), `"logs"`) {
t.Fatalf("a module with no log was issued logs: %s", none)
}
}
// logGrantsOf is the grants of a principal that are about logs.
func logGrantsOf(publish []string) []string {
var out []string
for _, s := range publish {
if strings.HasPrefix(s, "mesh.log.") || strings.Contains(s, ".LOG_") {
out = append(out, s)
}
}
slices.Sort(out)
return out
}
// **The runtime is granted exactly the contract's subjects for each log it carries, and nothing else
// of any log**: appending under the log's subjects, binding to its stream, reading it directly. No
// consumer, no delete, no purge, and nothing of a log its modules do not keep.
func TestTheRuntimeIsGrantedItsModulesLogsAndNoMore(t *testing.T) {
perms, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "one", Module: RuntimeModule,
Carries: []Declared{
{Module: "mesh-issues", Logs: []Log{{Module: "mesh-issues", Name: "changes"}}},
{Module: "audit"},
}, PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
want := []string{
"$JS.API.DIRECT.GET.LOG_mesh-issues_changes",
"$JS.API.DIRECT.GET.LOG_mesh-issues_changes.>",
"$JS.API.STREAM.INFO.LOG_mesh-issues_changes",
"mesh.log.mesh-issues.changes.>",
}
if got := logGrantsOf(perms.Publish); !slices.Equal(got, want) {
t.Fatalf("the runtime is granted\n %q\nwant\n %q", got, want)
}
for _, s := range perms.Subscribe {
if strings.HasPrefix(s, "mesh.log.") || strings.Contains(s, "LOG_") {
t.Fatalf("the runtime subscribes a log's subjects: %q", s)
}
}
// A module running on its own account is granted the same for its own logs.
own, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "mesh-issues",
Logs: []string{"changes"}, PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
if got := logGrantsOf(own.Publish); !slices.Equal(got, want) {
t.Fatalf("the module's own account is granted\n %q\nwant\n %q", got, want)
}
// And a module with no log, nothing of any.
none, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "audit", PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
if got := logGrantsOf(none.Publish); len(got) != 0 {
t.Fatalf("a module with no log is granted %q", got)
}
}
// A log name or module that is not one plain token is issued and granted nothing rather than a
// pattern that happens to parse.
func TestALogThatNamesNoStreamGrantsNothing(t *testing.T) {
if got := logGrants("a", []string{"x.y", "x>", "*", ""}); len(got) != 0 {
t.Fatalf("granted %v for logs that name no stream", got)
}
if got := logGrants("a.b", []string{"c"}); len(got) != 0 {
t.Fatalf("granted %v for a module that is not one token", got)
}
}
type logs struct {
ensured []string
on []string
}
func (l *logs) EnsureLog(x Log) error { l.ensured = append(l.ensured, x.Stream()); return nil }
func (l *logs) LogStreams() ([]string, error) { return l.on, nil }
// Every declared log is asserted; one on the server that nothing declares is said, not removed — and
// the asserter has no way to remove one.
func TestRaisingLogsReportsWhatNothingDeclares(t *testing.T) {
l := &logs{on: []string{"LOG_mesh-issues_changes", "LOG_gone_old", "KV_not_a_log"}}
undeclared, err := RaiseLogs(l, []Log{{Module: "mesh-issues", Name: "changes"}, {Module: "a", Name: "b"}})
if err != nil {
t.Fatal(err)
}
if !slices.Equal(l.ensured, []string{"LOG_a_b", "LOG_mesh-issues_changes"}) {
t.Fatalf("asserted %v", l.ensured)
}
if !slices.Equal(undeclared, []string{"LOG_gone_old"}) {
t.Fatalf("reported %v", undeclared)
}
}
// **No path of the mesh deletes or purges a log or a bucket, or recreates one** (novox/hq ADR 0297 §2,
// ADR 0201 §15–16): no code of this repository outside its tests calls the client's stream or bucket
// delete, or purges a stream, but for the controller lease's own stream, which purges its own history
// below a sequence (internal/lease), and the controller's record mirrors, which purge the key of a record
// the store no longer holds (internal/recordmirror; novox/hq ADR 0289, design 54 §2), and the buckets of the plans'
// state the controller deletes once at its first start (ADR 0539 clause 13). A new call is a decision, not a refactor.
func TestNoPathDeletesALogOrABucket(t *testing.T) {
removers := map[string]bool{"DeleteStream": true, "DeleteKeyValue": true, "PurgeStream": true,
"DeleteObjectStore": true}
root := filepath.Join("..", "..")
var found []string
for _, dir := range []string{"cmd", "internal"} {
err := filepath.WalkDir(filepath.Join(root, dir), func(path string, e fs.DirEntry, err error) error {
if err != nil || e.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return err
}
f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
if err != nil {
return err
}
ast.Inspect(f, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
name := sel.Sel.Name
decided := strings.Contains(filepath.ToSlash(path), "internal/lease/") ||
strings.Contains(filepath.ToSlash(path), "internal/recordmirror/")
// And the buckets of the plans' state release 6 deleted, at the controller's first start, each said (novox/hq
// ADR 0539 clause 13): the one decided deletion of a bucket.
release6 := strings.HasSuffix(filepath.ToSlash(path), "cmd/mesh-controller/composed_start.go") &&
(name == "DeleteKeyValue" || name == "DeleteStream") // and the declarations' stream once no node reads one (clause 11)
if (removers[name] && !release6) || (name == "Purge" && !decided) {
found = append(found, path+": "+name)
}
return true
})
for _, lit := range stringsIn(f) {
if strings.Contains(lit, "$JS.API.STREAM.DELETE") || strings.Contains(lit, "$JS.API.STREAM.PURGE") {
// Only the writers table names it, as a subject no one but the controller may publish.
// And the operator's release 6 hand path, whose object store purges a replaced object's chunks in a node's
// object bucket (novox/hq ADR 0539 clause 10): a grant, never a call of this repository.
// And the grants part's own object bucket of sealed user lists, whose put purges a replaced object's chunks
// (novox/hq ADR 0547 clause 7): a grant, never a call of this repository.
if !strings.HasSuffix(filepath.ToSlash(path), "internal/broker/writers.go") &&
!strings.HasSuffix(filepath.ToSlash(path), "internal/broker/composed.go") &&
!strings.HasSuffix(filepath.ToSlash(path), "internal/broker/userlist_switch.go") {
found = append(found, path+": "+lit)
}
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
if len(found) > 0 {
t.Fatalf("a path of the mesh removes a stream, a bucket or what one holds:\n %s", strings.Join(found, "\n "))
}
}
// stringsIn is every string literal of a file.
func stringsIn(f *ast.File) []string {
var out []string
ast.Inspect(f, func(n ast.Node) bool {
if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.STRING {
out = append(out, lit.Value)
}
return true
})
return out
}
// Against a real server: a log is created as its configuration says, asserting it again keeps what
// it holds, a changed cap is brought to match in place, an entry cannot be deleted from it, and one
// nothing declares any more is reported and stays.