Files
member-console/internal/provisioning/provisioning_test.go
Christian Galo 751bae7768 Use plan ladder for org defaults
Add default_plan_ladder_id with a forward data migration and update
the runtime to resolve the ladder's rank-0 tier at use-time. Regenerate
sqlc, update auto-provisioning, ReapplyDefaultsForPool, operator UI and
tests; add GetTierByLadderRank and pool/provision query helpers. Add a
CSP-safe confirm-action modal and wire operator actions to it. Close
plan-sole-writer safety gaps and serialize IssueGrant with a FOR UPDATE
pool lock to prevent ladder races.
2026-04-27 01:57:17 -05:00

405 lines
13 KiB
Go

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"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/fedwiki"
"git.coopcloud.tech/wiki-cafe/member-console/internal/identity"
"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 := append(db.BaseSources(),
identity.MigrationSource(),
organization.MigrationSource(),
billing.MigrationSource(),
entitlements.MigrationSource(),
fwmod.MigrationSource(),
)
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: "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 creates a system grant
// populating both product_id and entitlement_set_id, 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
set, err := entQ.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{
Name: "Public Tier Default",
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: "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 product_type per Doc 31 Amendment #3 — kind is derived structurally
// via plan_ladder_tiers membership.
product, err := billQ.CreateProduct(ctx, billing.CreateProductParams{
Name: "Public Tier",
ProductType: 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{
LadderKey: "test-default-" + uuid.New().String()[:8],
Name: "Test Default Ladder",
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,
Rank: 0,
}); 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 — both product_id and entitlement_set_id must be set
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.Valid || grant.ProductID.UUID.String() != product.ProductID {
t.Errorf("expected product_id=%s, got %v", product.ProductID, grant.ProductID)
}
if !grant.EntitlementSetID.Valid || grant.EntitlementSetID.UUID.String() != set.SetID {
t.Errorf("expected entitlement_set_id=%s (resolved from product), got %v", set.SetID, grant.EntitlementSetID)
}
// 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: "sites",
})
if err != nil {
t.Fatalf("expected entitlement to exist: %v", err)
}
if ent.ResourceLimit != 1 {
t.Errorf("expected resource_limit=1 (from Public Tier 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)",
IsActive: true,
})
if err != nil {
t.Fatalf("create empty entitlement set: %v", err)
}
emptyProduct, err := billQ.CreateProduct(ctx, billing.CreateProductParams{
Name: "Empty Product",
ProductType: 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{
LadderKey: "test-empty-default-" + uuid.New().String()[:8],
Name: "Test Empty-Set Default Ladder",
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,
Rank: 0,
}); 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: "sites",
})
if err != sql.ErrNoRows {
t.Errorf("expected no entitlements from empty set, got err=%v", err)
}
}