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)) cut := strings.LastIndex(admin, "/") t.Setenv(store.Variable(Name), admin[:cut]+"/"+name+"?sslmode=disable") 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)) cut := strings.LastIndex(admin, "/") t.Setenv(store.Variable(Name), admin[:cut]+"/"+name+"?sslmode=disable") 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 }