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.
297 lines
9.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|