package provisioning_test import ( "context" "database/sql" "os" "testing" "git.coopcloud.tech/wiki-cafe/member-console/internal/billing" "git.coopcloud.tech/wiki-cafe/member-console/internal/db" "git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements" "git.coopcloud.tech/wiki-cafe/member-console/internal/identity" "git.coopcloud.tech/wiki-cafe/member-console/internal/migrate" "git.coopcloud.tech/wiki-cafe/member-console/internal/organization" "git.coopcloud.tech/wiki-cafe/member-console/internal/provisioning" "github.com/google/uuid" _ "github.com/jackc/pgx/v5/stdlib" ) func testDB(t *testing.T) *sql.DB { t.Helper() dsn := os.Getenv("TEST_DATABASE_URL") if dsn == "" { t.Skip("TEST_DATABASE_URL not set, skipping integration test") } database, err := sql.Open("pgx", dsn) if err != nil { t.Fatalf("failed to open database: %v", err) } sources := migrate.Sources() if err := db.RunMigrations(database, sources); err != nil { t.Fatalf("failed to run migrations: %v", err) } t.Cleanup(func() { database.Close() }) return database } // TestAutoProvisionWithoutDefaultProduct verifies that provisioning // succeeds and DefaultGrant is nil when no default product is configured. func TestAutoProvisionWithoutDefaultProduct(t *testing.T) { database := testDB(t) ctx := context.Background() // Verify the personal org type has no default product tx, err := database.BeginTx(ctx, nil) if err != nil { t.Fatal(err) } orgQ := organization.New(tx) orgType, err := orgQ.GetOrgType(ctx, "personal") if err != nil { t.Fatalf("get org type: %v", err) } tx.Rollback() if orgType.DefaultPlanLadderID.Valid { t.Fatal("expected personal org type to have no default plan for this test") } claims := provisioning.OIDCClaims{ Subject: uuid.New().String(), Email: "no-default-" + uuid.New().String()[:8] + "@example.com", EmailVerified: true, Name: "No Default User", PreferredUsername: "no-default-" + uuid.New().String()[:8], } result, err := provisioning.AutoProvision(ctx, database, claims) if err != nil { t.Fatalf("auto-provision: %v", err) } // Core structures should be created if result.User.UserID == "" { t.Error("expected user to be created") } if result.Person.PersonID == "" { t.Error("expected person to be created") } if result.Org.OrgID == "" { t.Error("expected org to be created") } if result.Pool.PoolID == "" { t.Error("expected pool to be created") } if result.BillingAccount.BillingAccountID == "" { t.Error("expected billing account to be created") } // No default grant should be created if result.DefaultGrant != nil { t.Error("expected DefaultGrant to be nil when no default product is configured") } // Verify no entitlements were materialized on the pool tx2, err := database.BeginTx(ctx, nil) if err != nil { t.Fatal(err) } defer tx2.Rollback() entQ := entitlements.New(tx2) _, err = entQ.GetNumericEntitlementByPoolAndResource(ctx, entitlements.GetNumericEntitlementByPoolAndResourceParams{ PoolID: result.Pool.PoolID, ResourceKey: "fedwiki_sites", }) if err != sql.ErrNoRows { t.Errorf("expected no entitlements on pool, got err=%v", err) } } // TestAutoProvisionWithDefaultProduct verifies that when a default product // is configured on the org type, provisioning records a system default grant // for the product, confers it on the org's default pool, and materializes // entitlements automatically. func TestAutoProvisionWithDefaultProduct(t *testing.T) { database := testDB(t) ctx := context.Background() // Setup: create an entitlement set + product, configure product as the personal org type default tx, err := database.BeginTx(ctx, nil) if err != nil { t.Fatal(err) } entQ := entitlements.New(tx) billQ := billing.New(tx) orgQ := organization.New(tx) // Create an entitlement set with a sites limit rule. Unique-suffixed: // this test commits nothing, but the shared TEST_DATABASE_URL may carry // committed state from other suites, and uq_entitlement_sets_name is // global. set, err := entQ.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{ Name: "Standard Plan Default " + uuid.New().String()[:8], IsActive: true, }) if err != nil { t.Fatalf("create entitlement set: %v", err) } _, err = entQ.CreateEntitlementSetRule(ctx, entitlements.CreateEntitlementSetRuleParams{ SetID: set.SetID, RuleType: "limit", ResourceKey: sql.NullString{String: "fedwiki_sites", Valid: true}, ResourceValue: sql.NullInt64{Int64: 1, Valid: true}, ResourcePerUnit: sql.NullBool{Bool: false, Valid: true}, StackingPolicy: sql.NullString{String: "additive", Valid: true}, }) if err != nil { t.Fatalf("create set rule: %v", err) } // Create a product backed by that entitlement set. Plan products carry // NULL display_category (presentation-only per Doc 41 Decision 136) — kind // is derived structurally via plan_ladder_tiers membership. product, err := billQ.CreateProduct(ctx, billing.CreateProductParams{ Name: "Standard Plan", DisplayCategory: sql.NullString{}, IsActive: true, IsPublic: true, EntitlementSetID: uuid.NullUUID{UUID: uuid.MustParse(set.SetID), Valid: true}, LifecycleStatus: "published", }) if err != nil { t.Fatalf("create product: %v", err) } // Place the product on a plan ladder at rank 0 so it can serve as the // default plan (default-plan resolution requires a rank-0 ladder tier). ladder, err := billQ.CreatePlanLadder(ctx, billing.CreatePlanLadderParams{ Name: "Test Default Ladder " + uuid.New().String()[:8], Description: sql.NullString{String: "auto-provision default-plan test", Valid: true}, IsActive: true, }) if err != nil { t.Fatalf("create plan ladder: %v", err) } if _, err := billQ.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{ PlanLadderID: ladder.PlanLadderID, ProductID: product.ProductID, }); err != nil { t.Fatalf("create plan ladder tier: %v", err) } // Configure personal org type to use this ladder as its default plan _, err = orgQ.UpdateOrgTypeDefaultPlanLadder(ctx, organization.UpdateOrgTypeDefaultPlanLadderParams{ OrgType: "personal", DefaultPlanLadderID: uuid.NullUUID{UUID: uuid.MustParse(ladder.PlanLadderID), Valid: true}, }) if err != nil { t.Fatalf("update org type default plan: %v", err) } if err := tx.Commit(); err != nil { t.Fatalf("commit setup: %v", err) } // Reset the default after this test so other tests aren't affected t.Cleanup(func() { cleanTx, err := database.BeginTx(ctx, nil) if err != nil { return } oq := organization.New(cleanTx) oq.UpdateOrgTypeDefaultPlanLadder(ctx, organization.UpdateOrgTypeDefaultPlanLadderParams{ OrgType: "personal", DefaultPlanLadderID: uuid.NullUUID{Valid: false}, }) cleanTx.Commit() }) // Now provision a new user claims := provisioning.OIDCClaims{ Subject: uuid.New().String(), Email: "with-default-" + uuid.New().String()[:8] + "@example.com", EmailVerified: true, Name: "Default User", PreferredUsername: "with-default-" + uuid.New().String()[:8], } result, err := provisioning.AutoProvision(ctx, database, claims) if err != nil { t.Fatalf("auto-provision: %v", err) } // Core structures should be created if result.Org.OrgID == "" { t.Error("expected org to be created") } // Default grant SHOULD be populated if result.DefaultGrant == nil { t.Fatal("expected DefaultGrant to be populated when default product is configured") } // Verify grant properties — the decree records the default product. grant := result.DefaultGrant.Grant if grant.GrantReason != "default" { t.Errorf("expected grant_reason='default', got %q", grant.GrantReason) } if grant.GrantedByPersonID.Valid { t.Error("expected granted_by_person_id to be NULL for system grant") } if grant.ProductID != product.ProductID { t.Errorf("expected product_id=%s, got %v", product.ProductID, grant.ProductID) } if result.DefaultGrant.Outcome != "created" { t.Errorf("expected conferral outcome 'created', got %q", result.DefaultGrant.Outcome) } // Verify entitlements were materialized on the pool tx2, err := database.BeginTx(ctx, nil) if err != nil { t.Fatal(err) } defer tx2.Rollback() entQ2 := entitlements.New(tx2) ent, err := entQ2.GetNumericEntitlementByPoolAndResource(ctx, entitlements.GetNumericEntitlementByPoolAndResourceParams{ PoolID: result.Pool.PoolID, ResourceKey: "fedwiki_sites", }) if err != nil { t.Fatalf("expected entitlement to exist: %v", err) } if ent.ResourceLimit != 1 { t.Errorf("expected resource_limit=1 (from Standard Plan Default set), got %d", ent.ResourceLimit) } } // TestAutoProvisionTransactionIntegrity verifies that provisioning with a // product backed by an empty entitlement set still commits the user/org/grant // (materialization simply produces no entitlements) — tests the transactional // integrity of the flow. func TestAutoProvisionTransactionIntegrity(t *testing.T) { database := testDB(t) ctx := context.Background() // Setup: create an empty entitlement set + product, configure product as default tx, err := database.BeginTx(ctx, nil) if err != nil { t.Fatal(err) } entQ := entitlements.New(tx) billQ := billing.New(tx) orgQ := organization.New(tx) emptySet, err := entQ.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{ Name: "Empty Set (no rules) " + uuid.New().String()[:8], IsActive: true, }) if err != nil { t.Fatalf("create empty entitlement set: %v", err) } emptyProduct, err := billQ.CreateProduct(ctx, billing.CreateProductParams{ Name: "Empty Product", DisplayCategory: sql.NullString{}, IsActive: true, IsPublic: false, EntitlementSetID: uuid.NullUUID{UUID: uuid.MustParse(emptySet.SetID), Valid: true}, LifecycleStatus: "published", }) if err != nil { t.Fatalf("create empty product: %v", err) } // Place the empty product on a ladder at rank 0 so it can serve as the // default plan (default-plan resolution requires a rank-0 ladder tier). emptyLadder, err := billQ.CreatePlanLadder(ctx, billing.CreatePlanLadderParams{ Name: "Test Empty-Set Default Ladder " + uuid.New().String()[:8], Description: sql.NullString{String: "empty-set default-plan test", Valid: true}, IsActive: true, }) if err != nil { t.Fatalf("create empty-set plan ladder: %v", err) } if _, err := billQ.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{ PlanLadderID: emptyLadder.PlanLadderID, ProductID: emptyProduct.ProductID, }); err != nil { t.Fatalf("create empty-set plan ladder tier: %v", err) } _, err = orgQ.UpdateOrgTypeDefaultPlanLadder(ctx, organization.UpdateOrgTypeDefaultPlanLadderParams{ OrgType: "personal", DefaultPlanLadderID: uuid.NullUUID{UUID: uuid.MustParse(emptyLadder.PlanLadderID), Valid: true}, }) if err != nil { t.Fatalf("update org type default plan: %v", err) } if err := tx.Commit(); err != nil { t.Fatalf("commit setup: %v", err) } t.Cleanup(func() { cleanTx, err := database.BeginTx(ctx, nil) if err != nil { return } oq := organization.New(cleanTx) oq.UpdateOrgTypeDefaultPlanLadder(ctx, organization.UpdateOrgTypeDefaultPlanLadderParams{ OrgType: "personal", DefaultPlanLadderID: uuid.NullUUID{Valid: false}, }) cleanTx.Commit() }) // Provision with a product backed by an empty set — should still succeed claims := provisioning.OIDCClaims{ Subject: uuid.New().String(), Email: "empty-set-" + uuid.New().String()[:8] + "@example.com", EmailVerified: true, Name: "Empty Set User", PreferredUsername: "empty-set-" + uuid.New().String()[:8], } result, err := provisioning.AutoProvision(ctx, database, claims) if err != nil { t.Fatalf("auto-provision with empty set: %v", err) } // Grant should exist even with an empty set if result.DefaultGrant == nil { t.Fatal("expected DefaultGrant to be populated even with an empty entitlement set") } // Verify that the user, org, and all structures were committed tx2, err := database.BeginTx(ctx, nil) if err != nil { t.Fatal(err) } defer tx2.Rollback() idQ := identity.New(tx2) _, err = idQ.GetUserByOIDCSubject(ctx, claims.Subject) if err != nil { t.Errorf("expected user to be committed: %v", err) } // Verify no entitlements since the set has no rules entQ2 := entitlements.New(tx2) _, err = entQ2.GetNumericEntitlementByPoolAndResource(ctx, entitlements.GetNumericEntitlementByPoolAndResourceParams{ PoolID: result.Pool.PoolID, ResourceKey: "fedwiki_sites", }) if err != sql.ErrNoRows { t.Errorf("expected no entitlements from empty set, got err=%v", err) } }