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.
165 lines
4.6 KiB
Go
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")
|
|
}
|
|
}
|