Files
member-console/internal/server/operator_tier_reorder_test.go
T
cgalo5758 11ecb80907 Add tier reorder preview and commit flow
Classify rank-zero changes for default ladders and require operators to
grandfather or migrate affected organizations before applying ranks.

Fix operator attribution during tier induction backfills and make the
grant
extension walkthrough's optional-form check panic-safe.
2026-07-12 17:49:50 -05:00

415 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package server_test
// Handler tests for the two-step tier reorder (tier-reorder-preview-commit
// tasks 5.15.3): the drop-preview is read-only, cosmetic commits write ranks
// only, a rank-0 change on a default ladder gates on the bucket-2 disposition,
// grandfather/migrate enact like the org-type default-change commit, re-runs
// are idempotent, and a reorder commit racing an org-type default-change
// commit serializes on the pool lock. Reuses the otc* fixture and harness
// (same package, operator_org_type_change_test.go).
import (
"context"
"database/sql"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
"github.com/google/uuid"
"git.coopcloud.tech/wiki-cafe/member-console/internal/billing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
)
// postLadder invokes a ladder-scoped handler with the harness's
// authenticated-operator context. Returns status, body, and the HX-Trigger
// header — mutation success copy travels as a toast trigger (ux-conventions
// §3), not in the body.
func (h *otcHarness) postLadder(handler http.HandlerFunc, ladderID string, form url.Values) (int, string, string) {
h.t.Helper()
req := httptest.NewRequestWithContext(h.ctx, http.MethodPost, "/", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetPathValue("ladderID", ladderID)
rec := httptest.NewRecorder()
handler(rec, req)
return rec.Code, rec.Body.String(), rec.Header().Get("HX-Trigger")
}
// trAddTier appends a fresh published product as the ladder's bottom tier
// (rank self-assigned), returning the product ID.
func trAddTier(t *testing.T, database *sql.DB, ladderID, name string) string {
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)
bq := billing.New(tx)
set, err := eq.CreateEntitlementSet(ctx, entitlements.CreateEntitlementSetParams{Name: name + "-set", IsActive: true})
if err != nil {
t.Fatalf("set %s: %v", name, err)
}
prod, err := bq.CreateProduct(ctx, billing.CreateProductParams{
Name: name + "-prod", IsActive: true, IsPublic: false,
EntitlementSetID: uuid.NullUUID{UUID: uuid.MustParse(set.SetID), Valid: true},
LifecycleStatus: "published",
})
if err != nil {
t.Fatalf("product %s: %v", name, err)
}
if _, err := bq.CreatePlanLadderTier(ctx, billing.CreatePlanLadderTierParams{
PlanLadderID: ladderID, ProductID: prod.ProductID,
}); err != nil {
t.Fatalf("tier %s: %v", name, err)
}
if err := tx.Commit(); err != nil {
t.Fatalf("commit tier: %v", err)
}
return prod.ProductID
}
func trRank(t *testing.T, database *sql.DB, ladderID, productID string) int {
t.Helper()
return otcScalar(t, database, `SELECT rank FROM core.plan_ladder_tiers WHERE plan_ladder_id = $1 AND product_id = $2`, ladderID, productID)
}
// The drop-preview classifies the population against the pending rank-0
// product, renders the pending panel with disposition controls only when an
// outgoing-default bucket is non-empty, and writes nothing.
func TestTierReorder_PreviewIsReadOnlyAndClassifies(t *testing.T) {
database := testDB(t)
f := newOtcFixture(t, database)
h := newOtcHarness(t, database, f.operatorID)
sfx := uuid.New().String()[:8]
prodC := trAddTier(t, database, f.ladderA, "tr-c-"+sfx)
_, holderPool := otcOrg(t, database, f, "TR Holder", true)
otcConferDefaultA(t, database, f, holderPool) // type default = ladder A; holder at prodA
before := otcCounts(t, database, f)
code, body, _ := h.postLadder(h.handler.PreviewPlanLadderTiersReorder, f.ladderA,
url.Values{"product": {prodC, f.prodA}})
if code != http.StatusOK {
t.Fatalf("preview status=%d body=%s", code, body)
}
for _, want := range []string{
"Preview — nothing saved yet",
"Rank 0 changes from",
"TR Holder", // outgoing-default orgs are named
`name="bucket2_disposition"`,
"Commit tier order",
"Discard",
} {
if !strings.Contains(body, want) {
t.Errorf("preview missing %q", want)
}
}
if r := trRank(t, database, f.ladderA, f.prodA); r != 0 {
t.Errorf("prodA rank = %d after preview, want 0 (read-only)", r)
}
if r := trRank(t, database, f.ladderA, prodC); r != 1 {
t.Errorf("prodC rank = %d after preview, want 1 (read-only)", r)
}
if after := otcCounts(t, database, f); before["grants"] != after["grants"] || before["provisions"] != after["provisions"] ||
before["junctions"] != after["junctions"] || before["transitions"] != after["transitions"] {
t.Errorf("preview wrote rows: before=%v after=%v", before, after)
}
t.Run("rank-0-unchanged order is display-order-only", func(t *testing.T) {
code, body, _ := h.postLadder(h.handler.PreviewPlanLadderTiersReorder, f.ladderA,
url.Values{"product": {f.prodA, prodC}})
if code != http.StatusOK {
t.Fatalf("preview status=%d", code)
}
if !strings.Contains(body, "Display order only") {
t.Errorf("cosmetic preview missing display-order-only copy")
}
if strings.Contains(body, `name="bucket2_disposition"`) {
t.Errorf("cosmetic preview rendered disposition radios")
}
})
t.Run("non-default ladder rank-0 change is display-order-only", func(t *testing.T) {
prodD := trAddTier(t, database, f.ladderB, "tr-d-"+sfx)
code, body, _ := h.postLadder(h.handler.PreviewPlanLadderTiersReorder, f.ladderB,
url.Values{"product": {prodD, f.prodB}})
if code != http.StatusOK {
t.Fatalf("preview status=%d", code)
}
if !strings.Contains(body, "not any org type's default") {
t.Errorf("non-default preview missing not-a-default copy")
}
if strings.Contains(body, `name="bucket2_disposition"`) {
t.Errorf("non-default preview rendered disposition radios")
}
})
}
// A commit whose pending rank 0 displaces default-sourced incumbents is
// rejected without a disposition — nothing written, not even ranks.
func TestTierReorder_CommitRequiresDisposition(t *testing.T) {
database := testDB(t)
f := newOtcFixture(t, database)
h := newOtcHarness(t, database, f.operatorID)
prodC := trAddTier(t, database, f.ladderA, "tr-c-"+uuid.New().String()[:8])
_, holderPool := otcOrg(t, database, f, "TR Holder", true)
otcConferDefaultA(t, database, f, holderPool)
before := otcCounts(t, database, f)
code, body, _ := h.postLadder(h.handler.ReorderPlanLadderTiers, f.ladderA,
url.Values{"product": {prodC, f.prodA}})
if code != http.StatusOK {
t.Fatalf("commit status=%d", code)
}
if !strings.Contains(body, "Choose whether to grandfather or migrate") {
t.Errorf("gate error not rendered; body=%.400s", body)
}
if r := trRank(t, database, f.ladderA, f.prodA); r != 0 {
t.Errorf("prodA rank = %d after rejected commit, want 0 (ranks untouched)", r)
}
if after := otcCounts(t, database, f); before["grants"] != after["grants"] ||
before["provisions"] != after["provisions"] || before["junctions"] != after["junctions"] ||
before["transitions"] != after["transitions"] {
t.Errorf("rejected commit wrote rows: before=%v after=%v", before, after)
}
}
// A commit that keeps rank 0 writes ranks only — no dispositions required or
// enacted, even with default-sourced incumbents present.
func TestTierReorder_CosmeticCommitWritesRanksOnly(t *testing.T) {
database := testDB(t)
f := newOtcFixture(t, database)
h := newOtcHarness(t, database, f.operatorID)
sfx := uuid.New().String()[:8]
prodC := trAddTier(t, database, f.ladderA, "tr-c-"+sfx)
prodD := trAddTier(t, database, f.ladderA, "tr-d-"+sfx)
_, holderPool := otcOrg(t, database, f, "TR Holder", true)
otcConferDefaultA(t, database, f, holderPool)
before := otcCounts(t, database, f)
code, body, trigger := h.postLadder(h.handler.ReorderPlanLadderTiers, f.ladderA,
url.Values{"product": {f.prodA, prodD, prodC}})
if code != http.StatusOK {
t.Fatalf("commit status=%d body=%.300s", code, body)
}
if !strings.Contains(trigger, "Tier order updated.") {
t.Errorf("cosmetic commit missing plain success toast; trigger=%q", trigger)
}
if r := trRank(t, database, f.ladderA, prodD); r != 1 {
t.Errorf("prodD rank = %d, want 1", r)
}
if r := trRank(t, database, f.ladderA, prodC); r != 2 {
t.Errorf("prodC rank = %d, want 2", r)
}
if after := otcCounts(t, database, f); before["grants"] != after["grants"] || before["provisions"] != after["provisions"] ||
before["junctions"] != after["junctions"] || before["transitions"] != after["transitions"] {
t.Errorf("cosmetic commit wrote conferral rows: before=%v after=%v", before, after)
}
}
// Grandfather enactment through the reorder commit: ranks renumber, the
// incumbent is reissued as an operator-attributed legacy grant with a transfer
// supersession (incumbent grant row untouched), plan-less pools initiate onto
// the new rank 0, org_types is never written, and re-running the same order is
// a no-op.
func TestTierReorder_GrandfatherCommitAndRerun(t *testing.T) {
database := testDB(t)
ctx := context.Background()
f := newOtcFixture(t, database)
h := newOtcHarness(t, database, f.operatorID)
prodC := trAddTier(t, database, f.ladderA, "tr-c-"+uuid.New().String()[:8])
_, planlessPool := otcOrg(t, database, f, "TR Planless", true)
holderOrg, holderPool := otcOrg(t, database, f, "TR Holder", true)
otcConferDefaultA(t, database, f, holderPool)
var incumbentGrant string
if err := database.QueryRowContext(ctx,
`SELECT grant_id FROM core.grants WHERE granted_to_org_id = $1 AND grant_reason = 'default'`, holderOrg,
).Scan(&incumbentGrant); err != nil {
t.Fatalf("resolve incumbent: %v", err)
}
code, body, trigger := h.postLadder(h.handler.ReorderPlanLadderTiers, f.ladderA,
url.Values{"product": {prodC, f.prodA}, "bucket2_disposition": {"grandfather"}})
if code != http.StatusOK {
t.Fatalf("commit status=%d body=%.300s", code, body)
}
if !strings.Contains(trigger, "is now the default plan for new") {
t.Errorf("commit toast missing the default-change headline; trigger=%q", trigger)
}
if r := trRank(t, database, f.ladderA, prodC); r != 0 {
t.Fatalf("prodC rank = %d, want 0", r)
}
// The reorder changes the effective default without touching org_types.
if got := otcDefaultLadder(t, database, f.orgType); got != f.ladderA {
t.Errorf("org_types default ladder changed to %q; a reorder must never write it", got)
}
// Legacy grant: operator-attributed, lineaged to the incumbent; transfer
// recorded; incumbent grant row untouched; holder still delivers prodA.
if n := otcScalar(t, database, `
SELECT count(*) FROM core.grants
WHERE granted_to_org_id = $1 AND grant_reason = 'legacy'
AND granted_by_person_id = $2 AND extends_grant_id = $3`,
holderOrg, f.operatorID, incumbentGrant); n != 1 {
t.Errorf("legacy grants = %d, want 1", n)
}
if n := otcScalar(t, database, `
SELECT count(*) FROM core.pool_provision_transitions
WHERE pool_id = $1 AND transition_type = 'transfer'`, holderPool); n != 1 {
t.Errorf("transfer transitions = %d, want 1", n)
}
if n := otcScalar(t, database, `
SELECT count(*) FROM core.grants
WHERE grant_id = $1 AND status = 'active' AND revoked_at IS NULL`, incumbentGrant); n != 1 {
t.Errorf("incumbent default grant was mutated")
}
if n := otcScalar(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'`, holderPool, f.prodA); n != 1 {
t.Errorf("holder prodA attachments = %d, want 1 (grandfathered in place)", n)
}
// The plan-less pool was initiated onto the new rank 0.
if n := otcScalar(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'`, planlessPool, prodC); n != 1 {
t.Errorf("planless pool prodC attachments = %d, want 1 (initiated onto new rank 0)", n)
}
// Idempotent re-run: the same order finds rank 0 unchanged — no enactment,
// zero new conferral rows.
before := otcCounts(t, database, f)
code, _, _ = h.postLadder(h.handler.ReorderPlanLadderTiers, f.ladderA,
url.Values{"product": {prodC, f.prodA}, "bucket2_disposition": {"grandfather"}})
if code != http.StatusOK {
t.Fatalf("re-run status=%d", code)
}
if after := otcCounts(t, database, f); before["grants"] != after["grants"] || before["provisions"] != after["provisions"] ||
before["junctions"] != after["junctions"] || before["transitions"] != after["transitions"] {
t.Errorf("re-run wrote rows: before=%v after=%v", before, after)
}
}
// Migrate enactment through the reorder commit: the incumbent default-source
// position ends and the floor-guarded restoration confers the new rank 0.
func TestTierReorder_MigrateCommit(t *testing.T) {
database := testDB(t)
f := newOtcFixture(t, database)
h := newOtcHarness(t, database, f.operatorID)
prodC := trAddTier(t, database, f.ladderA, "tr-c-"+uuid.New().String()[:8])
holderOrg, holderPool := otcOrg(t, database, f, "TR Holder", true)
otcConferDefaultA(t, database, f, holderPool)
code, body, _ := h.postLadder(h.handler.ReorderPlanLadderTiers, f.ladderA,
url.Values{"product": {prodC, f.prodA}, "bucket2_disposition": {"migrate"}})
if code != http.StatusOK {
t.Fatalf("commit status=%d body=%.300s", code, body)
}
if n := otcScalar(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'`, holderPool, f.prodA); n != 0 {
t.Errorf("prodA attachments = %d, want 0 (migrated off)", n)
}
if n := otcScalar(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'`, holderPool, prodC); n != 1 {
t.Errorf("prodC attachments = %d, want 1 (migrated onto new rank 0)", n)
}
if n := otcScalar(t, database, `
SELECT count(*) FROM core.pool_provision_transitions
WHERE pool_id = $1 AND transition_type = 'end'`, holderPool); n < 1 {
t.Errorf("no end transition recorded")
}
// Outgoing decree kept as ledger history + the fresh restoration decree.
if n := otcScalar(t, database, `
SELECT count(*) FROM core.grants
WHERE granted_to_org_id = $1 AND grant_reason = 'default' AND status = 'active' AND revoked_at IS NULL`,
holderOrg); n != 2 {
t.Errorf("default decrees = %d, want 2", n)
}
}
// Adding a provision-bearing product as a tier of another ladder runs the
// induction backfill with a fully-attributed operator actor. Regression: the
// handler passed ActorType "operator" with no ActorID, violating
// chk_pool_provision_transitions_operator_actor and rolling back every such
// add (found 2026-07-12 by the tier-reorder walkthrough).
func TestAddTierInductionAlignsLiveProvisions(t *testing.T) {
database := testDB(t)
f := newOtcFixture(t, database)
h := newOtcHarness(t, database, f.operatorID)
_, holderPool := otcOrg(t, database, f, "TR Holder", true)
otcConferDefaultA(t, database, f, holderPool) // prodA live on ladder A
code, body, _ := h.postLadder(h.handler.CreatePlanLadderTier, f.ladderB,
url.Values{"product_id": {f.prodA}})
if code != http.StatusOK {
t.Fatalf("add-tier status=%d", code)
}
if strings.Contains(body, "Failed to add tier") {
t.Fatalf("add-tier failed: %.300s", body)
}
// prodA is now a tier of ladder B, and the live provision gained the rung.
if n := otcScalar(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 l.plan_ladder_id = $2 AND p.product_id = $3
AND l.status IN ('active','suspended')`,
holderPool, f.ladderB, f.prodA); n != 1 {
t.Errorf("induction junction on ladder B = %d, want 1", n)
}
}
// A tier-reorder commit racing an org-type default-change commit on the same
// pool serializes on the per-pool FOR UPDATE lock: whichever enacts first
// grandfathers the incumbent, the other re-derives the bucket in-tx and finds
// a legacy-held (other-source) pool — never a second grandfather.
func TestTierReorder_CommitRacesDefaultChangeCommit(t *testing.T) {
database := testDB(t)
f := newOtcFixture(t, database)
h := newOtcHarness(t, database, f.operatorID)
prodC := trAddTier(t, database, f.ladderA, "tr-c-"+uuid.New().String()[:8])
_, holderPool := otcOrg(t, database, f, "TR Holder", true)
otcConferDefaultA(t, database, f, holderPool)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
h.postLadder(h.handler.ReorderPlanLadderTiers, f.ladderA,
url.Values{"product": {prodC, f.prodA}, "bucket2_disposition": {"grandfather"}})
}()
go func() {
defer wg.Done()
h.post(h.handler.CommitOrgTypeDefaultChange, f.orgType,
url.Values{"default_plan_ladder_id": {f.ladderB}, "bucket2_disposition": {"grandfather"}})
}()
wg.Wait()
if n := otcScalar(t, database, `
SELECT count(*) FROM core.grants g
JOIN core.organizations o ON o.org_id = g.granted_to_org_id
WHERE o.org_type = $1 AND g.grant_reason = 'legacy'`, f.orgType); n != 1 {
t.Errorf("legacy grants = %d, want exactly 1 (one winner)", n)
}
if n := otcScalar(t, database, `
SELECT count(*) FROM core.pool_provision_transitions
WHERE pool_id = $1 AND transition_type = 'transfer'`, holderPool); n != 1 {
t.Errorf("transfer transitions = %d, want exactly 1", n)
}
if n := otcScalar(t, database, `
SELECT count(*) FROM core.pool_provision_ladders
WHERE pool_id = $1 AND status IN ('active','suspended')`, holderPool); n != 1 {
t.Errorf("live attachments = %d, want exactly 1", n)
}
}