//go:build integration package main import ( "encoding/json" "fmt" "sort" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/compose" "github.com/novox/mesh-controller/internal/inventory" ) // tableState is every table's rows and every sequence's last value, as the adapter's reads must leave them. func tableState(t *testing.T, inv *inventory.Inventory) map[string]string { t.Helper() out := map[string]string{} for _, row := range inv.QueryForTest(t, `select table_name from information_schema.tables where table_schema = 'public' and table_type = 'BASE TABLE' order by table_name`) { name := strings.Trim(row[0], `"`) count := inv.QueryForTest(t, fmt.Sprintf(`select count(*)::text from %q`, name)) out["rows:"+name] = fmt.Sprint(count) digest := inv.QueryForTest(t, fmt.Sprintf(`select coalesce(md5(string_agg(t::text, '|' order by t::text)), '') from %q t`, name)) out["content:"+name] = fmt.Sprint(digest) } for _, row := range inv.QueryForTest(t, `select sequencename, coalesce(last_value::text, '') from pg_sequences where schemaname = 'public' order by sequencename`) { out["sequence:"+row[0]] = row[1] } return out } // 1382/AC4 and 1383/AC6: building the Input writes nothing. The store's rows, their contents and its sequences are the same // before and after, on a mesh whose credentials are not made, whose ports are not given and which keeps a recorded build. func TestBuildingTheInputWritesNothing(t *testing.T) { open, _ := aMeshWithSecrets(t) ctx := t.Context() inv := open.inventory // A consumer whose credential nothing has made yet, on a mesh that already composed. register(t, open, catalogue.Manifest{Module: "reports", Version: "1", Requires: []string{"s3-bucket"}, Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/reports/config.json", "mode": "0600", "content": "{}", "merge": "json"}}}) if _, err := inv.Assign(ctx, "laptop", "reports"); err != nil { t.Fatal(err) } before := tableState(t, inv) if _, err := readComposeInput(ctx, open); err != nil { t.Fatal(err) } after := tableState(t, inv) var moved []string for k, v := range after { if before[k] != v { moved = append(moved, k) } } sort.Strings(moved) if len(moved) > 0 { t.Fatalf("building the Input wrote: %v", moved) } } // a consumer of the s3-bucket aMeshWithSecrets' provider offers, with a file the credential goes into. func aConsumerOfTheBucket(name string) catalogue.Manifest { return catalogue.Manifest{Module: name, Version: "1", Requires: []string{"s3-bucket"}, Secrets: map[string]string{"s3-bucket": "/etc/" + name + "/s3"}, Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/" + name + "/config.json", "mode": "0600", "content": "{}", "merge": "json"}}} } // 1383/AC1: of a provider's three consumers, two have their credentials made and one has not: Issued holds the two rows, with // the copy sealed to the provider, the third is a credential the next send makes, and no row was inserted for it (planFor // inserted it, plan.go: SecretFor in every mode). func TestAPairCredentialNoRowHoldsIsAWouldMakeAndNoRowIsInserted(t *testing.T) { open, _ := aMeshWithSecrets(t) ctx := t.Context() inv := open.inventory register(t, open, aConsumerOfTheBucket("reports")) if _, err := inv.Assign(ctx, "laptop", "reports"); err != nil { t.Fatal(err) } // A send makes the credentials of files and reports; a question makes none. if _, err := composeSend(ctx, open, "laptop"); err != nil { t.Fatal(err) } register(t, open, aConsumerOfTheBucket("metrics")) if _, err := inv.Assign(ctx, "laptop", "metrics"); err != nil { t.Fatal(err) } before := tableState(t, inv) in, err := readComposeInput(ctx, open) if err != nil { t.Fatal(err) } var rows []compose.Issued for _, r := range in.Issued { if r.Provider == "anchor" && r.Provision == "s3-bucket" { rows = append(rows, r) if r.ForProvider == "" { t.Errorf("the row of %s carries no copy sealed to the provider", r.ConsumerModule) } } } if len(rows) != 2 { t.Fatalf("Issued holds %d row(s) for anchor's s3-bucket, want files and reports: %+v", len(rows), rows) } var would []string for _, p := range in.wouldMake { would = append(would, p.ConsumerModule) } if len(would) != 1 || would[0] != "metrics" { t.Fatalf("the credentials the next send makes are %v, want metrics", would) } for _, c := range in.Credentials { if c.ConsumerModule == "metrics" { t.Errorf("a credential nobody holds has an entry: %+v", c) } } after := tableState(t, inv) for k, v := range after { if before[k] != v { t.Errorf("reading the Input moved %s", k) } } } // 1383/AC2: a credential only a person gives that nobody gave is not given, and so is one given under a key the machine no // longer has; a credential given by a person for the pair and sealed to a key one end no longer has is refused: in the words // plan.go and secrets.go use. func TestACredentialOnlyAPersonGivesIsNotGivenAndAGivenOneUnderAnOldKeyIsSaidSo(t *testing.T) { open := aMesh(t) ctx := t.Context() inv := open.inventory register(t, open, catalogue.Manifest{Module: "indexers", Version: "1", Provides: []catalogue.Offer{{ Name: "indexer-api", Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}}) register(t, open, catalogue.Manifest{Module: "grabber", Version: "1", Requires: []string{"indexer-api"}, Secrets: map[string]string{"indexer-api": "/etc/grabber/key"}}) register(t, open, aConsumerOfTheBucket("ledger")) register(t, open, catalogue.Manifest{Module: "objects", Version: "1", Provides: catalogue.FromAnywhere("s3-bucket")}) for _, a := range [][2]string{{"anchor", "indexers"}, {"laptop", "grabber"}, {"anchor", "objects"}, {"laptop", "ledger"}} { if _, err := inv.Assign(ctx, a[0], a[1]); err != nil { t.Fatalf("assign %s %s: %v", a[0], a[1], err) } } stateOf := func(provision, consumerModule string) (compose.Credential, bool) { in, err := readComposeInput(ctx, open) if err != nil { t.Fatal(err) } for _, c := range in.Credentials { if c.Provision == provision && c.ConsumerModule == consumerModule { return c, true } } return compose.Credential{}, false } c, found := stateOf("indexer-api", "grabber") if !found || c.State != compose.CredentialNotGiven || !strings.Contains(c.Why, "nobody gave it") { t.Fatalf("a credential nobody gave reads as %+v", c) } if err := inv.AcceptSecretForPair(ctx, "indexer-api", "laptop", "grabber", "anchor", "", "the-key"); err != nil { t.Fatal(err) } if c, found := stateOf("indexer-api", "grabber"); !found || c.State != compose.CredentialHeld || c.Sealed == "" { t.Fatalf("a credential a person gave reads as %+v", c) } // The consumer's machine generated a key of its own since. if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil { t.Fatal(err) } if c, found := stateOf("indexer-api", "grabber"); !found || c.State != compose.CredentialNotGiven || !strings.Contains(c.Why, "a sealing key one end no longer has") { t.Fatalf("a credential given under a key the machine no longer has reads as %+v", c) } // A credential for a pair, given by a person, under a key one end has since replaced: the mesh cannot make another. if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil { t.Fatal(err) } if err := inv.AcceptSecretForPair(ctx, "s3-bucket", "laptop", "ledger", "anchor", "", "the-bucket-key"); err != nil { t.Fatal(err) } if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil { t.Fatal(err) } if c, found := stateOf("s3-bucket", "ledger"); !found || c.State != compose.CredentialRefused || !strings.Contains(c.Why, "was accepted from a person, and a sealing key at one end has changed since") { t.Fatalf("a credential given for the pair under an old key reads as %+v", c) } } // 1383/AC3: the own secrets carry their origin, the key they are sealed to and the sealed value; the vault's kept boxes, the // licences (with their keys given, so an empty key means the mesh holds none), the users the bus has minted and the // memberships equal what the inventory answers. func TestTheAdaptersInputCarriesTheOwnSecretsTheBoxesTheLicencesAndTheBus(t *testing.T) { open, _ := aMeshWithSecrets(t) ctx := t.Context() inv := open.inventory key, err := inv.SealingKeyOf(ctx, "anchor") if err != nil { t.Fatal(err) } sealed, err := inv.SecretForModule(ctx, "anchor", "objects", "admin") if err != nil { t.Fatal(err) } box := json.RawMessage(`{"v":1,"eph":"e","iv":"i","tag":"t","ct":"c"}`) if err := inv.KeepCoreBox(ctx, "anchor", inventory.CoreBox{Module: "objects", Name: "admin", Generation: 4, SealedTo: key, Box: box}); err != nil { t.Fatal(err) } if err := inv.SeedBusUser(ctx, inventory.BusUser{Username: "anchor.objects", Kind: "module", Node: "anchor", Module: "objects"}, "a-password"); err != nil { t.Fatal(err) } if err := inv.PutBusMembership(ctx, "laptop", "sealed-membership"); err != nil { t.Fatal(err) } held, err := open.Licences(ctx) if err != nil { t.Fatal(err) } if err := held.Add(ctx, "claude", "static", map[string]any{"model": "x"}); err != nil { t.Fatal(err) } in, err := readComposeInput(ctx, open) if err != nil { t.Fatal(err) } var own *compose.OwnSecretRow for i := range in.OwnSecrets { if r := &in.OwnSecrets[i]; r.Node == "anchor" && r.Module == "objects" && r.Name == "admin" { own = r } } if own == nil || own.Origin != "made" || own.SealedTo != key || own.Sealed != sealed { t.Errorf("the own secret reads as %+v; the inventory holds %q sealed to %q", own, sealed, key) } var kept *compose.CoreBox for i := range in.KeptCoreBoxes { if b := &in.KeptCoreBoxes[i]; b.Node == "anchor" && b.Module == "objects" && b.Name == "admin" { kept = b } } if kept == nil || kept.Generation != 4 || kept.SealedTo != key || string(kept.Box) != string(box) { t.Errorf("the kept box reads as %+v", kept) } if got := strings.Join(in.BusMinted, ","); !strings.Contains(got, "anchor.objects") { t.Errorf("BusMinted is %v", in.BusMinted) } if in.BusMemberships["laptop"] != "sealed-membership" { t.Errorf("BusMemberships is %v", in.BusMemberships) } if len(in.Licences) != 1 || in.Licences[0].Name != "claude" || in.Licences[0].Keys == nil { t.Errorf("Licences read as %+v", in.Licences) } } // 1383/AC4: the bus's user list is what busUserList gives for the machine that runs the bus, and the Input carries none when // no module of the shelf runs the bus. func TestTheBusUsersAreWhatTheControllerComposesForTheMachineThatRunsTheBus(t *testing.T) { open, _ := aMeshWithSecrets(t) ctx := t.Context() in, err := readComposeInput(ctx, open) if err != nil { t.Fatal(err) } if in.BusUsers != "" || len(in.BusMissing) != 0 { t.Fatalf("a mesh whose shelf runs no bus carries a bus user list: %q %v", in.BusUsers, in.BusMissing) } bus := catalogue.Manifest{Module: "nats", Version: "1", BusUsers: "/etc/nats/users.conf", Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} register(t, open, bus) if err := open.inventory.SeedBusUser(ctx, inventory.BusUser{Username: "controller", Kind: "controller"}, "pw"); err != nil { t.Fatal(err) } in, err = readComposeInput(ctx, open) if err != nil { t.Fatal(err) } list, missing, _ := busUserList(ctx, open.inventory, []catalogue.Manifest{bus}) if in.BusUsers != list || fmt.Sprint(in.BusMissing) != fmt.Sprint(missing) { t.Errorf("the Input carries %q (%v); busUserList gives %q (%v)", in.BusUsers, in.BusMissing, list, missing) } } // 1383/AC5: Built, CurrentBuilds and BuildIDs equal the inventory's Held, CurrentBuilds and BuildOf, and the serving key and // the certificate are the machine's. func TestTheAdaptersInputCarriesTheBuildsAndTheMachinesKey(t *testing.T) { open, _ := aMeshWithSecrets(t) ctx := t.Context() inv := open.inventory in, err := readComposeInput(ctx, open) if err != nil { t.Fatal(err) } held, err := inv.Held(ctx) if err != nil { t.Fatal(err) } var wantBuilt []string for repository := range held { wantBuilt = append(wantBuilt, repository) } sort.Strings(wantBuilt) if fmt.Sprint(in.Built) != fmt.Sprint(wantBuilt) && !(len(in.Built) == 0 && len(wantBuilt) == 0) { t.Errorf("Built is %v, the inventory's %v", in.Built, wantBuilt) } current, err := inv.CurrentBuilds(ctx) if err != nil { t.Fatal(err) } for module, b := range current { if in.CurrentBuilds[module] != b.Commit { t.Errorf("CurrentBuilds[%s] is %q, the inventory's %q", module, in.CurrentBuilds[module], b.Commit) } } for module, byCommit := range in.BuildIDs { for commit, build := range byCommit { if want, _ := inv.BuildOf(ctx, module, commit); want != build { t.Errorf("BuildIDs[%s][%s] is %q, the inventory's %q", module, commit, build, want) } } } } // The lead's answer 4: where the mesh holds no credential for any bus user, the machine that runs the bus is refused with // busUserList's words (the controller's composition refused it), the Input still builds, and another machine composes. func TestTheBusMachineIsRefusedWhereNoUserHasACredentialAndTheOthersCompose(t *testing.T) { open, _ := aMeshWithSecrets(t) ctx := t.Context() bus := catalogue.Manifest{Module: "nats", Version: "1", BusUsers: "/etc/nats/users.conf", Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} register(t, open, bus) register(t, open, aWebModule()) if _, err := assign(ctx, open, "anchor", "nats"); err != nil { t.Fatal(err) } if _, err := assign(ctx, open, "laptop", "web"); err != nil { t.Fatal(err) } if _, _, err := composeQuestion(ctx, open, "anchor", compose.Foreseeing, false); err == nil || !strings.Contains(err.Error(), "not one user has a credential") { t.Fatalf("the bus machine is composed: %v", err) } if _, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err != nil { t.Fatalf("a machine that does not run the bus is refused: %v", err) } }