Files
member-console/internal/db/entity_keys_test.go
T
cgalo5758 568383b55e Reorganize status docs and add identifier docs
Move settled investigation records into
`status/explorations/<topic>-<date>/` with README indexes,
promote durable conventions to `docs/`, and add
`docs/identifiers.md` plus upstream Doc 46 person-name
provenance documents. Update cross-references and archive
resolved issues.
2026-08-29 21:15:31 -05:00

306 lines
11 KiB
Go

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.<table>.<column> 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)
}
}