355 lines
14 KiB
Go
355 lines
14 KiB
Go
//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)
|
|
}
|
|
}
|