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.
166 lines
7.4 KiB
Go
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)
|
|
}
|
|
}
|