Files
member-console/internal/server/operator_grants_delivery_state_test.go
T
cgalo5758 257955c9d3 Add operator list-scale contract and People directory
Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.

People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.

Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.

Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.

Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
2026-08-24 03:58:18 -05:00

166 lines
7.4 KiB
Go

package server_test
// DB-backed integration coverage for the shared ListGrantsWithDelivery query
// (ux-honest-surfaces design decision 1) end to end: real IssueGrant / Extend
// / Revoke flows against a real Postgres, read back through both the
// org-detail composite and the grants index, asserting the two surfaces
// agree on delivery-state wording and that supersession lineage is visible.
//
// Also locks in the derivation's precise "superseded" boundary: a grant
// whose provision simply ended with no recorded successor (a plain revoke)
// renders "Inactive", not "Superseded" -- "Superseded" is reserved for the
// case a replacing grant is actually on record (extends_grant_id), because
// the UI must be able to name what replaced a superseded row.
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
// postOrg posts to a handler with only {orgID} set (Issue Grant's form has
// no pool-scoped path parameter). Success/error copy fires as an HX-Trigger
// toast (fireSuccessToast/fireErrorToast), not inline body text -- callers
// assert against the returned trigger header, mirroring postOrgPool.
func (h *otcHarness) postOrg(handler http.HandlerFunc, orgID 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("orgID", orgID)
rec := httptest.NewRecorder()
handler(rec, req)
return rec.Code, rec.Body.String(), rec.Header().Get("HX-Trigger")
}
// postGrant posts to a handler with {grantID} set (Revoke).
func (h *otcHarness) postGrant(handler http.HandlerFunc, grantID 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("grantID", grantID)
rec := httptest.NewRecorder()
handler(rec, req)
return rec.Code, rec.Body.String(), rec.Header().Get("HX-Trigger")
}
// getOrg issues a GET against a handler with {orgID} set. target carries
// the query string (the composite's grants ledger tabs read ?tab=...).
func (h *otcHarness) getOrg(handler http.HandlerFunc, orgID, target string) (int, string) {
h.t.Helper()
req := httptest.NewRequestWithContext(h.ctx, http.MethodGet, target, nil)
req.SetPathValue("orgID", orgID)
rec := httptest.NewRecorder()
handler(rec, req)
return rec.Code, rec.Body.String()
}
func TestGrantDeliveryState_EndToEnd(t *testing.T) {
database := testDB(t)
f := newOtcFixture(t, database)
h := newOtcHarness(t, database, f.operatorID)
org, pool := otcOrg(t, database, f, "Delivery State Org", true)
// Issue on ladder A's product: creates a live grant with no lineage.
code, body, trigger := h.postOrg(h.handler.IssueGrant, org, url.Values{
"product_id": {f.prodA}, "reason": {"manual"}, "quantity": {"1"},
})
if code != http.StatusOK || !strings.Contains(trigger, "issued") {
t.Fatalf("issue grant A: status=%d trigger=%q body=%s", code, trigger, body)
}
// Issue on ladder B's product too: this one gets revoked with no
// successor, so it must land on "Inactive", not "Superseded".
code, body, trigger = h.postOrg(h.handler.IssueGrant, org, url.Values{
"product_id": {f.prodB}, "reason": {"manual"}, "quantity": {"1"},
})
if code != http.StatusOK || !strings.Contains(trigger, "issued") {
t.Fatalf("issue grant B: status=%d trigger=%q body=%s", code, trigger, body)
}
grantA := otcScalar(t, database, `SELECT count(*) FROM core.grants WHERE granted_to_org_id = $1 AND product_id = $2`, org, f.prodA)
if grantA != 1 {
t.Fatalf("expected exactly one grant for product A, found %d", grantA)
}
var grantBID string
if err := database.QueryRow(`SELECT grant_id FROM core.grants WHERE granted_to_org_id = $1 AND product_id = $2`, org, f.prodB).Scan(&grantBID); err != nil {
t.Fatalf("resolve grant B id: %v", err)
}
// Extend A's delivery: the form names A's provision (extension is
// position-scoped, ux-operator-scale review round) and the handler
// issues+confers a replacement, which must supersede A's position and
// mark A "Superseded" / the new grant "Live", with lineage naming each
// other.
code, body, trigger = h.postOrgPool(h.handler.ExtendGrant, org, pool, url.Values{
"reason": {"extend test"},
"provision_id": {activeProvisionID(t, database, pool, f.prodA)},
})
if code != http.StatusOK || !strings.Contains(trigger, "extended") {
t.Fatalf("extend grant A: status=%d trigger=%q body=%s", code, trigger, body)
}
// Revoke B with no successor.
code, body, trigger = h.postGrant(h.handler.RevokeGrantAndTransition, grantBID, url.Values{"org_id": {org}})
if code != http.StatusOK || !strings.Contains(trigger, "revoked") {
t.Fatalf("revoke grant B: status=%d trigger=%q body=%s", code, trigger, body)
}
// Org-detail composite, History tab (the full ledger): A is Superseded
// with a named replacement, the new grant is Live and names A as what
// it extends, and B is Inactive with no "replaced by" (it was revoked
// outright, no successor).
code, orgBody := h.getOrg(h.handler.GetOrgEnrollment, org, "/?tab=history")
if code != http.StatusOK {
t.Fatalf("get org enrollment: status=%d body=%s", code, orgBody)
}
assertDeliveryRendering(t, "org-detail history tab", orgBody)
// The default (Active) tab shows only rows delivering right now: the
// Live replacement with its lineage, and neither of the ended rows.
code, activeBody := h.getOrg(h.handler.GetOrgEnrollment, org, "/")
if code != http.StatusOK {
t.Fatalf("get org enrollment (active tab): status=%d body=%s", code, activeBody)
}
if !strings.Contains(activeBody, ">Live<") || !strings.Contains(activeBody, "extends") {
t.Errorf("active tab: expected the Live grant with its lineage, got:\n%s", activeBody)
}
if strings.Contains(activeBody, ">Superseded<") || strings.Contains(activeBody, ">Inactive<") {
t.Errorf("active tab: history rows must not render on the default tab, got:\n%s", activeBody)
}
// Grants index: same derivation, same wording -- design decision 1
// ("one query, two consumers") means these two reads must agree.
req := httptest.NewRequestWithContext(h.ctx, http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
h.handler.GetGrantsPage(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("get grants page: status=%d body=%s", rec.Code, rec.Body.String())
}
assertDeliveryRendering(t, "grants index", rec.Body.String())
}
// assertDeliveryRendering checks the delivery-state vocabulary and
// supersession lineage shared between the org-detail composite and the
// grants index (ux-honest-surfaces UX-3).
func assertDeliveryRendering(t *testing.T, surface, body string) {
t.Helper()
if !strings.Contains(body, ">Live<") {
t.Errorf("%s: expected a Live delivery badge, got:\n%s", surface, body)
}
if !strings.Contains(body, ">Superseded<") {
t.Errorf("%s: expected a Superseded delivery badge, got:\n%s", surface, body)
}
if !strings.Contains(body, ">Inactive<") {
t.Errorf("%s: expected an Inactive delivery badge (the plain revoke with no successor), got:\n%s", surface, body)
}
if !strings.Contains(body, "replaced by") {
t.Errorf("%s: expected the superseded grant to name its replacement, got:\n%s", surface, body)
}
if !strings.Contains(body, "extends") {
t.Errorf("%s: expected the replacing grant to name what it extends, got:\n%s", surface, body)
}
}