Files
member-console/internal/entitlements/rendezvous_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

107 lines
3.3 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package entitlements_test
import (
"context"
"strings"
"testing"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"github.com/google/uuid"
)
// Scenario: a transaction that materializes without holding the rendezvous
// is refused by the assertion, and the error names the pool.
func TestMaterializeWithoutRendezvousIsRefused(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := database.BeginTx(ctx, nil) // deliberately not BeginMaterializing
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
poolID := uuid.New().String()
err = entitlements.MaterializePoolEntitlements(ctx, entitlements.New(tx), poolID)
if err == nil {
t.Fatal("expected the assertion to refuse a materialization without the rendezvous")
}
if !entitlements.IsRendezvousMissing(err) {
t.Fatalf("expected materialization_rendezvous_missing, got: %v", err)
}
if !strings.Contains(err.Error(), poolID) {
t.Errorf("expected the error to name pool %s, got: %v", poolID, err)
}
}
// Scenario: the shared rendezvous is granted while no rule change holds it.
func TestMaterializingTransactionHoldsRendezvous(t *testing.T) {
database := testDB(t)
ctx := context.Background()
tx, err := entitlements.BeginMaterializing(ctx, database, nil)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, "SELECT core.assert_rendezvous(false)"); err != nil {
t.Fatalf("expected the shared rendezvous to satisfy the assertion: %v", err)
}
if _, err := tx.ExecContext(ctx, "SELECT core.assert_rendezvous(true)"); err == nil || !entitlements.IsRendezvousMissing(err) {
t.Fatalf("expected the shared rendezvous to fail the exclusive assertion, got: %v", err)
}
}
// Scenario: a rule change holds the exclusive rendezvous; a materializer
// opened meanwhile waits and is granted only once the change is over.
func TestRuleChangeBlocksMaterializersUntilItEnds(t *testing.T) {
database := testDB(t)
ctx := context.Background()
holder, err := entitlements.BeginRuleChange(ctx, database)
if err != nil {
t.Fatal(err)
}
defer holder.Rollback() // a fatal below must not strand the exclusive rendezvous
if _, err := holder.ExecContext(ctx, "SELECT core.assert_rendezvous(true)"); err != nil {
t.Fatalf("expected the rule change to hold the exclusive rendezvous: %v", err)
}
const hold = 300 * time.Millisecond
waited := make(chan time.Duration, 1)
failed := make(chan error, 1)
start := time.Now()
go func() {
tx, err := entitlements.BeginMaterializing(ctx, database, nil)
if err != nil {
failed <- err
return
}
waited <- time.Since(start)
_ = tx.Rollback()
}()
time.Sleep(hold)
select {
case d := <-waited:
t.Fatalf("materializer was granted the rendezvous after %v while a rule change held it", d)
case err := <-failed:
t.Fatalf("materializer failed instead of waiting: %v", err)
default:
}
if err := holder.Rollback(); err != nil {
t.Fatal(err)
}
select {
case d := <-waited:
if d < hold {
t.Errorf("materializer proceeded after %v, before the rule change ended", d)
}
case err := <-failed:
t.Fatalf("materializer failed after the rule change ended: %v", err)
case <-time.After(10 * time.Second):
t.Fatal("materializer never proceeded after the rule change ended")
}
}