Files
member-console/internal/entitlements/boolean_materialize_test.go
T
cgalo5758 6661787a51 Materialize boolean entitlements
Persist pool-scoped grants using OR semantics across active provisions,
retaining lapsed rows for transition visibility. Add consumer queries,
DB-backed coverage, and plans for the dependent Discourse integration.
2026-07-17 11:13:38 -07:00

297 lines
9.2 KiB
Go

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)
}
_, err = q.CreateEntitlementSetRule(ctx, entitlements.CreateEntitlementSetRuleParams{
SetID: set.SetID,
RuleType: "boolean",
ResourceKey: sql.NullString{String: resourceKey, Valid: true},
})
if err != nil {
t.Fatalf("create boolean rule for %s: %v", name, err)
}
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 := database.BeginTx(ctx, nil)
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 := database.BeginTx(ctx, nil)
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 := database.BeginTx(ctx, nil)
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 := database.BeginTx(ctx, nil)
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 := database.BeginTx(ctx, nil)
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 := database.BeginTx(ctx, nil)
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)
}
}
}