Replace the entity slugs on organizations, workspaces, resource pools, and plan ladders with nullable `key` columns and add keys to products, prices, and entitlement sets. Rename `providers.slug` to `provider` and add partial unique indexes for system and org role names. Assign invoice numbers per billing account from a gapless transactional counter; Stripe's number moves to the invoice mapping as an external reference. Seeds, fixtures, and the operator lookup address rows by key, and the returning-login resync no longer blanks a display name when the IdP sends no `name` claim.
178 lines
5.9 KiB
Go
178 lines
5.9 KiB
Go
package systemtenant
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"testing"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/identity"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/organization"
|
|
)
|
|
|
|
// newTestDB opens the test DB and runs core's migrations only (identity +
|
|
// organization, plus billing + entitlements, all part of the single core
|
|
// schema) — not the full internal/migrate.Sources() integration-registry
|
|
// set. internal/migrate imports internal/integrations, whose registered
|
|
// integrations (e.g. internal/integrations/fedwiki) call this package's
|
|
// Ensure from their WorkflowProvider Startup hook to re-derive the system
|
|
// workspace ID, so importing internal/migrate here would cycle back
|
|
// through this very package. This test only exercises core tables, so
|
|
// db.BaseSources() is sufficient.
|
|
func newTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
dsn := os.Getenv("TEST_DATABASE_URL")
|
|
if dsn == "" {
|
|
t.Skip("TEST_DATABASE_URL not set")
|
|
}
|
|
database, err := sql.Open("pgx", dsn)
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
t.Cleanup(func() { database.Close() })
|
|
|
|
sources := db.BaseSources()
|
|
if err := db.RunMigrations(database, sources); err != nil {
|
|
t.Fatalf("migrations: %v", err)
|
|
}
|
|
return database
|
|
}
|
|
|
|
// TestEnsureCreatesAndIsIdempotent: a fresh DB gets the full chain created and a
|
|
// valid workspace ID returned; a second call returns the same ID and creates no
|
|
// duplicate workspace. Also asserts the NULL default-ladder invariant.
|
|
func TestEnsureCreatesAndIsIdempotent(t *testing.T) {
|
|
database := newTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
// The shared test-stack DB may already hold a live system tenant (an app
|
|
// boot creates one). The assertions below are therefore relative: Ensure
|
|
// must not add a workspace beyond whatever the first call establishes.
|
|
wsID, err := Ensure(ctx, database)
|
|
if err != nil {
|
|
t.Fatalf("Ensure (first): %v", err)
|
|
}
|
|
if wsID == "" {
|
|
t.Fatal("Ensure returned an empty workspace ID")
|
|
}
|
|
|
|
orgQ := organization.New(database)
|
|
org, err := orgQ.GetSystemOrganization(ctx)
|
|
if err != nil {
|
|
t.Fatalf("system org missing after Ensure: %v", err)
|
|
}
|
|
if org.OrgType != OrgType {
|
|
t.Fatalf("system org_type = %q, want %q", org.OrgType, OrgType)
|
|
}
|
|
|
|
ot, err := orgQ.GetOrgType(ctx, OrgType)
|
|
if err != nil {
|
|
t.Fatalf("system org_type row missing: %v", err)
|
|
}
|
|
if ot.DefaultPlanLadderID.Valid {
|
|
t.Fatal("system org_type must have NULL default_plan_ladder_id (no default grant)")
|
|
}
|
|
|
|
wssAfterFirst, err := orgQ.GetWorkspacesByOrgID(ctx, org.OrgID)
|
|
if err != nil {
|
|
t.Fatalf("list system workspaces: %v", err)
|
|
}
|
|
if len(wssAfterFirst) == 0 {
|
|
t.Fatal("expected at least 1 system workspace after Ensure")
|
|
}
|
|
|
|
wsID2, err := Ensure(ctx, database)
|
|
if err != nil {
|
|
t.Fatalf("Ensure (second): %v", err)
|
|
}
|
|
if wsID2 != wsID {
|
|
t.Fatalf("Ensure not idempotent: second call returned %q, want %q", wsID2, wsID)
|
|
}
|
|
|
|
wss, err := orgQ.GetWorkspacesByOrgID(ctx, org.OrgID)
|
|
if err != nil {
|
|
t.Fatalf("list system workspaces: %v", err)
|
|
}
|
|
if len(wss) != len(wssAfterFirst) {
|
|
t.Fatalf("repeat Ensure changed the workspace count: %d -> %d", len(wssAfterFirst), len(wss))
|
|
}
|
|
}
|
|
|
|
// TestEnsureAdoptsExistingWorkspace: when the system org already has a
|
|
// differently-named workspace (the removed ad-hoc seed used "reconcile"),
|
|
// Ensure adopts it rather than creating a second one (design D3).
|
|
func TestEnsureAdoptsExistingWorkspace(t *testing.T) {
|
|
database := newTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
idQ := identity.New(database)
|
|
orgQ := organization.New(database)
|
|
|
|
// The adoption scenario needs to own the system tenant's workspace set:
|
|
// against a database where the system org already has a workspace (any
|
|
// app boot or a prior Ensure-based test creates one), seeding the legacy
|
|
// workspace makes two and the adoption assertion is meaningless. Only a
|
|
// fresh database can exercise this path.
|
|
if org, err := orgQ.GetSystemOrganization(ctx); err == nil {
|
|
if wss, err := orgQ.GetWorkspacesByOrgID(ctx, org.OrgID); err == nil && len(wss) > 0 {
|
|
t.Skip("system tenant already has a workspace — the legacy-adoption scenario needs a fresh database")
|
|
}
|
|
}
|
|
|
|
// Seed a system tenant whose workspace carries a different name.
|
|
if err := idQ.EnsureSystemUser(ctx, userOIDCSubject); err != nil {
|
|
t.Fatalf("seed user: %v", err)
|
|
}
|
|
user, err := idQ.GetUserByOIDCSubject(ctx, userOIDCSubject)
|
|
if err != nil {
|
|
t.Fatalf("get seeded user: %v", err)
|
|
}
|
|
if err := idQ.EnsureSystemPerson(ctx, identity.EnsureSystemPersonParams{
|
|
UserID: user.UserID, DisplayName: personName, PrimaryEmail: personEmail,
|
|
}); err != nil {
|
|
t.Fatalf("seed person: %v", err)
|
|
}
|
|
person, err := idQ.GetPersonByUserID(ctx, user.UserID)
|
|
if err != nil {
|
|
t.Fatalf("get seeded person: %v", err)
|
|
}
|
|
if err := orgQ.EnsureSystemOrgType(ctx, organization.EnsureSystemOrgTypeParams{
|
|
OrgType: OrgType, DisplayName: orgName,
|
|
}); err != nil {
|
|
t.Fatalf("seed org_type: %v", err)
|
|
}
|
|
if err := orgQ.EnsureSystemOrganization(ctx, organization.EnsureSystemOrganizationParams{
|
|
Name: orgName, OrgType: OrgType, OwnerPersonID: person.PersonID,
|
|
}); err != nil {
|
|
t.Fatalf("seed org: %v", err)
|
|
}
|
|
org, err := orgQ.GetSystemOrganization(ctx)
|
|
if err != nil {
|
|
t.Fatalf("get seeded org: %v", err)
|
|
}
|
|
legacy, err := orgQ.CreateWorkspace(ctx, organization.CreateWorkspaceParams{
|
|
OrgID: org.OrgID, Name: "Legacy"})
|
|
if err != nil {
|
|
t.Fatalf("seed legacy workspace: %v", err)
|
|
}
|
|
|
|
wsID, err := Ensure(ctx, database)
|
|
if err != nil {
|
|
t.Fatalf("Ensure: %v", err)
|
|
}
|
|
if wsID != legacy.WorkspaceID {
|
|
t.Fatalf("Ensure did not adopt the existing workspace: got %q, want %q", wsID, legacy.WorkspaceID)
|
|
}
|
|
|
|
wss, err := orgQ.GetWorkspacesByOrgID(ctx, org.OrgID)
|
|
if err != nil {
|
|
t.Fatalf("list system workspaces: %v", err)
|
|
}
|
|
if len(wss) != 1 {
|
|
t.Fatalf("expected exactly 1 (adopted) system workspace, got %d", len(wss))
|
|
}
|
|
}
|