Files
mesh-controller/internal/inventory/fortest.go
T
jschoubben 185d0294eb
mesh/merge-gate head 185d0294ebd4 merged into trunk 89abce5df955 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; the ch…
mesh/repo-check head 185d0294ebd4 merged into trunk 89abce5df955 — pass: its merge-check.sh passed
mesh/delivery covered after its stop: a later merge that contains it was delivered: novox/mesh-controller@0b312218f90f (merged as 791786d5 into main, wal…
mesh/check pass 185d0294ebd4 on 89abce5df955 | unit 1756 run, 0 kept, 4 skipped | integration 3034 run, 0 kept, 16 skipped | over bound: integration (…
Keep the store address's query in every test database's address, so store tests reach a socket-addressed store (issue 1889)
The checker's integration store answers on a unix socket named in the query of MESH_TEST_POSTGRES
(?host=<socket dir>&sslmode=disable). The identity, inventory and licences fixtures cut the address at
its last slash and appended ?sslmode=disable, dropping host=, so pgx dialled its default socket where no
server is: every store test waited out its 20 s readiness bound, each package ran into go test's
10-minute timeout, and the verdict named only the first failure,
identity.TestACertificateChainsToTheMeshsOwnAuthority.

store.DatabaseForTest now builds that address for every fixture, keeping the query as the template raise
already did; identity's fresh is ForTest rather than a second copy. The readiness bound and its error,
which wraps the driver's cause, are unchanged.
2026-10-23 08:55:50 +02:00

205 lines
6.4 KiB
Go

package inventory
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5"
"golang.org/x/crypto/bcrypt"
"github.com/novox/mesh-controller/internal/chain"
"github.com/novox/mesh-controller/internal/store"
)
// ForTest is a fresh inventory in a database of its own, dropped when the test ends.
//
// Exported because the check that this mesh reads what a node sends lives beside the link, and a
// second copy of this would be a second thing to keep true. It takes a *testing.T, so it cannot
// be called from anything that is not a test.
func ForTest(t *testing.T) *Inventory {
t.Helper()
admin := os.Getenv("MESH_TEST_POSTGRES")
if admin == "" {
t.Skip("no MESH_TEST_POSTGRES; run `make check` to raise one")
}
name := fmt.Sprintf("inv_%d_%s", time.Now().UnixNano()%1_000_000,
strings.ToLower(strings.NewReplacer("/", "", "-", "").Replace(t.Name())))
if len(name) > 60 {
name = name[:60]
}
// Created already migrated, a copy of a template migrated once (novox/hq issue 591); the Migrate
// below then finds nothing pending, which it does only if the copy's record matches every migration.
migrations, err := Migrations()
if err != nil {
t.Fatal(err)
}
store.CreateMigratedForTest(t, admin, name, migrations)
busPasswordCostForTest.Store(int32(bcrypt.MinCost))
t.Setenv(store.Variable(Name), store.DatabaseForTest(admin, name))
inv, err := Open(t.Context())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
inv.Close()
c, err := pgx.Connect(context.Background(), admin)
if err != nil {
return
}
defer c.Close(context.Background())
_, _ = c.Exec(context.Background(), "drop database if exists "+name+" with (force)")
})
if err := inv.Ready(t.Context(), 20*time.Second); err != nil {
t.Fatal(err)
}
if _, err := inv.store.Migrate(t.Context(), migrations); err != nil {
t.Fatal(err)
}
// The chain is on the bus in a mesh; in a test it is in memory (ADR 0408).
c := chain.InMemory(nil)
// A fresh test chain has nothing to lay out: it is laid out already, as the controller's start leaves a mesh (issue 1121).
if _, err := c.Builds.CreateRaw(t.Context(), chain.LayoutMarkerKey, []byte(`{}`)); err != nil {
t.Fatal(err)
}
inv.UseChain(c)
testChains.Store(os.Getenv(store.Variable(Name)), c)
t.Cleanup(func() { testChains.Delete(os.Getenv(store.Variable(Name))) })
return inv
}
// RemoveNodeForTest removes a machine's record as a person removing it from the store would: no verb of the
// mesh removes one yet, and what hangs off it must go with it.
func (i *Inventory) RemoveNodeForTest(ctx context.Context, name string) (bool, error) {
tag, err := i.store.Pool().Exec(ctx, `delete from node where name = $1`, name)
if err != nil {
return false, err
}
return tag.RowsAffected() == 1, nil
}
// ForTestBefore is ForTest with the store migrated only below migration `number`, and a function that runs the
// rest: for a test of a migration that moves data, which seeds the store as the mesh holds it before the
// migration, migrates, and reads what the migration made of it (novox/hq issue 651).
func ForTestBefore(t *testing.T, number int) (*Inventory, func()) {
t.Helper()
admin := os.Getenv("MESH_TEST_POSTGRES")
if admin == "" {
t.Skip("no MESH_TEST_POSTGRES; run `make check` to raise one")
}
name := fmt.Sprintf("invb_%d_%s", time.Now().UnixNano()%1_000_000,
strings.ToLower(strings.NewReplacer("/", "", "-", "").Replace(t.Name())))
if len(name) > 60 {
name = name[:60]
}
all, err := Migrations()
if err != nil {
t.Fatal(err)
}
var before []store.Migration
for _, m := range all {
if m.Number < number {
before = append(before, m)
}
}
store.CreateMigratedForTest(t, admin, name, before)
busPasswordCostForTest.Store(int32(bcrypt.MinCost))
t.Setenv(store.Variable(Name), store.DatabaseForTest(admin, name))
inv, err := Open(t.Context())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
inv.Close()
c, err := pgx.Connect(context.Background(), admin)
if err != nil {
return
}
defer c.Close(context.Background())
_, _ = c.Exec(context.Background(), "drop database if exists "+name+" with (force)")
})
if err := inv.Ready(t.Context(), 20*time.Second); err != nil {
t.Fatal(err)
}
// The chain is in memory, for what a test of the store before a migration reads of it.
c := chain.InMemory(nil)
// A fresh test chain has nothing to lay out: it is laid out already, as the controller's start leaves a mesh (issue 1121).
if _, err := c.Builds.CreateRaw(t.Context(), chain.LayoutMarkerKey, []byte(`{}`)); err != nil {
t.Fatal(err)
}
inv.UseChain(c)
testChains.Store(os.Getenv(store.Variable(Name)), c)
t.Cleanup(func() { testChains.Delete(os.Getenv(store.Variable(Name))) })
return inv, func() {
t.Helper()
if _, err := inv.store.Migrate(t.Context(), all); err != nil {
t.Fatal(err)
}
}
}
// ExecForTest runs one statement against a test's store, for a fixture no verb of the mesh writes: a row as an
// older migration left it.
func (i *Inventory) ExecForTest(t *testing.T, sql string, args ...any) {
t.Helper()
if _, err := i.store.Pool().Exec(t.Context(), sql, args...); err != nil {
t.Fatalf("%s: %v", sql, err)
}
}
// QueryForTest reads rows of a test's store, each row's columns as JSON text, for a test that checks what a
// migration left.
func (i *Inventory) QueryForTest(t *testing.T, sql string, args ...any) [][]string {
t.Helper()
rows, err := i.store.Pool().Query(t.Context(), sql, args...)
if err != nil {
t.Fatalf("%s: %v", sql, err)
}
defer rows.Close()
var out [][]string
for rows.Next() {
values, err := rows.Values()
if err != nil {
t.Fatal(err)
}
row := make([]string, len(values))
for k, v := range values {
row[k] = fmt.Sprint(v)
if b, err := json.Marshal(v); err == nil {
row[k] = string(b)
}
}
out = append(out, row)
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
return out
}
// PlanValuesForTest runs the pass that keeps the provisioner's plans in the values of the assignments (KeepAssignmentPlans)
// and answers the values that stand, by key: what a reader of the bucket reads.
func (i *Inventory) PlanValuesForTest(t *testing.T) map[string][]byte {
t.Helper()
if _, _, err := i.KeepAssignmentPlans(t.Context()); err != nil {
t.Fatal(err)
}
kept, err := i.KeptAssignments(t.Context())
if err != nil {
t.Fatal(err)
}
out := make(map[string][]byte, len(kept))
for key, v := range kept {
out[key] = v.Value
}
return out
}