Files
member-console/internal/server/operator_plan_ladders_test.go
T
cgalo5758 1a19ebe971 Implement uniform conferral semantics
Replace product-kind branching and direct position writes with enclosed
database functions driven by structural product shape.

Migrate grant and provision data, unify operator issuance, update
subscription and expiry flows, and add migration and integration proofs.
2026-07-11 13:05:53 -05:00

165 lines
4.6 KiB
Go

package server_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/migrate"
"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
}
// TestPlanLadderOperatorWorkflow verifies the full operator ladder CRUD workflow:
// create ladder, add tiers, reorder ranks, and delete ladder.
func TestPlanLadderOperatorWorkflow(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := database.BeginTx(ctx, nil)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
bq := billing.New(tx)
eq := entitlements.New(tx)
// 1. Create a plan ladder
ladder, err := bq.CreatePlanLadder(ctx, billing.CreatePlanLadderParams{
LadderKey: "test-ladder-" + uuid.New().String()[:8],
Name: "Test Ladder",
Description: sql.NullString{String: "A test ladder", Valid: true},
IsActive: true,
})
if err != nil {
t.Fatalf("create plan ladder: %v", err)
}
if ladder.PlanLadderID == "" {
t.Fatal("expected ladder ID to be set")
}
// 2. Create a published product — any published product may be a ladder
// tier since doc 41 (display_category is presentation-only).
// First we need an entitlement set
es, err := eq.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{
Name: "test-set-" + uuid.New().String()[:8],
Description: sql.NullString{String: "Test set", Valid: true},
})
if err != nil {
t.Fatalf("create entitlement set: %v", err)
}
product, err := bq.CreateProduct(ctx, billing.CreateProductParams{
Name: "Test Plan Product",
Description: sql.NullString{String: "A test plan product", Valid: true},
IsActive: true,
IsPublic: true,
EntitlementSetID: uuid.NullUUID{UUID: uuid.MustParse(es.SetID), Valid: true},
LifecycleStatus: "published",
})
if err != nil {
t.Fatalf("create product: %v", err)
}
// 3. Add tier to ladder — rank self-assigns append-at-end, so the first
// tier of a fresh ladder lands at rank 0.
tier, err := bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
PlanLadderID: ladder.PlanLadderID,
ProductID: product.ProductID,
})
if err != nil {
t.Fatalf("create plan ladder tier: %v", err)
}
if tier.Rank != 0 {
t.Fatalf("expected rank 0, got %d", tier.Rank)
}
// Verify tier appears in list
tiers, err := bq.ListTiersByLadder(ctx, ladder.PlanLadderID)
if err != nil {
t.Fatalf("list tiers: %v", err)
}
if len(tiers) != 1 {
t.Fatalf("expected 1 tier, got %d", len(tiers))
}
// 4. Reorder tier to rank 2
updatedTier, err := bq.UpdateTierRank(ctx, billing.UpdateTierRankParams{
PlanLadderID: ladder.PlanLadderID,
ProductID: product.ProductID,
Rank: 2,
})
if err != nil {
t.Fatalf("update tier rank: %v", err)
}
if updatedTier.Rank != 2 {
t.Fatalf("expected rank 2, got %d", updatedTier.Rank)
}
// 5. Verify delete is blocked when no active attachments exist
// (no attachments in this test, so delete should succeed)
count, err := eq.CountActiveAttachmentsByLadder(ctx, ladder.PlanLadderID)
if err != nil {
t.Fatalf("count active attachments: %v", err)
}
if count != 0 {
t.Fatalf("expected 0 active attachments, got %d", count)
}
// 6. Delete the tier first (should succeed since no active attachments)
err = bq.DeleteTier(ctx, billing.DeleteTierParams{
PlanLadderID: ladder.PlanLadderID,
ProductID: product.ProductID,
})
if err != nil {
t.Fatalf("delete tier: %v", err)
}
// Verify tier is gone
tiers, err = bq.ListTiersByLadder(ctx, ladder.PlanLadderID)
if err != nil {
t.Fatalf("list tiers after delete: %v", err)
}
if len(tiers) != 0 {
t.Fatalf("expected 0 tiers after delete, got %d", len(tiers))
}
// 7. Delete the ladder
err = bq.DeletePlanLadder(ctx, ladder.PlanLadderID)
if err != nil {
t.Fatalf("delete plan ladder: %v", err)
}
// Verify ladder is gone
_, err = bq.GetPlanLadderByID(ctx, ladder.PlanLadderID)
if err == nil {
t.Fatal("expected ladder to be deleted")
}
}