diff --git a/internal/catalogue/placement_test.go b/internal/catalogue/placement_test.go index 1b02ca8d..e75d770b 100644 --- a/internal/catalogue/placement_test.go +++ b/internal/catalogue/placement_test.go @@ -23,7 +23,7 @@ func placeable() Manifest { } func placedBy(values map[string]any) Rendering { - return Rendering{Settings: SettingsBy{"arr": {{From: "node anchor", Values: values}}}} + return Rendering{Settings: SettingsBy{"arr": {LayerOf("node anchor", values)}}} } func TestAnAssignmentPlacesDirectoriesAndAccesses(t *testing.T) { @@ -179,3 +179,43 @@ func TestAnUnknownAccessReferenceIsRefusedAtParse(t *testing.T) { t.Fatalf("an access with no id and no path was accepted: %v", err) } } + +// **Placement is read from the placement, never from a layer's values** (novox/hq ADR 0348 clause 11, issue +// 651). A `places` or `accesses` among the config values places nothing; the same in Placement does. A +// mergeable file of today's form still takes every key of the layer as it is said, placement too, so it +// composes as it did while placement was kept in the layer. +func TestPlacementIsReadFromThePlacementOnly(t *testing.T) { + m := placeable() + said := map[string]any{ + PlacesSetting: map[string]any{"config": "/services/arr/config"}, + AccessesSetting: map[string]any{"series": "/storage/media/series", "spool": "/storage/downloads"}, + "log": "warn", + } + among := []Layer{{From: "anchor", Values: said}} + if placed, err := Places(m, among); err != nil || placed != nil { + t.Fatalf("a place among the config values placed %v (%v)", placed, err) + } + if _, _, err := accessesFor(m, among); err == nil { + t.Fatal("an access among the config values placed it") + } + beside := []Layer{LayerOf("anchor", said)} + if beside[0].Values["log"] != "warn" || beside[0].Values[PlacesSetting] != nil || beside[0].Placement[PlacesSetting] == nil { + t.Fatalf("LayerOf split %+v", beside[0]) + } + if placed, err := Places(m, beside); err != nil || placed["config"].Path != "/services/arr/config" { + t.Fatalf("the placement did not place: %v (%v)", placed, err) + } + + file := map[string]any{"id": "runtime", "type": "file", "path": "/x", "content": `{"a":1}`, "merge": "json"} + whole, err := ApplySettings(file, among) + if err != nil { + t.Fatal(err) + } + split, err := ApplySettings(file, beside) + if err != nil { + t.Fatal(err) + } + if whole["content"] != split["content"] || !strings.Contains(split["content"].(string), `"places"`) { + t.Fatalf("today's mergeable file composes %q, was %q", split["content"], whole["content"]) + } +} diff --git a/internal/inventory/fortest.go b/internal/inventory/fortest.go index 77aec9cc..cbd7f73a 100644 --- a/internal/inventory/fortest.go +++ b/internal/inventory/fortest.go @@ -2,6 +2,7 @@ package inventory import ( "context" + "encoding/json" "fmt" "os" "strings" @@ -75,3 +76,94 @@ func (i *Inventory) RemoveNodeForTest(ctx context.Context, name string) (bool, e } 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) + } + 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 +}