Files
member-console/internal/entitlements/boolean_materialize_test.go
T
cgalo5758 3727ff31d8 Add entitlement set rule change ledger and preview flow
Add an append-only ledger of entitlement set rule changes with per-pool
effect rows, a preview-and-commit rule change flow, and an automatic
drain that settles deferred recomputations. Rules gain a tier reduction
policy, resource keys declare over-limit behavior, and the materializer
now lowers limits when a rule stops applying.
Add entitlement set rule change ledger and preview flow

Add an append-only ledger of entitlement set rule changes with a
preview-and-commit operator flow. Rule writes now go through an enclosed
`core.commit_rule_change` function that files an act row and one
obligation per carrying pool, with a drain workflow settling deferred
recomputations. The preview dry-runs the materializer with a rule
overlay and renders per-pool buckets, reduction-policy disclosures, and
provider over-limit consequences. Materializing transactions take a
shared advisory rendezvous that rule changes hold exclusively, enforced
by a possession assertion. Add History and Entitlement changes surfaces,
a rule-less warning on five product-selection surfaces, and a
`tier_reduction_policy` column that gates FedWiki parking.
2026-09-15 03:53:28 -05:00

296 lines
9.3 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package entitlements_test
import (
"context"
"database/sql"
"testing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"github.com/google/uuid"
)
// createBoolResourceKey inserts a platform-owned boolean resource key for the
// test transaction (rolled back with it).
func createBoolResourceKey(t *testing.T, ctx context.Context, tx *sql.Tx, key string) {
t.Helper()
_, err := tx.ExecContext(ctx,
`INSERT INTO core.resource_keys (resource_key, display_name, unit)
VALUES ($1, $2, 'flag')`,
key, "Test Boolean "+key,
)
if err != nil {
t.Fatalf("create resource key %s: %v", key, err)
}
}
// createBoolTestProduct creates a published product whose entitlement set
// carries a single boolean rule for the given resource key.
func createBoolTestProduct(t *testing.T, ctx context.Context, tx *sql.Tx, name, resourceKey string) testProduct {
t.Helper()
q := entitlements.New(tx)
set, err := q.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{
Name: name + " Set",
IsActive: true,
})
if err != nil {
t.Fatalf("create entitlement set for %s: %v", name, err)
}
addSetRule(t, ctx, q, set.SetID, entitlements.RuleFields{
RuleType: "boolean",
ResourceKey: sql.NullString{String: resourceKey, Valid: true},
})
var productID string
err = tx.QueryRowContext(ctx,
`INSERT INTO core.products (name, is_active, is_public, lifecycle_status, entitlement_set_id)
VALUES ($1, TRUE, TRUE, 'published', $2)
RETURNING product_id`,
name, set.SetID,
).Scan(&productID)
if err != nil {
t.Fatalf("create product %s: %v", name, err)
}
return testProduct{productID: productID, setID: set.SetID}
}
func getBooleanEntitlement(t *testing.T, ctx context.Context, q *entitlements.Queries, poolID, resourceKey string) (entitlements.BooleanEntitlement, error) {
t.Helper()
return q.GetBooleanEntitlementByPoolAndResource(ctx, entitlements.GetBooleanEntitlementByPoolAndResourceParams{
PoolID: poolID,
ResourceKey: resourceKey,
})
}
// endGrantConferral revokes the grant and ends its conferral, then re-materializes.
func endGrantConferral(t *testing.T, ctx context.Context, q *entitlements.Queries, grantID, poolID string) {
t.Helper()
if _, err := q.RevokeGrant(ctx, entitlements.RevokeGrantParams{GrantID: grantID}); err != nil {
t.Fatalf("revoke grant: %v", err)
}
ended, err := q.EndConferral(ctx, entitlements.EndConferralParams{
GrantID: uuid.NullUUID{UUID: uuid.MustParse(grantID), Valid: true},
})
if err != nil {
t.Fatalf("end conferral: %v", err)
}
if len(ended) != 1 {
t.Fatalf("expected 1 ended provision, got %d", len(ended))
}
if err := entitlements.MaterializePoolEntitlements(ctx, q, poolID); err != nil {
t.Fatalf("re-materialize: %v", err)
}
}
// Scenario: boolean rule materializes on provision activation.
func TestBooleanMaterializeOnActivation(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := entitlements.BeginRuleChange(ctx, database)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
q := entitlements.New(tx)
to := setupTestOrg(t, ctx, tx)
key := "test.bool-" + uuid.New().String()[:8]
createBoolResourceKey(t, ctx, tx, key)
product := createBoolTestProduct(t, ctx, tx, "BoolProduct", key)
createGrantAndProvision(t, ctx, tx, q, to, product.productID, 1)
be, err := getBooleanEntitlement(t, ctx, q, to.pool.PoolID, key)
if err != nil {
t.Fatalf("get boolean entitlement: %v", err)
}
if !be.Granted {
t.Errorf("expected granted=true after activation, got false")
}
}
// Scenario: OR-aggregation across provisions — one of two carriers ends,
// the row stays granted.
func TestBooleanORAggregation(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := entitlements.BeginRuleChange(ctx, database)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
q := entitlements.New(tx)
to := setupTestOrg(t, ctx, tx)
key := "test.bool-" + uuid.New().String()[:8]
createBoolResourceKey(t, ctx, tx, key)
// Two distinct products (distinct sets) carrying the same boolean key —
// exercises OR-aggregation across sets, not just repeated grants.
productA := createBoolTestProduct(t, ctx, tx, "BoolProductA", key)
productB := createBoolTestProduct(t, ctx, tx, "BoolProductB", key)
grant1 := createGrantAndProvision(t, ctx, tx, q, to, productA.productID, 1)
createGrantAndProvision(t, ctx, tx, q, to, productB.productID, 1)
endGrantConferral(t, ctx, q, grant1.GrantID, to.pool.PoolID)
be, err := getBooleanEntitlement(t, ctx, q, to.pool.PoolID, key)
if err != nil {
t.Fatalf("get boolean entitlement: %v", err)
}
if !be.Granted {
t.Errorf("expected granted=true while second carrier active, got false")
}
}
// Scenario: last carrying provision ends — row updated to granted=false and
// retained, not deleted.
func TestBooleanLastCarrierLapse(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := entitlements.BeginRuleChange(ctx, database)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
q := entitlements.New(tx)
to := setupTestOrg(t, ctx, tx)
key := "test.bool-" + uuid.New().String()[:8]
createBoolResourceKey(t, ctx, tx, key)
product := createBoolTestProduct(t, ctx, tx, "BoolProduct", key)
grant := createGrantAndProvision(t, ctx, tx, q, to, product.productID, 1)
endGrantConferral(t, ctx, q, grant.GrantID, to.pool.PoolID)
be, err := getBooleanEntitlement(t, ctx, q, to.pool.PoolID, key)
if err != nil {
t.Fatalf("expected retained row after lapse, got error: %v", err)
}
if be.Granted {
t.Errorf("expected granted=false after last carrier ended, got true")
}
}
// Scenario: never-conferred key has no row.
func TestBooleanNeverConferredNoRow(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := entitlements.BeginRuleChange(ctx, database)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
q := entitlements.New(tx)
to := setupTestOrg(t, ctx, tx)
key := "test.bool-" + uuid.New().String()[:8]
createBoolResourceKey(t, ctx, tx, key)
// Provision a limit-only product; the boolean key is never carried.
product := createTestProduct(t, ctx, tx, "LimitOnlyProduct", 5, false)
createGrantAndProvision(t, ctx, tx, q, to, product.productID, 1)
_, err = getBooleanEntitlement(t, ctx, q, to.pool.PoolID, key)
if err != sql.ErrNoRows {
t.Errorf("expected sql.ErrNoRows for never-conferred key, got %v", err)
}
}
// Scenario: idempotent boolean materialization.
func TestBooleanMaterializeIdempotency(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := entitlements.BeginRuleChange(ctx, database)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
q := entitlements.New(tx)
to := setupTestOrg(t, ctx, tx)
key := "test.bool-" + uuid.New().String()[:8]
createBoolResourceKey(t, ctx, tx, key)
product := createBoolTestProduct(t, ctx, tx, "BoolProduct", key)
createGrantAndProvision(t, ctx, tx, q, to, product.productID, 1)
var first entitlements.BooleanEntitlement
for i := 0; i < 3; i++ {
if err := entitlements.MaterializePoolEntitlements(ctx, q, to.pool.PoolID); err != nil {
t.Fatalf("materialize iteration %d: %v", i, err)
}
be, err := getBooleanEntitlement(t, ctx, q, to.pool.PoolID, key)
if err != nil {
t.Fatalf("get boolean entitlement iteration %d: %v", i, err)
}
if i == 0 {
first = be
continue
}
if be.Granted != first.Granted || be.PoolID != first.PoolID || be.ResourceKey != first.ResourceKey || !be.CreatedAt.Equal(first.CreatedAt) {
t.Errorf("iteration %d produced different row: %+v vs %+v", i, be, first)
}
}
rows, err := q.ListBooleanEntitlementsByPoolID(ctx, to.pool.PoolID)
if err != nil {
t.Fatalf("list boolean entitlements: %v", err)
}
if len(rows) != 1 {
t.Errorf("expected exactly 1 boolean row after repeated materialize, got %d", len(rows))
}
}
// Scenario: read-surface query returns exactly the pools with active carriers,
// each with its owning organization.
func TestListPoolsGrantedBooleanKey(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := entitlements.BeginRuleChange(ctx, database)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
q := entitlements.New(tx)
key := "test.bool-" + uuid.New().String()[:8]
createBoolResourceKey(t, ctx, tx, key)
product := createBoolTestProduct(t, ctx, tx, "BoolProduct", key)
orgA := setupTestOrg(t, ctx, tx)
orgB := setupTestOrg(t, ctx, tx)
orgC := setupTestOrg(t, ctx, tx)
createGrantAndProvision(t, ctx, tx, q, orgA, product.productID, 1)
createGrantAndProvision(t, ctx, tx, q, orgB, product.productID, 1)
grantC := createGrantAndProvision(t, ctx, tx, q, orgC, product.productID, 1)
endGrantConferral(t, ctx, q, grantC.GrantID, orgC.pool.PoolID)
granted, err := q.ListPoolsGrantedBooleanKey(ctx, key)
if err != nil {
t.Fatalf("list pools granted: %v", err)
}
if len(granted) != 2 {
t.Fatalf("expected 2 granted pools, got %d", len(granted))
}
wantPools := map[string]string{
orgA.pool.PoolID: orgA.org.OrgID,
orgB.pool.PoolID: orgB.org.OrgID,
}
for _, row := range granted {
wantOrg, ok := wantPools[row.PoolID]
if !ok {
t.Errorf("unexpected pool %s in results", row.PoolID)
continue
}
if row.OrgID != wantOrg {
t.Errorf("pool %s: expected org %s, got %s", row.PoolID, wantOrg, row.OrgID)
}
}
}