Require explicit grandfather or migrate dispositions for outgoing defaults. Treat any live plan attachment as blocking baseline restoration.
544 lines
18 KiB
Go
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)
|
|
}
|
|
}
|