package console // The account-level object-storage entitlement doors (issue #381). These // tests pin the authority boundary first: only an account owner may apply, // while decisions belong to a deployment operator who need not be a member // of the account. They then pin the record transitions and the operator queue. import ( "context" "errors" "net/http" "strings" "testing" "time" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/svcrole" ) func entitlementApplyURL(fx *consoleFixture, account string) string { return fx.ts.URL + "/api/cli/accounts/" + account + "/store/apply" } func entitlementDecisionURL(fx *consoleFixture, account, decision string) string { return fx.ts.URL + "/api/cli/accounts/" + account + "/store/" + decision } func entitlementApprovalsURL(fx *consoleFixture) string { return fx.ts.URL + "/api/cli/store/approvals" } func storedEntitlement(t *testing.T, fx *consoleFixture, account string) *objstore.Entitlement { t.Helper() e, _, err := objstore.GetEntitlement(context.Background(), fx.rel, account) if err != nil { t.Fatalf("read entitlement for %s: %v", account, err) } return e } func putEntitlement(t *testing.T, fx *consoleFixture, account string, e *objstore.Entitlement) { t.Helper() if _, err := objstore.PutEntitlement(context.Background(), fx.rel, account, e, ""); err != nil { t.Fatalf("seed entitlement for %s: %v", account, err) } } func entitlementOperatorToken(t *testing.T, fx *consoleFixture, address, code string) string { t.Helper() if err := fx.st.SetOperator(address, true); err != nil { t.Fatal(err) } fx.srv.cfg.StoreOperators = []string{address} return cliToken(t, fx, address, code) } func entitlementBlock(t *testing.T, out map[string]any) map[string]any { t.Helper() return block(t, out, "entitlement") } func TestAPIStoreApplyNeedsAnOwner(t *testing.T) { fx := newConsoleFixture(t) if err := fx.st.AddMember(OpsAccountID, "member@keymail.dev", RoleMember, time.Now()); err != nil { t.Fatal(err) } token := cliToken(t, fx, "member@keymail.dev", "code-store-apply-member") status, _ := postJSON(t, entitlementApplyURL(fx, fx.sqid), map[string]any{"note": "uploads"}, token) if status != http.StatusForbidden { t.Fatalf("apply by a non-owner = %d, want 403", status) } if got := storedEntitlement(t, fx, fx.sqid); got != nil { t.Fatalf("refused apply wrote %+v", got) } } func TestAPIStoreApplyHidesUnknownAndOtherAccounts(t *testing.T) { fx := newConsoleFixture(t) eventoken := cliToken(t, fx, "eve@keymail.dev", "code-store-apply-stranger") owner := cliToken(t, fx, "paul@keymail.dev", "code-store-apply-unknown") statusA, bodyA := postRaw(t, entitlementApplyURL(fx, fx.sqid), map[string]any{}, eventoken) statusB, bodyB := postRaw(t, entitlementApplyURL(fx, "zzzz"), map[string]any{}, owner) if statusA != http.StatusNotFound || statusB != http.StatusNotFound { t.Fatalf("apply probes = %d / %d, want 404 for both", statusA, statusB) } if bodyA != bodyB || bodyA != "404 page not found\n" { t.Fatalf("apply probe bodies = %q / %q, want byte-identical stdlib 404", bodyA, bodyB) } } func TestAPIStoreApplyWritesAndRepliesWithTheEntitlement(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-store-apply") status, out := postJSON(t, entitlementApplyURL(fx, fx.sqid), map[string]any{"note": "customer uploads"}, token) if status != http.StatusOK { t.Fatalf("apply = %d %v, want 200", status, out) } if str(t, out, "account") != fx.sqid { t.Fatalf("account = %v, want %s", out["account"], fx.sqid) } e := entitlementBlock(t, out) applied := block(t, e, "applied") if applied["by"] != "paul@keymail.dev" || applied["note"] != "customer uploads" { t.Fatalf("applied = %v", applied) } if e["decision"] != nil { t.Fatalf("decision = %v, want null while pending", e["decision"]) } got := storedEntitlement(t, fx, fx.sqid) if got == nil || !got.Pending() || got.Applied.By != "paul@keymail.dev" || got.Applied.Note != "customer uploads" { t.Fatalf("stored entitlement = %+v, want the pending application", got) } auditedBy(t, fx, "", "store-apply", "paul@keymail.dev", "customer uploads") } func TestAPIStoreApplyReopensADeclineUsingCAS(t *testing.T) { fx := newConsoleFixture(t) oldApplied := time.Now().Add(-time.Hour).UTC() putEntitlement(t, fx, fx.sqid, &objstore.Entitlement{ Applied: &objstore.Applied{By: "paul@keymail.dev", At: oldApplied, Note: "first ask"}, Decision: &objstore.Decision{Approved: false, By: "ops@keymail.dev", At: time.Now().Add(-time.Minute), Reason: "more detail needed"}, }) token := cliToken(t, fx, "paul@keymail.dev", "code-store-reapply") status, out := postJSON(t, entitlementApplyURL(fx, fx.sqid), map[string]any{"note": "expanded plan"}, token) if status != http.StatusOK { t.Fatalf("re-apply = %d %v, want 200", status, out) } got := storedEntitlement(t, fx, fx.sqid) if got == nil || !got.Pending() || got.Decision != nil { t.Fatalf("re-applied entitlement = %+v, want pending with the decision cleared", got) } if got.Applied.Note != "expanded plan" || !got.Applied.At.After(oldApplied) { t.Fatalf("re-applied block = %+v, want a fresh application", got.Applied) } } func TestAPIStoreApplySurfacesACASConflictWithoutOverwritingTheWinner(t *testing.T) { fx := newConsoleFixture(t) putEntitlement(t, fx, fx.sqid, &objstore.Entitlement{ Applied: &objstore.Applied{By: "paul@keymail.dev", At: time.Now().Add(-time.Hour), Note: "first ask"}, Decision: &objstore.Decision{Approved: false, By: "ops@keymail.dev", At: time.Now().Add(-time.Minute), Reason: "revise"}, }) racer := &racingStore{Store: fx.rel, raceKeyOnce: objstore.EntitlementKey(fx.sqid)} racer.race = func(inner release.Store) { winner, etag, err := objstore.GetEntitlement(context.Background(), inner, fx.sqid) if err != nil { t.Fatal(err) } winner.Applied.Note = "the concurrent winner" if _, err := objstore.PutEntitlement(context.Background(), inner, fx.sqid, winner, etag); err != nil { t.Fatal(err) } } fx.srv.cfg.Releases = racer token := cliToken(t, fx, "paul@keymail.dev", "code-store-apply-cas") status, _ := postJSON(t, entitlementApplyURL(fx, fx.sqid), map[string]any{"note": "the losing write"}, token) if status != http.StatusConflict { t.Fatalf("apply losing its CAS = %d, want 409", status) } got := storedEntitlement(t, fx, fx.sqid) if got.Applied.Note != "the concurrent winner" { t.Fatalf("application after conflict = %+v, want the concurrent winner preserved", got.Applied) } } func TestAPIStoreApplyUsesTheSharedWriteBudget(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cli.write = newRateLimiter(1, time.Minute) token := cliToken(t, fx, "paul@keymail.dev", "code-store-apply-limit") if status, out := postJSON(t, entitlementApplyURL(fx, fx.sqid), map[string]any{}, token); status != http.StatusOK { t.Fatalf("first apply = %d %v, want 200", status, out) } if status, _ := postJSON(t, entitlementApplyURL(fx, fx.sqid), map[string]any{}, token); status != http.StatusTooManyRequests { t.Fatalf("second apply on an exhausted budget = %d, want 429", status) } } func TestAPIStoreApplyToAnApprovedAccountIsANoOp(t *testing.T) { fx := newConsoleFixture(t) want := &objstore.Entitlement{ Applied: &objstore.Applied{By: "paul@keymail.dev", At: time.Now().Add(-time.Hour).UTC(), Note: "original"}, Decision: &objstore.Decision{Approved: true, By: "ops@keymail.dev", At: time.Now().Add(-time.Minute).UTC()}, } putEntitlement(t, fx, fx.sqid, want) token := cliToken(t, fx, "paul@keymail.dev", "code-store-apply-approved") status, out := postJSON(t, entitlementApplyURL(fx, fx.sqid), map[string]any{"note": "must not replace approval"}, token) if status != http.StatusOK { t.Fatalf("apply when approved = %d %v, want 200", status, out) } got := storedEntitlement(t, fx, fx.sqid) if got == nil || !got.Approved() || got.Decision.By != want.Decision.By || !got.Decision.At.Equal(want.Decision.At) { t.Fatalf("approved decision changed: got %+v, want %+v", got, want) } if got.Applied.Note != "original" || !got.Applied.At.Equal(want.Applied.At) { t.Fatalf("approved application changed: got %+v, want %+v", got.Applied, want.Applied) } } func TestAPIStoreDecisionNeedsBothOperatorAuthorities(t *testing.T) { tests := []struct { name string fleet bool allowed bool }{ {"ordinary user on allowlist", false, true}, {"fleet operator absent from allowlist", true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fx := newConsoleFixture(t) putEntitlement(t, fx, fx.sqid, &objstore.Entitlement{Applied: &objstore.Applied{By: "paul@keymail.dev", At: time.Now()}}) const address = "candidate@keymail.dev" if tt.fleet { if err := fx.st.SetOperator(address, true); err != nil { t.Fatal(err) } } if tt.allowed { fx.srv.cfg.StoreOperators = []string{address} } token := cliToken(t, fx, address, "code-store-authority-"+tt.name) status, body := postRaw(t, entitlementDecisionURL(fx, fx.sqid, "approve"), map[string]any{}, token) if status != http.StatusNotFound || body != "404 page not found\n" { t.Fatalf("approve with only one authority bit = %d %q, want bare 404", status, body) } status, body = getRaw(t, entitlementApprovalsURL(fx), token) if status != http.StatusNotFound || body != "404 page not found\n" { t.Fatalf("approvals with only one authority bit = %d %q, want bare 404", status, body) } if !storedEntitlement(t, fx, fx.sqid).Pending() { t.Fatal("refused operator changed the pending entitlement") } }) } } func TestAPIStoreOperatorMatchingIsExact(t *testing.T) { fx := newConsoleFixture(t) const lookalike = "owner@example.teſt" if err := fx.st.SetOperator(lookalike, true); err != nil { t.Fatal(err) } fx.srv.cfg.StoreOperators = []string{"owner@example.test"} token := cliToken(t, fx, lookalike, "code-store-long-s") status, body := getRaw(t, entitlementApprovalsURL(fx), token) if status != http.StatusNotFound || body != "404 page not found\n" { t.Fatalf("lookalike operator = %d %q, want bare 404", status, body) } } func TestAPIStoreNonMemberOperatorCanApprove(t *testing.T) { fx := newConsoleFixture(t) putEntitlement(t, fx, fx.sqid, &objstore.Entitlement{Applied: &objstore.Applied{By: "paul@keymail.dev", At: time.Now(), Note: "backups"}}) token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-approve-nonmember") role, err := fx.st.Role(OpsAccountID, "ops@keymail.dev") if err != nil { t.Fatal(err) } if role != "" { t.Fatalf("operator role in target account = %q, want none", role) } status, out := postJSON(t, entitlementDecisionURL(fx, fx.sqid, "approve"), map[string]any{}, token) if status != http.StatusOK { t.Fatalf("approve by non-member operator = %d %v, want 200", status, out) } if str(t, out, "account") != fx.sqid { t.Fatalf("account = %v, want %s", out["account"], fx.sqid) } got := storedEntitlement(t, fx, fx.sqid) if got == nil || !got.Approved() || got.Decision.By != "ops@keymail.dev" || got.Decision.Reason != "" { t.Fatalf("approved entitlement = %+v", got) } auditedBy(t, fx, "", "store-approve", "ops@keymail.dev", "approved") } func TestAPIStoreOperatorDecisionsHideUnknownAccounts(t *testing.T) { fx := newConsoleFixture(t) token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-decision-unknown") for _, decision := range []string{"approve", "decline"} { status, body := postRaw(t, entitlementDecisionURL(fx, "zzzz", decision), map[string]any{"reason": "irrelevant"}, token) if status != http.StatusNotFound || body != "404 page not found\n" { t.Errorf("%s unknown account = %d %q, want bare 404", decision, status, body) } } } func TestAPIStoreApproveNeedsAnApplication(t *testing.T) { fx := newConsoleFixture(t) token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-approve-empty") status, _ := postJSON(t, entitlementDecisionURL(fx, fx.sqid, "approve"), map[string]any{}, token) if status != http.StatusConflict { t.Fatalf("approve without an application = %d, want 409", status) } if got := storedEntitlement(t, fx, fx.sqid); got != nil { t.Fatalf("approve without an application wrote %+v", got) } } func TestAPIStoreDeclineRequiresAReasonAndMayReplaceApproval(t *testing.T) { fx := newConsoleFixture(t) putEntitlement(t, fx, fx.sqid, &objstore.Entitlement{ Applied: &objstore.Applied{By: "paul@keymail.dev", At: time.Now().Add(-time.Hour)}, Decision: &objstore.Decision{Approved: true, By: "first-op@keymail.dev", At: time.Now().Add(-time.Minute)}, }) token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-decline") status, _ := postJSON(t, entitlementDecisionURL(fx, fx.sqid, "decline"), map[string]any{}, token) if status != http.StatusBadRequest { t.Fatalf("decline without a reason = %d, want 400", status) } if !storedEntitlement(t, fx, fx.sqid).Approved() { t.Fatal("a refused decline changed the approval") } status, out := postJSON(t, entitlementDecisionURL(fx, fx.sqid, "decline"), map[string]any{"reason": "capacity review"}, token) if status != http.StatusOK { t.Fatalf("decline = %d %v, want 200", status, out) } got := storedEntitlement(t, fx, fx.sqid) if got == nil || !got.Declined() || got.Decision.By != "ops@keymail.dev" || got.Decision.Reason != "capacity review" { t.Fatalf("declined entitlement = %+v", got) } auditedBy(t, fx, "", "store-decline", "ops@keymail.dev", "capacity review") } func TestAPIStoreDecisionSurfacesACASConflictWithoutOverwritingTheWinner(t *testing.T) { fx := newConsoleFixture(t) putEntitlement(t, fx, fx.sqid, &objstore.Entitlement{ Applied: &objstore.Applied{By: "paul@keymail.dev", At: time.Now().Add(-time.Hour)}, }) racer := &racingStore{Store: fx.rel, raceKeyOnce: objstore.EntitlementKey(fx.sqid)} racer.race = func(inner release.Store) { winner, etag, err := objstore.GetEntitlement(context.Background(), inner, fx.sqid) if err != nil { t.Fatal(err) } winner.Decision = &objstore.Decision{Approved: false, By: "winner@keymail.dev", At: time.Now(), Reason: "winner's decision"} if _, err := objstore.PutEntitlement(context.Background(), inner, fx.sqid, winner, etag); err != nil { t.Fatal(err) } } fx.srv.cfg.Releases = racer token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-decision-cas") status, _ := postJSON(t, entitlementDecisionURL(fx, fx.sqid, "approve"), map[string]any{}, token) if status != http.StatusConflict { t.Fatalf("decision losing its CAS = %d, want 409", status) } got := storedEntitlement(t, fx, fx.sqid) if got == nil || !got.Declined() || got.Decision.By != "winner@keymail.dev" { t.Fatalf("entitlement after conflict = %+v, want the concurrent winner preserved", got) } } func TestAPIStoreApprovalsListsPendingOldestFirst(t *testing.T) { fx := newConsoleFixture(t) now := time.Now().UTC() const secondID int64 = 4242 secondSqid := accountSqid(secondID) if err := fx.st.CreateAccount(secondID, secondSqid, "Second", "second@keymail.dev", now); err != nil { t.Fatal(err) } putEntitlement(t, fx, secondSqid, &objstore.Entitlement{Applied: &objstore.Applied{By: "second@keymail.dev", At: now.Add(-2 * time.Hour), Note: "oldest"}}) putEntitlement(t, fx, fx.sqid, &objstore.Entitlement{Applied: &objstore.Applied{By: "paul@keymail.dev", At: now.Add(-time.Hour), Note: "newer"}}) token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-approvals-order") status, out := getJSON(t, entitlementApprovalsURL(fx), token) if status != http.StatusOK { t.Fatalf("approvals = %d %v, want 200", status, out) } raw, ok := out["applications"].([]any) if !ok || len(raw) != 2 { t.Fatalf("applications = %#v, want two entries", out["applications"]) } first, ok := raw[0].(map[string]any) if !ok { t.Fatalf("applications[0] = %#v, want an object", raw[0]) } second, ok := raw[1].(map[string]any) if !ok { t.Fatalf("applications[1] = %#v, want an object", raw[1]) } if first["account"] != secondSqid || second["account"] != fx.sqid { t.Fatalf("application order = %v then %v, want %s then %s", first["account"], second["account"], secondSqid, fx.sqid) } if _, ok := first["applied"].(map[string]any); !ok { t.Fatalf("first application = %#v, want an entitlement object", first) } } func TestAPIStoreApprovalsFiltersDecidedDeletedAndUnknownAccounts(t *testing.T) { fx := newConsoleFixture(t) now := time.Now().UTC() const deletedID int64 = 4343 deletedSqid := accountSqid(deletedID) if err := fx.st.CreateAccount(deletedID, deletedSqid, "Deleted", "deleted@keymail.dev", now); err != nil { t.Fatal(err) } if err := fx.st.DeleteAccount(deletedID, now.Add(time.Minute)); err != nil { t.Fatal(err) } putEntitlement(t, fx, deletedSqid, &objstore.Entitlement{Applied: &objstore.Applied{By: "deleted@keymail.dev", At: now.Add(-3 * time.Hour)}}) putEntitlement(t, fx, "bde", &objstore.Entitlement{Applied: &objstore.Applied{By: "orphan@keymail.dev", At: now.Add(-2 * time.Hour)}}) putEntitlement(t, fx, fx.sqid, &objstore.Entitlement{ Applied: &objstore.Applied{By: "paul@keymail.dev", At: now.Add(-time.Hour)}, Decision: &objstore.Decision{Approved: false, By: "ops@keymail.dev", At: now, Reason: "decided"}, }) token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-approvals-filter") status, out := getJSON(t, entitlementApprovalsURL(fx), token) if status != http.StatusOK { t.Fatalf("approvals = %d %v, want 200", status, out) } raw, ok := out["applications"].([]any) if !ok { t.Fatalf("applications = %#v, want a list", out["applications"]) } if len(raw) != 0 { t.Fatalf("applications = %#v, want decided, deleted, and unknown accounts filtered", raw) } } type entitlementListErrorStore struct{ release.Store } func (s entitlementListErrorStore) List(ctx context.Context, prefix string) ([]string, error) { if prefix == "store/entitlements/" { return nil, errors.New("list unavailable") } return s.Store.List(ctx, prefix) } func TestAPIStoreApprovalsFailsWhenTheQueueCannotBeListed(t *testing.T) { fx := newConsoleFixture(t) fx.srv.cfg.Releases = entitlementListErrorStore{Store: fx.rel} token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-approvals-list-error") status, _ := getJSON(t, entitlementApprovalsURL(fx), token) if status != http.StatusBadGateway { t.Fatalf("approvals with a failed list = %d, want 502", status) } } func TestAPIStoreApprovalsFailsOnAMalformedEntitlement(t *testing.T) { fx := newConsoleFixture(t) if _, err := fx.rel.Put(context.Background(), objstore.EntitlementKey(fx.sqid), []byte("not json"), ""); err != nil { t.Fatal(err) } token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-approvals-malformed") status, _ := getJSON(t, entitlementApprovalsURL(fx), token) if status != http.StatusBadGateway { t.Fatalf("approvals with malformed entitlement = %d, want 502", status) } } func TestAPIStoreApprovalsIgnoresNonCanonicalKeys(t *testing.T) { fx := newConsoleFixture(t) if _, err := fx.rel.Put(context.Background(), "store/entitlements/nested/bab.json", []byte("not json"), ""); err != nil { t.Fatal(err) } if _, err := fx.rel.Put(context.Background(), "store/entitlements/readme", []byte("not json"), ""); err != nil { t.Fatal(err) } token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-approvals-keys") status, out := getJSON(t, entitlementApprovalsURL(fx), token) if status != http.StatusOK { t.Fatalf("approvals with unrelated keys = %d %v, want 200", status, out) } if raw, ok := out["applications"].([]any); !ok || len(raw) != 0 { t.Fatalf("applications = %#v, want an empty list", out["applications"]) } } func TestAPIStoreApprovalsTrustsTheKeyForTheAccount(t *testing.T) { fx := newConsoleFixture(t) now := time.Now().UTC() putEntitlement(t, fx, fx.sqid, &objstore.Entitlement{Applied: &objstore.Applied{By: "paul@keymail.dev", At: now}}) raw, etag, err := fx.rel.Get(context.Background(), objstore.EntitlementKey(fx.sqid)) if err != nil { t.Fatal(err) } raw = []byte(strings.Replace(string(raw), `"account": "`+fx.sqid+`"`, `"account": "zzz"`, 1)) if _, err := fx.rel.Put(context.Background(), objstore.EntitlementKey(fx.sqid), raw, etag); err != nil { t.Fatal(err) } token := entitlementOperatorToken(t, fx, "ops@keymail.dev", "code-store-approvals-account") status, out := getJSON(t, entitlementApprovalsURL(fx), token) if status != http.StatusOK { t.Fatalf("approvals = %d %v, want 200", status, out) } applications := out["applications"].([]any) got := applications[0].(map[string]any)["account"] if got != fx.sqid { t.Fatalf("application account = %v, want key account %s", got, fx.sqid) } } // The projection tests (§4 versus §8, resolved 2026-09-08: a person may read // why they were declined, a machine may not, and the deciding operator's // address travels to neither). These read the STORE door rather than the apply // door because that is the one an app's own service credential can reach. func declinedStoreFixture(t *testing.T, code string) *consoleFixture { t.Helper() fx, _ := storeFixture(t, code) now := time.Now() putRecord(t, fx, "titogo", &objstore.Record{ Declared: &objstore.Declared{EnvPrefix: "CARLOS_STORE", At: now, By: "alice@keymail.dev"}, }) putEntitlement(t, fx, release.OpsAccount, &objstore.Entitlement{ Applied: &objstore.Applied{By: "alice@keymail.dev", At: now, Note: "customer uploads"}, Decision: &objstore.Decision{ Approved: false, By: "paul@keymail.dev", At: now, Reason: "not until the account is on a card", }, }) return fx } func TestAPIStoreReplyGivesAMemberTheReasonButNotTheOperator(t *testing.T) { fx := declinedStoreFixture(t, "code-store-project-person") token := cliToken(t, fx, "alice@keymail.dev", "code-store-project-person-2") status, out := getJSON(t, storeURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("status = %d %v, want 200", status, out) } e := entitlementBlock(t, out) d := block(t, e, "decision") if d["reason"] != "not until the account is on a card" { // The whole point of requiring a reason is that the account learns it. t.Fatalf("decision.reason = %v, want the reason a person was given", d["reason"]) } if _, present := d["by"]; present { t.Fatalf("decision.by = %v, want the deciding operator's address withheld from the tenant", d["by"]) } if applied := block(t, e, "applied"); applied["note"] != "customer uploads" { t.Fatalf("applied.note = %v, want the applicant's own text returned to them", applied["note"]) } } func TestAPIStoreReplyWithholdsTheReasonFromAServiceCredential(t *testing.T) { fx := declinedStoreFixture(t, "code-store-project-service") svc := serviceToken(t, fx.st, OpsAccountID, "uploader", "titogo", svcrole.Admin) status, out := getJSON(t, storeURL(fx, "titogo"), svc) if status != http.StatusOK { t.Fatalf("status = %d %v, want 200", status, out) } e := entitlementBlock(t, out) d := block(t, e, "decision") // The state still has to arrive — a machine reading "not approved" is the // whole reason the block travels at all. if d["approved"] != false { t.Fatalf("decision.approved = %v, want false", d["approved"]) } for _, k := range []string{"reason", "by"} { if v, present := d[k]; present { t.Fatalf("decision.%s = %v, want it withheld from a service credential", k, v) } } applied := block(t, e, "applied") for _, k := range []string{"note", "by"} { if v, present := applied[k]; present { t.Fatalf("applied.%s = %v, want it withheld from a service credential", k, v) } } }