// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package db_test // DB-backed tests for migration 00012_entity_keys.sql (entity-keys change; // docs/identifiers.md §2 to §4). What is under test is the column // contract itself, one rule for seven tables: a nullable `key`, the ratified // grammar `^[a-z][a-z0-9_]*$` at most 64 characters, and uniqueness within // the smallest namespace the entity inhabits (the table for a root entity, // the parent for a child). The two constants the console writes for itself -- // the System tenant's `system` and each organization's default pool's // `default` -- are proven through the code paths that create those rows, not // by asserting on rows a fixture inserted. // // Harness is schema_hardening_test.go's: skip without TEST_DATABASE_URL, // migrate via db.RunMigrations, reuse its mustInsert* fixture helpers, // assertPgError, and uniqueLabel. import ( "context" "database/sql" "testing" "git.coopcloud.tech/wiki-cafe/member-console/internal/organization" "git.coopcloud.tech/wiki-cafe/member-console/internal/provisioning" ) // columnExists reports whether core.. is present. func columnExists(t *testing.T, ctx context.Context, database *sql.DB, table, column string) bool { t.Helper() var exists bool if err := database.QueryRowContext(ctx, `SELECT EXISTS( SELECT 1 FROM information_schema.columns WHERE table_schema = 'core' AND table_name = $1 AND column_name = $2 )`, table, column).Scan(&exists); err != nil { t.Fatalf("column probe %s.%s: %v", table, column, err) } return exists } // TestEntityKeys_ColumnsPresentAndOldNamesGone covers §2 (which entities carry // a key) and §7 (the three slug columns and the plan ladder's ladder_key // become `key`, they are not dropped and re-added under the old spelling). func TestEntityKeys_ColumnsPresentAndOldNamesGone(t *testing.T) { database := newSchemaHardeningTestDB(t) ctx := context.Background() keyed := []string{ "organizations", "workspaces", "resource_pools", "plan_ladders", "entitlement_sets", "products", "prices", } for _, table := range keyed { if !columnExists(t, ctx, database, table, "key") { t.Errorf("core.%s has no `key` column", table) } } // The retired spellings. `slug` named a value derived from a login name // or a display name; `ladder_key` was the one-off spelling of the same // role the uniform column now carries. retired := []struct{ table, column string }{ {"organizations", "slug"}, {"workspaces", "slug"}, {"resource_pools", "slug"}, {"plan_ladders", "ladder_key"}, } for _, r := range retired { if columnExists(t, ctx, database, r.table, r.column) { t.Errorf("core.%s still has the retired column %q", r.table, r.column) } } } // TestEntityKeys_GrammarCheckRejectsAndAccepts covers §4: one grammar for // every key column, so one validator and one error message. `Bad-Key` fails // it twice over (an upper-case letter and a hyphen); `demo_ladder` is the // shape the demo seed writes. func TestEntityKeys_GrammarCheckRejectsAndAccepts(t *testing.T) { database := newSchemaHardeningTestDB(t) ctx := context.Background() insertLadder := func(key any) error { _, err := database.ExecContext(ctx, `INSERT INTO core.plan_ladders (name, key) VALUES ($1, $2)`, uniqueLabel("keys-grammar-ladder-"), key) return err } err := insertLadder("Bad-Key") assertPgError(t, err, "23514", "chk_plan_ladders_key_grammar") accepted := uniqueLabel("demo_ladder_") if err := insertLadder(accepted); err != nil { t.Fatalf("grammar rejected a conforming key %q: %v", accepted, err) } // The literal the demo seed uses, spelled exactly, must pass the CHECK. // It is inserted and rolled back so the seeded row is not claimed here. tx, err := database.BeginTx(ctx, nil) if err != nil { t.Fatalf("begin: %v", err) } defer tx.Rollback() //nolint:errcheck if _, err := tx.ExecContext(ctx, `INSERT INTO core.plan_ladders (name, key) VALUES ($1, 'demo_ladder')`, uniqueLabel("keys-grammar-demo-")); err != nil { t.Errorf("grammar rejected the demo seed's key `demo_ladder`: %v", err) } } // TestEntityKeys_RootUniquenessAndNullsCoexist covers §3 (root entities are // `UNIQUE (key)`) and §4 (Postgres treats NULLs as distinct, so rows without // a key never conflict and no partial index is needed). func TestEntityKeys_RootUniquenessAndNullsCoexist(t *testing.T) { database := newSchemaHardeningTestDB(t) ctx := context.Background() personID := mustInsertPerson(t, ctx, database, "keys-org-person") key := uniqueLabel("keys_org_") insertOrg := func(k any) error { _, err := database.ExecContext(ctx, `INSERT INTO core.organizations (name, org_type, owner_person_id, key) VALUES ($1, 'personal', $2, $3)`, uniqueLabel("keys-org-"), personID, k) return err } if err := insertOrg(key); err != nil { t.Fatalf("first keyed organization rejected: %v", err) } assertPgError(t, insertOrg(key), "23505", "uq_organizations_key") // Two organizations with no key at all sit side by side. if err := insertOrg(nil); err != nil { t.Fatalf("first key-less organization rejected: %v", err) } if err := insertOrg(nil); err != nil { t.Fatalf("second key-less organization rejected -- NULLs must not conflict: %v", err) } } // TestEntityKeys_WorkspaceKeyScopedToOrganization covers §3's child rule: the // same key is free in a second organization, and taken twice in one. func TestEntityKeys_WorkspaceKeyScopedToOrganization(t *testing.T) { database := newSchemaHardeningTestDB(t) ctx := context.Background() personID := mustInsertPerson(t, ctx, database, "keys-ws-person") orgA := mustInsertOrg(t, ctx, database, uniqueLabel("keys-ws-org-a-"), personID) orgB := mustInsertOrg(t, ctx, database, uniqueLabel("keys-ws-org-b-"), personID) key := uniqueLabel("keys_ws_") insertWorkspace := func(orgID string, k any) error { _, err := database.ExecContext(ctx, `INSERT INTO core.workspaces (org_id, name, key) VALUES ($1, $2, $3)`, orgID, uniqueLabel("keys-ws-"), k) return err } if err := insertWorkspace(orgA, key); err != nil { t.Fatalf("first keyed workspace rejected: %v", err) } assertPgError(t, insertWorkspace(orgA, key), "23505", "uq_workspaces_org_id_key") if err := insertWorkspace(orgB, key); err != nil { t.Fatalf("the same key in a second organization was rejected; the scope is (org_id, key): %v", err) } if err := insertWorkspace(orgA, nil); err != nil { t.Fatalf("first key-less workspace rejected: %v", err) } if err := insertWorkspace(orgA, nil); err != nil { t.Fatalf("second key-less workspace rejected -- NULLs must not conflict: %v", err) } } // TestEntityKeys_PriceKeyScopedToProduct covers §3 for the second child // relation: a price's namespace is its product, which is what makes a key // like `monthly` usable on every product at once. func TestEntityKeys_PriceKeyScopedToProduct(t *testing.T) { database := newSchemaHardeningTestDB(t) ctx := context.Background() insertProduct := func() string { t.Helper() var productID string if err := database.QueryRowContext(ctx, `INSERT INTO core.products (name) VALUES ($1) RETURNING product_id`, uniqueLabel("keys-price-product-")).Scan(&productID); err != nil { t.Fatalf("insert product: %v", err) } return productID } productA := insertProduct() productB := insertProduct() key := uniqueLabel("keys_price_") insertPrice := func(productID string, k any) error { _, err := database.ExecContext(ctx, `INSERT INTO core.prices (product_id, currency, unit_amount, key) VALUES ($1, 'usd', 1000, $2)`, productID, k) return err } if err := insertPrice(productA, key); err != nil { t.Fatalf("first keyed price rejected: %v", err) } assertPgError(t, insertPrice(productA, key), "23505", "uq_prices_product_id_key") if err := insertPrice(productB, key); err != nil { t.Fatalf("the same key on a second product was rejected; the scope is (product_id, key): %v", err) } if err := insertPrice(productA, nil); err != nil { t.Fatalf("first key-less price rejected: %v", err) } if err := insertPrice(productA, nil); err != nil { t.Fatalf("second key-less price rejected -- NULLs must not conflict: %v", err) } } // TestEntityKeys_SystemTenantCarriesSystemKey covers §4's first constant. The // key is written by EnsureSystemOrganization, the one query that creates the // singleton, so this drives that query rather than asserting on a fixture // row. Everything runs in a rolled-back transaction for the reason // TestOrganizations_SecondSystemOrganizationRejected documents: a leaked // system organization would be adopted by systemtenant.Ensure later. // // A database that already holds the System tenant (an app boot, or an earlier // package's systemtenant.Ensure) cannot have a second one inserted, so the // assertion falls back to the row that is there -- which must carry the same // key, whether migration 00012 backfilled it or the query wrote it. func TestEntityKeys_SystemTenantCarriesSystemKey(t *testing.T) { database := newSchemaHardeningTestDB(t) ctx := context.Background() personID := mustInsertPerson(t, ctx, database, "keys-sys-person") tx, err := database.BeginTx(ctx, nil) if err != nil { t.Fatalf("begin: %v", err) } defer tx.Rollback() //nolint:errcheck // The 'system' org type is boot-ensured, never migration-seeded; the FK // on organizations.org_type needs it before any system org can exist. if _, err := tx.ExecContext(ctx, `INSERT INTO core.org_types (org_type, display_name, description, is_active, is_reserved) VALUES ('system', 'System', 'Reserved platform tenant', TRUE, TRUE) ON CONFLICT (org_type) DO NOTHING`); err != nil { t.Fatalf("ensure system org type: %v", err) } orgQ := organization.New(tx) if err := orgQ.EnsureSystemOrganization(ctx, organization.EnsureSystemOrganizationParams{ Name: uniqueLabel("keys-sys-org-"), OrgType: "system", OwnerPersonID: personID, }); err != nil { t.Fatalf("EnsureSystemOrganization: %v", err) } org, err := orgQ.GetSystemOrganization(ctx) if err != nil { t.Fatalf("GetSystemOrganization: %v", err) } if !org.Key.Valid || org.Key.String != "system" { t.Errorf("System tenant key = %v, want %q", org.Key, "system") } } // TestEntityKeys_DefaultPoolCarriesDefaultKey covers §4's second constant. // The default pool is created by CreateWorkspaceWithPrimaryAssignment, the // one path that creates it, so this drives that path; the key is scoped to // the organization, so every organization has its own `default`. func TestEntityKeys_DefaultPoolCarriesDefaultKey(t *testing.T) { database := newSchemaHardeningTestDB(t) ctx := context.Background() personID := mustInsertPerson(t, ctx, database, "keys-pool-person") orgID := mustInsertOrg(t, ctx, database, uniqueLabel("keys-pool-org-"), personID) tx, err := database.BeginTx(ctx, nil) if err != nil { t.Fatalf("begin: %v", err) } defer tx.Rollback() //nolint:errcheck result, err := provisioning.CreateWorkspaceWithPrimaryAssignment(ctx, tx, orgID, uniqueLabel("keys-pool-ws-")) if err != nil { t.Fatalf("CreateWorkspaceWithPrimaryAssignment: %v", err) } if !result.Pool.Key.Valid || result.Pool.Key.String != "default" { t.Errorf("default pool key = %v, want %q", result.Pool.Key, "default") } // The workspace itself gets none: a key is never derived from a display // name (§4), and a singleton child is addressed through its parent. if result.Workspace.Key.Valid { t.Errorf("workspace key = %q, want NULL", result.Workspace.Key.String) } }