package inventory import ( "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // The seat set is data the control plane owns (novox/hq ADR 0122): seeded from the binary's // defaults, read back as the working set, and thereafter an operator's to change without a rebuild. func TestSeatsAreSeededFromTheDefaultsAndReadBack(t *testing.T) { inv := ForTest(t) defaults := catalogue.DefaultSeats() added, err := inv.SeedSeats(t.Context(), defaults) if err != nil { t.Fatal(err) } if added != len(defaults) { t.Fatalf("seeded %d of %d seats", added, len(defaults)) } got, err := inv.Seats(t.Context()) if err != nil { t.Fatal(err) } if len(got) != len(defaults) { t.Fatalf("read back %d seats, seeded %d", len(got), len(defaults)) } // The set round-trips: name, scope and what it delivers survive the store. by := map[string]catalogue.Seat{} for _, s := range got { by[s.Name] = s } for _, d := range defaults { if by[d.Name].Scope != d.Scope || by[d.Name].Delivers != d.Delivers { t.Errorf("%s came back as %+v, seeded %+v", d.Name, by[d.Name], d) } } } // Re-seeding an already-seeded set adds nothing and changes nothing — every deploy runs SeedSeats, // and a mesh already holding the set must be left exactly as it is (an operator's edit to a row // included). A row's fields are not overwritten: on conflict the insert does nothing. // // (Phase 1 keys the table by name, so a seat *renamed* in the table would have its old name // re-seeded — the move to a stable id a rename does not touch is the next step, ADR 0122. This test // asserts only the property that holds now: an unchanged set re-seeds to a no-op.) func TestReSeedingAnUnchangedSetIsANoOp(t *testing.T) { inv := ForTest(t) defaults := catalogue.DefaultSeats() if _, err := inv.SeedSeats(t.Context(), defaults); err != nil { t.Fatal(err) } // An operator changes a row's scope in the table — the point of it being data. if _, err := inv.store.Pool().Exec(t.Context(), `update seat set scope = 'mesh' where name = 'node-uplink'`); err != nil { t.Fatal(err) } added, err := inv.SeedSeats(t.Context(), defaults) if err != nil { t.Fatal(err) } if added != 0 { t.Fatalf("re-seeding an already-present set added %d rows", added) } got, err := inv.Seats(t.Context()) if err != nil { t.Fatal(err) } for _, s := range got { if s.Name == "node-uplink" && s.Scope != "mesh" { t.Fatalf("re-seeding overwrote the operator's change: node-uplink scope is %q", s.Scope) } } } // A rename is one operation: the seat gets the new name, the old name becomes an alias that still // resolves to it (novox/hq ADR 0122). func TestRenameSeatKeepsTheFormerNameAsAnAlias(t *testing.T) { inv := ForTest(t) if _, err := inv.SeedSeats(t.Context(), catalogue.DefaultSeats()); err != nil { t.Fatal(err) } if err := inv.RenameSeat(t.Context(), "node-packet-filter", "node-firewall"); err != nil { t.Fatal(err) } seats, err := inv.Seats(t.Context()) if err != nil { t.Fatal(err) } names := map[string]bool{} for _, s := range seats { names[s.Name] = true } if !names["node-firewall"] || names["node-packet-filter"] { t.Fatalf("the seat was not renamed in place: %v", names) } aliases, err := inv.Aliases(t.Context()) if err != nil { t.Fatal(err) } if aliases["node-packet-filter"] != "node-firewall" { t.Fatalf("the former name is not an alias of the new one: %v", aliases) } // Renaming what has no seat is refused; renaming to the same name is refused. if err := inv.RenameSeat(t.Context(), "no-such-seat", "x"); err == nil { t.Fatal("renaming a seat that does not exist was accepted") } if err := inv.RenameSeat(t.Context(), "node-firewall", "node-firewall"); err == nil { t.Fatal("renaming a seat to its own name was accepted") } }