Files
member-console/internal/workflows/entitlements/activities_test.go
T
cgalo5758 75d350f88b Replace org type backfill with previewed changes
Require explicit grandfather or migrate dispositions for outgoing
defaults.
Treat any live plan attachment as blocking baseline restoration.
2026-07-11 21:02:17 -05:00

544 lines
18 KiB
Go

package entitlements_test
// ExpireGrantActivity integration tests (Doc 41 change, tasks 6.5 / design D5): the
// activity is decree (status -> expired) + end_conferral by source +
// floor-guarded baseline restoration (ReapplyDefaultsIfVacant) — restore
// the org-type default only when expiry left the pool with no live plan
// position on any ladder; a position held by any other source is never
// superseded nor layered alongside.
//
// The activity opens its own transaction, so each test commits its fixture
// first; unique org types and slugs keep runs isolated in a shared database.
import (
"context"
"database/sql"
"encoding/json"
"log/slog"
"os"
"strings"
"testing"
"time"
"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"
wfEnt "git.coopcloud.tech/wiki-cafe/member-console/internal/workflows/entitlements"
"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")
}
if !strings.Contains(dsn, "search_path") {
dsn += "&search_path=core,public"
}
database, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatalf("open db: %v", err)
}
sources := migrate.Sources()
if err := db.RunMigrations(database, sources); err != nil {
t.Fatalf("migrations: %v", err)
}
t.Cleanup(func() { database.Close() })
return database
}
type testProduct struct {
productID string
setID string
}
func makeProduct(t *testing.T, ctx context.Context, tx *sql.Tx, eq *entitlements.Queries, name string) testProduct {
t.Helper()
set, err := eq.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{
Name: name + " Set",
IsActive: true,
})
if err != nil {
t.Fatalf("set: %v", err)
}
if _, err := eq.CreateEntitlementSetRule(ctx, entitlements.CreateEntitlementSetRuleParams{
SetID: set.SetID,
RuleType: "limit",
ResourceKey: sql.NullString{String: "fedwiki_sites", Valid: true},
ResourceValue: sql.NullInt64{Int64: 1, Valid: true},
StackingPolicy: sql.NullString{String: "additive", Valid: true},
}); err != nil {
t.Fatalf("set rule: %v", err)
}
var productID string
if err := tx.QueryRowContext(ctx,
`INSERT INTO core.products (name, display_category, is_active, is_public, entitlement_set_id, lifecycle_status)
VALUES ($1, NULL, TRUE, TRUE, $2, 'published') RETURNING product_id`,
name, set.SetID,
).Scan(&productID); err != nil {
t.Fatalf("product: %v", err)
}
return testProduct{productID: productID, setID: set.SetID}
}
// expiryFixture is a committed org with its own unique org type, a pool, and
// (when withDefaultLadder) a two-tier default ladder: def at rank 0, plan at
// rank 1.
type expiryFixture struct {
orgType string
orgID string
personID string
poolID string
ladderID string // empty when withDefaultLadder is false
def testProduct // rank-0 baseline (zero value when no ladder)
plan testProduct // rank-1 plan (zero value when no ladder)
}
func setupExpiryFixture(t *testing.T, database *sql.DB, withDefaultLadder bool) expiryFixture {
t.Helper()
ctx := context.Background()
tx, err := database.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("begin: %v", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
bq := billing.New(tx)
eq := entitlements.New(tx)
iq := identity.New(tx)
oq := organization.New(tx)
f := expiryFixture{orgType: "ge-" + uuid.New().String()[:10]}
if _, err := tx.ExecContext(ctx,
`INSERT INTO core.org_types (org_type, display_name) VALUES ($1, $2)`,
f.orgType, "Test "+f.orgType,
); err != nil {
t.Fatalf("create org_type: %v", err)
}
user, err := iq.CreateUser(ctx, "u-"+uuid.New().String())
if err != nil {
t.Fatalf("user: %v", err)
}
person, err := iq.CreatePerson(ctx, identity.CreatePersonParams{
UserID: user.UserID,
DisplayName: "T",
PrimaryEmail: uuid.New().String() + "@example.com",
PrimaryEmailVerified: true,
})
if err != nil {
t.Fatalf("person: %v", err)
}
f.personID = person.PersonID
org, err := oq.CreateOrganization(ctx, organization.CreateOrganizationParams{
Name: "Org",
Slug: "ge-" + uuid.New().String()[:8],
OrgType: f.orgType,
OwnerPersonID: person.PersonID,
})
if err != nil {
t.Fatalf("org: %v", err)
}
f.orgID = org.OrgID
pool, err := eq.CreateResourcePool(ctx, entitlements.CreateResourcePoolParams{
OrgID: org.OrgID,
Name: "Default",
Slug: "d-" + uuid.New().String()[:6],
PoolType: "default",
IsAutoManaged: true,
})
if err != nil {
t.Fatalf("pool: %v", err)
}
f.poolID = pool.PoolID
if withDefaultLadder {
f.def = makeProduct(t, ctx, tx, eq, "Default-"+uuid.New().String()[:6])
f.plan = makeProduct(t, ctx, tx, eq, "Plan-"+uuid.New().String()[:6])
ladder, err := bq.CreatePlanLadder(ctx, billing.CreatePlanLadderParams{
LadderKey: "gel-" + uuid.New().String()[:8],
Name: "GrantExpLadder",
IsActive: true,
})
if err != nil {
t.Fatalf("ladder: %v", err)
}
f.ladderID = ladder.PlanLadderID
// Tiers self-assign rank MAX+1: def lands at rank 0, plan at rank 1.
if _, err := bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
PlanLadderID: ladder.PlanLadderID,
ProductID: f.def.productID,
}); err != nil {
t.Fatalf("tier0: %v", err)
}
if _, err := bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
PlanLadderID: ladder.PlanLadderID,
ProductID: f.plan.productID,
}); err != nil {
t.Fatalf("tier1: %v", err)
}
if _, err := oq.UpdateOrgTypeDefaultPlanLadder(ctx, organization.UpdateOrgTypeDefaultPlanLadderParams{
OrgType: f.orgType,
DefaultPlanLadderID: uuid.NullUUID{UUID: uuid.MustParse(ladder.PlanLadderID), Valid: true},
}); err != nil {
t.Fatalf("set default plan ladder: %v", err)
}
}
if err := tx.Commit(); err != nil {
t.Fatalf("commit fixture: %v", err)
}
committed = true
return f
}
// conferBoundedGrant records an already-due bounded grant of the product and
// confers it onto the fixture pool, committing both. Returns the grant.
func conferBoundedGrant(t *testing.T, database *sql.DB, f expiryFixture, productID, reason string) entitlements.Grant {
t.Helper()
ctx := context.Background()
tx, err := database.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("begin: %v", err)
}
defer tx.Rollback()
eq := entitlements.New(tx)
grant, err := eq.CreateGrant(ctx, entitlements.CreateGrantParams{
ProductID: productID,
GrantedToOrgID: uuid.NullUUID{UUID: uuid.MustParse(f.orgID), Valid: true},
GrantedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(f.personID), Valid: true},
GrantReason: reason,
Quantity: 1,
ValidFrom: time.Now().Add(-time.Hour),
ValidUntil: sql.NullTime{Time: time.Now().Add(-time.Minute), Valid: true},
})
if err != nil {
t.Fatalf("grant: %v", err)
}
if _, _, err := eq.Confer(ctx, entitlements.ConferParams{
PoolID: f.poolID,
ProductID: productID,
GrantID: uuid.NullUUID{UUID: uuid.MustParse(grant.GrantID), Valid: true},
Quantity: 1,
ActorType: "system",
Reason: sql.NullString{String: "expiry-test-setup", Valid: true},
}); err != nil {
t.Fatalf("setup conferral: %v", err)
}
if err := tx.Commit(); err != nil {
t.Fatalf("commit grant: %v", err)
}
return grant
}
func testActivities(database *sql.DB) *wfEnt.Activities {
return wfEnt.NewActivities(database, slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})))
}
func scalarInt(t *testing.T, database *sql.DB, query string, args ...any) int {
t.Helper()
var n int
if err := database.QueryRowContext(context.Background(), query, args...).Scan(&n); err != nil {
t.Fatalf("query %q: %v", query, err)
}
return n
}
// activeAttach counts active junction rows for (pool, product).
func activeAttach(t *testing.T, database *sql.DB, poolID, productID string) int {
return scalarInt(t, database, `
SELECT count(*) FROM core.pool_provision_ladders l
JOIN core.pool_provisions p ON p.provision_id = l.provision_id
WHERE l.pool_id = $1 AND p.product_id = $2 AND l.status = 'active'`,
poolID, productID)
}
func defaultDecrees(t *testing.T, database *sql.DB, orgID string) int {
return scalarInt(t, database, `
SELECT count(*) FROM core.grants
WHERE granted_to_org_id = $1 AND grant_reason = 'default' AND granted_by_person_id IS NULL`,
orgID)
}
// A bounded PLAN grant expiring leaves the default ladder vacant, so the
// vacancy-guarded restoration returns the pool to the rank-0 default; the
// re-run is a no-op (the decree is no longer active).
func TestExpireGrantActivity_PlanGrantRestoresDefault(t *testing.T) {
database := testDB(t)
ctx := context.Background()
f := setupExpiryFixture(t, database, true)
grant := conferBoundedGrant(t, database, f, f.plan.productID, "evaluation")
acts := testActivities(database)
out, err := acts.ExpireGrantActivity(ctx, wfEnt.ExpireGrantInput{GrantID: grant.GrantID})
if err != nil {
t.Fatalf("ExpireGrantActivity: %v", err)
}
if out.Skipped {
t.Fatalf("expected activity to run, got Skipped=true")
}
if out.TransitionsRecorded != 1 {
t.Fatalf("expected 1 provision ended, got %d", out.TransitionsRecorded)
}
// Decree expired.
var grantStatus string
if err := database.QueryRowContext(ctx,
`SELECT status FROM core.grants WHERE grant_id = $1`, grant.GrantID,
).Scan(&grantStatus); err != nil {
t.Fatalf("read grant status: %v", err)
}
if grantStatus != "expired" {
t.Fatalf("grant status = %q, want expired", grantStatus)
}
// The plan rung is ended and the rank-0 default was restored by the
// vacancy guard, backed by a system-authored default decree.
if n := activeAttach(t, database, f.poolID, f.plan.productID); n != 0 {
t.Errorf("plan attachments = %d, want 0 (expired)", n)
}
if n := activeAttach(t, database, f.poolID, f.def.productID); n != 1 {
t.Errorf("rank-0 default attachments = %d, want 1 (restored)", n)
}
if n := defaultDecrees(t, database, f.orgID); n != 1 {
t.Errorf("default decrees = %d, want 1", n)
}
// Idempotent re-run: skipped, and nothing double-restored.
out2, err := acts.ExpireGrantActivity(ctx, wfEnt.ExpireGrantInput{GrantID: grant.GrantID})
if err != nil {
t.Fatalf("second ExpireGrantActivity: %v", err)
}
if !out2.Skipped {
t.Fatalf("expected Skipped=true on second run, got %+v", out2)
}
if n := defaultDecrees(t, database, f.orgID); n != 1 {
t.Errorf("default decrees after re-run = %d, want 1 (no duplicate restoration)", n)
}
}
// A pool whose only live plan position sits on a NON-default ladder (e.g. a
// grandfathered legacy grant) is occupied under the floor guard: expiring an
// unrelated off-ladder grant performs no restoration, so the pool never holds
// its legacy plan and the current default side by side
// (org-type-default-change-flow floor-guard divergent case, expiry path).
func TestExpireGrantActivity_DefersToPlanOnNonDefaultLadder(t *testing.T) {
database := testDB(t)
ctx := context.Background()
f := setupExpiryFixture(t, database, true)
// Committed fixture extension: a single-tier ladder that is NOT the
// org-type default, held by an unbounded legacy grant, plus an off-ladder
// addon product for the expiring grant.
tx, err := database.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("begin: %v", err)
}
defer tx.Rollback()
bq := billing.New(tx)
eq := entitlements.New(tx)
legacy := makeProduct(t, ctx, tx, eq, "Legacy-"+uuid.New().String()[:6])
addon := makeProduct(t, ctx, tx, eq, "Addon-"+uuid.New().String()[:6])
ladder, err := bq.CreatePlanLadder(ctx, billing.CreatePlanLadderParams{
LadderKey: "flr-" + uuid.New().String()[:8],
Name: "FloorLegacyLadder",
IsActive: true,
})
if err != nil {
t.Fatalf("legacy ladder: %v", err)
}
if _, err := bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
PlanLadderID: ladder.PlanLadderID,
ProductID: legacy.productID,
}); err != nil {
t.Fatalf("legacy tier: %v", err)
}
legacyGrant, err := eq.CreateGrant(ctx, entitlements.CreateGrantParams{
ProductID: legacy.productID,
GrantedToOrgID: uuid.NullUUID{UUID: uuid.MustParse(f.orgID), Valid: true},
GrantedByPersonID: uuid.NullUUID{UUID: uuid.MustParse(f.personID), Valid: true},
GrantReason: "legacy",
Quantity: 1,
ValidFrom: time.Now().Add(-time.Hour),
})
if err != nil {
t.Fatalf("legacy grant: %v", err)
}
if _, _, err := eq.Confer(ctx, entitlements.ConferParams{
PoolID: f.poolID,
ProductID: legacy.productID,
GrantID: uuid.NullUUID{UUID: uuid.MustParse(legacyGrant.GrantID), Valid: true},
Quantity: 1,
ActorType: "system",
Reason: sql.NullString{String: "floor-test-setup", Valid: true},
}); err != nil {
t.Fatalf("confer legacy: %v", err)
}
if err := tx.Commit(); err != nil {
t.Fatalf("commit floor fixture: %v", err)
}
addonGrant := conferBoundedGrant(t, database, f, addon.productID, "promotional")
acts := testActivities(database)
out, err := acts.ExpireGrantActivity(ctx, wfEnt.ExpireGrantInput{GrantID: addonGrant.GrantID})
if err != nil {
t.Fatalf("ExpireGrantActivity: %v", err)
}
if out.Skipped {
t.Fatalf("expected activity to run, got Skipped=true")
}
if out.TransitionsRecorded != 1 {
t.Fatalf("expected 1 provision ended, got %d", out.TransitionsRecorded)
}
// Floor guard: the pool is occupied on the legacy ladder, so nothing is
// restored — no system default decree, no rank-0 attachment, and the
// legacy plan is untouched.
if n := activeAttach(t, database, f.poolID, f.def.productID); n != 0 {
t.Errorf("rank-0 default attachments = %d, want 0 (floor guard must not layer the default)", n)
}
if n := defaultDecrees(t, database, f.orgID); n != 0 {
t.Errorf("default decrees = %d, want 0 (guard must not restore)", n)
}
if n := activeAttach(t, database, f.poolID, legacy.productID); n != 1 {
t.Errorf("legacy attachments = %d, want 1 (untouched)", n)
}
}
// A bounded NON-plan (off-ladder) grant expiring is ended with no ladder
// machinery and no restoration (the org configures no default); the re-run is
// a no-op.
func TestExpireGrantActivity_OffLadderGrantEndsAndIsIdempotent(t *testing.T) {
database := testDB(t)
ctx := context.Background()
f := setupExpiryFixture(t, database, false)
// An off-ladder product minted directly against the committed database.
tx, err := database.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("begin: %v", err)
}
eq := entitlements.New(tx)
addon := makeProduct(t, ctx, tx, eq, "Addon-"+uuid.New().String()[:6])
if err := tx.Commit(); err != nil {
t.Fatalf("commit addon: %v", err)
}
grant := conferBoundedGrant(t, database, f, addon.productID, "evaluation")
acts := testActivities(database)
out, err := acts.ExpireGrantActivity(ctx, wfEnt.ExpireGrantInput{GrantID: grant.GrantID})
if err != nil {
t.Fatalf("ExpireGrantActivity: %v", err)
}
if out.Skipped || out.TransitionsRecorded != 1 {
t.Fatalf("out = %+v, want run with 1 provision ended", out)
}
if n := scalarInt(t, database,
`SELECT count(*) FROM core.pool_provisions WHERE grant_id = $1 AND status <> 'ended'`,
grant.GrantID); n != 0 {
t.Errorf("live provisions for expired grant = %d, want 0", n)
}
// Off-ladder: no junction rows ever existed, and no restoration ran.
if n := scalarInt(t, database,
`SELECT count(*) FROM core.pool_provision_ladders WHERE pool_id = $1`, f.poolID); n != 0 {
t.Errorf("junction rows = %d, want 0", n)
}
if n := defaultDecrees(t, database, f.orgID); n != 0 {
t.Errorf("default decrees = %d, want 0 (no default configured)", n)
}
out2, err := acts.ExpireGrantActivity(ctx, wfEnt.ExpireGrantInput{GrantID: grant.GrantID})
if err != nil {
t.Fatalf("second ExpireGrantActivity: %v", err)
}
if !out2.Skipped {
t.Fatalf("expected Skipped=true on second run, got %+v", out2)
}
}
// Expiry while a subscription-sourced provision holds the default ladder:
// the guard finds the ladder occupied and does nothing — the subscription's
// position is never superseded by the restoration.
func TestExpireGrantActivity_GuardRespectsSubscriptionHolder(t *testing.T) {
database := testDB(t)
ctx := context.Background()
f := setupExpiryFixture(t, database, true)
// A subscription holds the plan rung of the default ladder, plus an
// off-ladder addon delivered by a bounded grant.
tx, err := database.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("begin: %v", err)
}
bq := billing.New(tx)
eq := entitlements.New(tx)
account, err := bq.CreateBillingAccount(ctx, billing.CreateBillingAccountParams{
OrgID: f.orgID,
Name: "exp-guard-" + uuid.New().String()[:8],
Status: "active",
Metadata: json.RawMessage("{}"),
})
if err != nil {
t.Fatalf("billing account: %v", err)
}
var subID uuid.UUID
if err := tx.QueryRowContext(ctx,
`INSERT INTO core.subscriptions (billing_account_id, status) VALUES ($1, 'active')
RETURNING subscription_id`, account.BillingAccountID,
).Scan(&subID); err != nil {
t.Fatalf("subscription: %v", err)
}
if _, _, err := eq.Confer(ctx, entitlements.ConferParams{
PoolID: f.poolID,
ProductID: f.plan.productID,
SubscriptionID: uuid.NullUUID{UUID: subID, Valid: true},
Quantity: 1,
ActorType: "webhook",
}); err != nil {
t.Fatalf("confer subscription plan: %v", err)
}
addon := makeProduct(t, ctx, tx, eq, "Addon-"+uuid.New().String()[:6])
if err := tx.Commit(); err != nil {
t.Fatalf("commit: %v", err)
}
grant := conferBoundedGrant(t, database, f, addon.productID, "evaluation")
acts := testActivities(database)
out, err := acts.ExpireGrantActivity(ctx, wfEnt.ExpireGrantInput{GrantID: grant.GrantID})
if err != nil {
t.Fatalf("ExpireGrantActivity: %v", err)
}
if out.Skipped || out.TransitionsRecorded != 1 {
t.Fatalf("out = %+v, want run with 1 provision ended", out)
}
// The subscription's plan position is untouched; the guard restored
// nothing (the default ladder was never vacant).
if n := activeAttach(t, database, f.poolID, f.plan.productID); n != 1 {
t.Errorf("subscription plan attachments = %d, want 1 (never superseded)", n)
}
if n := activeAttach(t, database, f.poolID, f.def.productID); n != 0 {
t.Errorf("rank-0 default attachments = %d, want 0 (guard does nothing)", n)
}
if n := defaultDecrees(t, database, f.orgID); n != 0 {
t.Errorf("default decrees = %d, want 0", n)
}
}