carlos / platform Public

mail: fix pause enforcement gaps found in review (C1, I1, I2, I4, I3, M3)

Rotate and Enable's repair re-walk both minted keys without checking the
record's own Active flag, so either one could silently lift a standing
pause at IAM while the record and console kept saying paused (C1, I1) — an
ordinary member rotate could defeat an abuse pause, and Task 16's automatic
enforcement depends on this holding. Fixed at one enforcement point,
enforceKeyActive, called unconditionally after every mint so it
self-corrects in both directions.

SetPaused never reached standalone credentials, so a tenant paused for
abuse could keep sending through a displayed credential on the same
domain (I2) — each is its own IAM user, so the pause now loops over them
too.

Teardown's destroy branch decided on the stale ListDomains snapshot with
no re-read and no CAS, so a sibling app joining a domain between the list
and the decision would have its identity, DKIM tokens, and hand-published
DNS destroyed with it (I4) — the worst irreversibility in the task. The
re-read now happens once, before the branch, and the decision is made on
that fresh copy.

Added coverage for two holes the review found: deleting the whole
standalone-deletion loop in Teardown went uncaught (I3), and deleting the
whole DNS record-deletion loop did too because both teardown tests threw
away their writer without inspecting it (M3).
Paul Campbell pushed by paul@keymail.dev 1006beac404f31cc852afa0da293a5f392bd561a parent 28f32a0
2 files changed, +311 −40
  • internal/mail/walk.go +95 −37
  • internal/mail/walk_lifecycle_test.go +216 −3
diff --git a/internal/mail/walk.go b/internal/mail/walk.go
index 41a194573..28c04054f 100644
--- a/internal/mail/walk.go
+++ b/internal/mail/walk.go
@@ -387,6 +387,17 @@ func Enable(ctx context.Context, s release.Store, p Provisioner, sealPub *ecdh.P
}
}
+ // Active is carried forward, not reset. A re-walk that repairs missing
+ // delivery for a paused app is legitimate; silently un-pausing it is
+ // not — Active only ever moves from false to true through the pause
+ // verb itself (Task 13), never as a side effect of this walk running.
+ // A first-time enable (no prior Provisioned block) starts active.
+ active, prev := true, ""
+ if rec.Provisioned != nil {
+ prev = rec.Provisioned.PrevKeyID
+ active = rec.Provisioned.Active
+ }
+
user := UserName(acct, app)
arn, err := p.EnsureUser(ctx, user, UserPath, arns, names)
if err != nil {
@@ -398,8 +409,8 @@ func Enable(ctx context.Context, s release.Store, p Provisioner, sealPub *ecdh.P
// record names either; only list-and-delete finds them. A pending-finish
// key from an unfinished rotation is kept for its explicit finish.
var keep []string
- if rec.Provisioned != nil && rec.Provisioned.PrevKeyID != "" {
- keep = append(keep, rec.Provisioned.PrevKeyID)
+ if prev != "" {
+ keep = append(keep, prev)
}
if err := p.ReapKeys(ctx, user, keep); err != nil {
return nil, fmt.Errorf("mail: reap keys for %s: %w", user, err)
@@ -408,22 +419,18 @@ func Enable(ctx context.Context, s release.Store, p Provisioner, sealPub *ecdh.P
if err != nil {
return nil, fmt.Errorf("mail: mint key for %s: %w", user, err)
}
+ // I1: same defect as C1, reached through the repair path — a re-walk
+ // that repairs missing delivery for a PAUSED app must not silently
+ // lift the pause at IAM just because it reaps and re-mints.
+ if err := enforceKeyActive(ctx, p, user, keyID, active); err != nil {
+ return nil, err
+ }
if err := writeSMTPEnv(ctx, s, sealPub, acct, app, rec.Declared.EnvPrefix,
d.Region, keyID, secret, rec.Declared.From); err != nil {
return nil, err
}
- // Active is carried forward, not reset. A re-walk that repairs missing
- // delivery for a paused app is legitimate; silently un-pausing it is
- // not — Active only ever moves from false to true through the pause
- // verb itself (Task 13), never as a side effect of this walk running.
- // A first-time enable (no prior Provisioned block) starts active.
- active, prev := true, ""
- if rec.Provisioned != nil {
- prev = rec.Provisioned.PrevKeyID
- active = rec.Provisioned.Active
- }
rec.Provisioned = &Provisioned{
IAMUser: user, UserARN: arn, KeyID: keyID, PrevKeyID: prev, Active: active, At: now,
}
@@ -528,6 +535,24 @@ type StandaloneCreds struct {
Host, Port, Username, Password, From string
}
+// enforceKeyActive sets a freshly minted key's IAM active state to match the
+// record's OWN Active flag. It is the single enforcement point every
+// mint-and-deliver path (Enable, Rotate) must call.
+//
+// MintKey always returns an ACTIVE key. Skipping this after a mint silently
+// lifts a standing pause at IAM even though the record keeps saying paused —
+// the console keeps showing paused, the audit log keeps showing the pause,
+// and the app is sending again. The call is UNCONDITIONAL, not gated behind
+// `if !active`: unconditional self-corrects in both directions (an active
+// record's freshly minted key is confirmed active, not just left alone) and
+// cannot drift out of sync with the record.
+func enforceKeyActive(ctx context.Context, p Provisioner, user, keyID string, active bool) error {
+ if err := p.SetKeyActive(ctx, user, keyID, active); err != nil {
+ return fmt.Errorf("mail: set key active for %s/%s: %w", user, keyID, err)
+ }
+ return nil
+}
+
// Rotate mints a fresh key, delivers it, and leaves the OLD key ACTIVE.
//
// The overlap is the whole point. Adoption swaps a symlink; it does not
@@ -569,6 +594,12 @@ func Rotate(ctx context.Context, s release.Store, p Provisioner, sealPub *ecdh.P
if err != nil {
return nil, fmt.Errorf("mail: mint key for %s: %w", user, err)
}
+ // C1: a rotate against a PAUSED app must not silently lift the pause at
+ // IAM. MintKey returns an active key regardless; this brings it back in
+ // line with what the record already says.
+ if err := enforceKeyActive(ctx, p, user, keyID, rec.Provisioned.Active); err != nil {
+ return nil, err
+ }
if err := writeSMTPEnv(ctx, s, sealPub, acct, app, rec.Declared.EnvPrefix,
d.Region, keyID, secret, rec.Declared.From); err != nil {
return nil, err
@@ -606,15 +637,20 @@ func RotateFinish(ctx context.Context, s release.Store, p Provisioner, acct, app
return rec, nil
}
-// SetPaused stops or resumes sending.
+// SetPaused stops or resumes sending — every principal this app owns, not
+// just the delivered one.
//
// It toggles the KEY, never the policy. A policy rewrite would have to be
// undone correctly to resume, and a half-failed rewrite leaves a grant that
// is neither the old one nor the new one. Deactivating a key is atomic at
// IAM and trivially reversible.
//
-// BOTH keys are toggled: an app mid-rotation still holds the previous
-// credential, and pausing only the current one would leave it sending.
+// BOTH of the delivered user's keys are toggled: an app mid-rotation still
+// holds the previous credential, and pausing only the current one would
+// leave it sending. Every standalone credential (I2) is toggled too — each
+// is its OWN IAM user, so there is no two-key ceiling interaction, and
+// skipping them would leave a tenant paused for abuse still sending from the
+// same verified domain through, say, a laptop credential.
func SetPaused(ctx context.Context, s release.Store, p Provisioner,
acct, app string, paused bool, now time.Time) (*Record, error) {
@@ -634,6 +670,11 @@ func SetPaused(ctx context.Context, s release.Store, p Provisioner,
return nil, err
}
}
+ for _, sa := range rec.Standalone {
+ if err := p.SetKeyActive(ctx, sa.IAMUser, sa.KeyID, !paused); err != nil {
+ return nil, err
+ }
+ }
rec.Provisioned.Active = !paused
rec.Provisioned.At = now
if _, err := Put(ctx, s, acct, app, rec, etag); err != nil {
@@ -749,10 +790,19 @@ func RevokeStandalone(ctx context.Context, s release.Store, p Provisioner, acct,
// referencing it goes. Getting that backwards revokes a sibling app's
// verified domain — silently, and with the customer's own DNS still
// pointing at an identity that no longer exists.
+//
+// NOT SELF-HEALING across a crash (I5): a crash between the DeleteUser calls
+// below and the domain loop leaves a zombie — the IAM user is gone, the
+// record still says Provisioned, and EnvDelivered is satisfied (there is
+// nothing left to compare against), so Enable will short-circuit rather than
+// recreate the user, and Rotate will fail at MintKey because there is no
+// user to mint against. Nothing re-runs Teardown automatically on a
+// half-failure; the caller must retry it to completion. A reconciler that
+// notices and repairs a half-torn-down app is out of scope here.
func Teardown(ctx context.Context, s release.Store, p Provisioner, w dnsrec.Writer,
d Deployment, acct, app string) error {
- rec, etag, err := Get(ctx, s, acct, app)
+ rec, _, err := Get(ctx, s, acct, app)
if err != nil {
return err
}
@@ -774,22 +824,31 @@ func Teardown(ctx context.Context, s release.Store, p Provisioner, w dnsrec.Writ
return err
}
for _, dom := range domains {
- if !dom.RemoveApp(app) {
- continue
+ // The re-read happens ONCE, before any branch, and every decision
+ // below is made on the fresh copy (cur), never on the stale one
+ // (dom) ListDomains returned. Two things depend on that:
+ //
+ // - F6: an empty etag is CREATE-ONLY, and every one of these
+ // records already exists, so a CAS position has to come from a
+ // fresh read.
+ // - I4: without a fresh read, the DESTROY branch below had no CAS
+ // and no re-check at all — s.Delete was unconditional. A sibling
+ // app joining this domain between the ListDomains call and this
+ // decision would have its identity, DKIM tokens and (for a
+ // custom domain) hand-published DNS destroyed out from under it.
+ // Deciding on cur.Apps closes that window: a join that lands
+ // before this read is seen and the domain survives instead.
+ cur, curEtag, err := GetDomain(ctx, s, acct, dom.Domain)
+ if err != nil {
+ return err
}
- if len(dom.Apps) > 0 {
- // ListDomains does not carry etags (nothing else needs them), so
- // the record is re-read here to get a CAS position that is not
- // "" — an empty etag is CREATE-ONLY, and every one of these
- // records already exists (controller ruling F6).
- cur, curEtag, err := GetDomain(ctx, s, acct, dom.Domain)
- if err != nil {
- return err
- }
- if cur == nil {
- continue // gone since the list; nothing to write
- }
- cur.RemoveApp(app)
+ if cur == nil {
+ continue // gone since the list; nothing to do
+ }
+ if !cur.RemoveApp(app) {
+ continue // this app was never a referrer, or already removed
+ }
+ if len(cur.Apps) > 0 {
if _, err := PutDomain(ctx, s, acct, cur, curEtag); err != nil {
return err
}
@@ -798,25 +857,24 @@ func Teardown(ctx context.Context, s release.Store, p Provisioner, w dnsrec.Writ
// Last referrer: remove the identity and, for a platform domain,
// the records CARLOS published. A custom domain's records are the
// customer's to remove.
- for region, st := range dom.Regions {
- if dom.Kind == KindPlatform {
- for _, r := range RecordSet(dom.Domain, region, st.DKIMTokens, d.DMARCRua) {
+ for region, st := range cur.Regions {
+ if cur.Kind == KindPlatform {
+ for _, r := range RecordSet(cur.Domain, region, st.DKIMTokens, d.DMARCRua) {
if err := w.DeleteRecord(ctx, r); err != nil {
return err
}
}
}
- if err := p.DeleteIdentity(ctx, region, dom.Domain); err != nil {
+ if err := p.DeleteIdentity(ctx, region, cur.Domain); err != nil {
return err
}
}
- if err := s.Delete(ctx, DomainKey(acct, dom.Domain)); err != nil {
+ if err := s.Delete(ctx, DomainKey(acct, cur.Domain)); err != nil {
return err
}
}
if rec != nil {
- _ = etag
if err := s.Delete(ctx, Key(acct, app)); err != nil {
return err
}
diff --git a/internal/mail/walk_lifecycle_test.go b/internal/mail/walk_lifecycle_test.go
index d99ec9aab..53da9b1ac 100644
--- a/internal/mail/walk_lifecycle_test.go
+++ b/internal/mail/walk_lifecycle_test.go
@@ -1,9 +1,12 @@
package mail
import (
+ "context"
"testing"
+ "github.com/carlosframework/platform/internal/appconfig"
"github.com/carlosframework/platform/internal/dnsrec"
+ "github.com/carlosframework/platform/internal/release"
)
// Rotation leaves the OLD key ACTIVE. Adoption swaps a symlink; it does not
@@ -36,6 +39,34 @@ func TestRotateKeepsTheOldKeyLive(t *testing.T) {
}
}
+// C1: Rotate must not silently lift a standing pause at IAM. MintKey always
+// returns an ACTIVE key regardless of the record's own Active flag; without
+// the enforcement point, a paused app's record keeps saying paused (and the
+// console and audit log keep agreeing) while the freshly minted key is
+// live — an ordinary member `carlos email rotate` would defeat an abuse
+// pause.
+func TestRotateDoesNotLiftAPauseAtIAM(t *testing.T) {
+ ctx, s, p, pub, now := enableFixture(t)
+ if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now); err != nil {
+ t.Fatalf("Enable: %v", err)
+ }
+ if _, err := SetPaused(ctx, s, p, "bag", "seapointish", true, now); err != nil {
+ t.Fatalf("SetPaused(true): %v", err)
+ }
+
+ rec, err := Rotate(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now)
+ if err != nil {
+ t.Fatalf("Rotate: %v", err)
+ }
+ if rec.Provisioned.Active {
+ t.Fatal("rotate flipped the record back to active")
+ }
+ user := UserName("bag", "seapointish")
+ if p.Users[user].Keys[rec.Provisioned.KeyID].Active {
+ t.Fatal("rotate minted a LIVE key for a paused app — the pause was silently lifted at IAM")
+ }
+}
+
// Finish is the only walk that destroys a credential an app might still be
// using, so it is explicit and separate.
func TestRotateFinishClosesTheWindow(t *testing.T) {
@@ -56,6 +87,38 @@ func TestRotateFinishClosesTheWindow(t *testing.T) {
}
}
+// I1: Enable's repair re-walk has the identical defect as Rotate. Task 12
+// made Enable carry Active forward in the RECORD; that alone is half the
+// feature — a repair re-walk still reaps the paused key and mints a fresh
+// one, and MintKey always hands back a live key.
+func TestEnableRepairDoesNotLiftAPauseAtIAM(t *testing.T) {
+ ctx, s, p, pub, now := enableFixture(t)
+ if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now); err != nil {
+ t.Fatalf("first Enable: %v", err)
+ }
+ if _, err := SetPaused(ctx, s, p, "bag", "seapointish", true, now); err != nil {
+ t.Fatalf("SetPaused(true): %v", err)
+ }
+ // Clear a delivered var so the idempotence short-circuit cannot fire —
+ // this forces Enable to actually re-walk (reap + re-mint) rather than
+ // return early.
+ if _, err := appconfig.Unset(ctx, s, "bag", "seapointish", []string{"SEAPOINTISH_SMTP_HOST"}); err != nil {
+ t.Fatalf("Unset: %v", err)
+ }
+
+ rec, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now)
+ if err != nil {
+ t.Fatalf("repair Enable: %v", err)
+ }
+ if rec.Provisioned.Active {
+ t.Fatal("repair re-walk flipped the record back to active")
+ }
+ user := UserName("bag", "seapointish")
+ if p.Users[user].Keys[rec.Provisioned.KeyID].Active {
+ t.Fatal("repair re-walk minted a LIVE key for a paused app — the pause was silently lifted at IAM")
+ }
+}
+
// The pause is SetKeyActive, and it must be reversible with no policy
// rewrite — a policy rewrite would have to be undone correctly to resume
// and could half-fail into a broken grant.
@@ -119,6 +182,48 @@ func TestPauseTogglesBothKeysDuringARotationOverlap(t *testing.T) {
}
}
+// I2: SetPaused must reach standalone principals too. Each is its own IAM
+// user, so there is no two-key ceiling interaction to worry about; skipping
+// them leaves a tenant paused for abuse still sending through, say, a
+// laptop credential on the very domain the pause was meant to protect.
+func TestPauseReachesStandaloneCredentials(t *testing.T) {
+ ctx, s, p, pub, now := enableFixture(t)
+ Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now)
+ if _, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "seapointish",
+ "laptop", "paul@rslw.com", now); err != nil {
+ t.Fatalf("CreateStandalone: %v", err)
+ }
+ standaloneUser := StandaloneUserName("bag", "seapointish", "laptop")
+
+ rec, _, err := Get(ctx, s, "bag", "seapointish")
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ var keyID string
+ for _, sa := range rec.Standalone {
+ if sa.Name == "laptop" {
+ keyID = sa.KeyID
+ }
+ }
+ if keyID == "" {
+ t.Fatal("fixture broken: standalone entry missing from record")
+ }
+
+ if _, err := SetPaused(ctx, s, p, "bag", "seapointish", true, now); err != nil {
+ t.Fatalf("SetPaused(true): %v", err)
+ }
+ if p.Users[standaloneUser].Keys[keyID].Active {
+ t.Fatal("pause did not reach the standalone credential — it can still send")
+ }
+
+ if _, err := SetPaused(ctx, s, p, "bag", "seapointish", false, now); err != nil {
+ t.Fatalf("SetPaused(false): %v", err)
+ }
+ if !p.Users[standaloneUser].Keys[keyID].Active {
+ t.Fatal("resume did not reach the standalone credential")
+ }
+}
+
// A standalone credential lives on its OWN IAM user, because the delivered
// user's two key slots are spent on the delivered key and the rotation
// overlap. Sharing would work until the first rotation and then break it.
@@ -179,7 +284,19 @@ func TestTeardownKeepsASharedDomainAlive(t *testing.T) {
t.Fatalf("sibling EnsureDomain: %v", err)
}
- if err := Teardown(ctx, s, p, memWriterForTest(), testDeployment(), "bag", "seapointish"); err != nil {
+ // M3: thread a real, inspectable writer through (rather than a fresh
+ // throwaway one) so a wrongly-triggered record deletion would actually
+ // be visible to this test.
+ w := dnsrec.NewMemWriter()
+ if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(),
+ "bag", "seapointish", "seapointish.bag.oncarlos.com", now); err != nil {
+ t.Fatalf("re-publish into the inspectable writer: %v", err)
+ }
+ if len(w.Records) == 0 {
+ t.Fatal("fixture broken: no records published to inspect")
+ }
+
+ if err := Teardown(ctx, s, p, w, testDeployment(), "bag", "seapointish"); err != nil {
t.Fatalf("Teardown: %v", err)
}
if _, ok := p.Identities[identityKey("eu-west-1", "seapointish.bag.oncarlos.com")]; !ok {
@@ -189,15 +306,105 @@ func TestTeardownKeepsASharedDomainAlive(t *testing.T) {
if len(dom.Apps) != 1 || dom.Apps[0] != "marketing" {
t.Fatalf("apps = %v, want [marketing]", dom.Apps)
}
+ if len(w.Records) == 0 {
+ t.Fatal("teardown deleted DNS records for a domain a sibling app still references")
+ }
+}
+
+// I4: the destroy branch's decision must be made on a FRESH read, not the
+// snapshot ListDomains took at the top of Teardown. A sibling that joins
+// this domain between that snapshot and the decision must not have its
+// identity, DKIM tokens, and (for a custom domain) hand-published DNS
+// destroyed out from under it.
+func TestTeardownDestroyDecisionSeesASiblingThatJoinsBetweenListAndDecision(t *testing.T) {
+ ctx, s, p, pub, now := enableFixture(t)
+ Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now)
+
+ domain := "seapointish.bag.oncarlos.com"
+ rs := &raceGetStore{
+ Store: s,
+ key: DomainKey("bag", domain),
+ trigger: 2, // 1st Get is ListDomains' own unmarshal pass; 2nd is Teardown's decision re-read.
+ inject: func() error {
+ cur, etag, err := GetDomain(ctx, s, "bag", domain)
+ if err != nil {
+ return err
+ }
+ cur.AddApp("marketing")
+ _, err = PutDomain(ctx, s, "bag", cur, etag)
+ return err
+ },
+ }
+
+ if err := Teardown(ctx, rs, p, memWriterForTest(), testDeployment(), "bag", "seapointish"); err != nil {
+ t.Fatalf("Teardown: %v", err)
+ }
+ if _, ok := p.Identities[identityKey("eu-west-1", domain)]; !ok {
+ t.Fatal("teardown destroyed an identity a sibling that joined mid-teardown still references")
+ }
+ dom, _, err := GetDomain(ctx, s, "bag", domain)
+ if err != nil {
+ t.Fatalf("GetDomain: %v", err)
+ }
+ if dom == nil {
+ t.Fatal("teardown deleted the domain record even though a sibling joined mid-teardown")
+ }
+ if len(dom.Apps) != 1 || dom.Apps[0] != "marketing" {
+ t.Fatalf("apps = %v, want [marketing]", dom.Apps)
+ }
}
-// Teardown removes the identity and the app's record once it is the LAST
+// raceGetStore wraps a release.Store and, on the Nth Get call for one
+// specific key, runs inject() immediately before that call returns. It
+// models a write landing between an earlier snapshot read (ListDomains' own
+// pass) and a later re-read (Teardown's decision), inside one synchronous
+// call, without real concurrency.
+type raceGetStore struct {
+ release.Store
+ key string
+ trigger int
+ calls int
+ inject func() error
+}
+
+func (r *raceGetStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ if key == r.key {
+ r.calls++
+ if r.calls == r.trigger && r.inject != nil {
+ if err := r.inject(); err != nil {
+ return nil, "", err
+ }
+ }
+ }
+ return r.Store.Get(ctx, key)
+}
+
+// Teardown removes the identity, every principal (delivered AND standalone —
+// I3), and every DNS record it published (M3), once it is the LAST
// referrer.
func TestTeardownRemovesTheIdentityWhenTheLastAppGoes(t *testing.T) {
ctx, s, p, pub, now := enableFixture(t)
Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now)
+ if _, err := CreateStandalone(ctx, s, p, testDeployment(), "bag", "seapointish",
+ "laptop", "paul@rslw.com", now); err != nil {
+ t.Fatalf("CreateStandalone: %v", err)
+ }
+ standaloneUser := StandaloneUserName("bag", "seapointish", "laptop")
+ if _, ok := p.Users[standaloneUser]; !ok {
+ t.Fatal("fixture broken: standalone user was not created")
+ }
- if err := Teardown(ctx, s, p, memWriterForTest(), testDeployment(), "bag", "seapointish"); err != nil {
+ // M3: thread a real, inspectable writer through.
+ w := dnsrec.NewMemWriter()
+ if _, _, err := EnsureDomain(ctx, s, p, w, testDeployment(),
+ "bag", "seapointish", "seapointish.bag.oncarlos.com", now); err != nil {
+ t.Fatalf("re-publish into the inspectable writer: %v", err)
+ }
+ if len(w.Records) == 0 {
+ t.Fatal("fixture broken: no records published to inspect")
+ }
+
+ if err := Teardown(ctx, s, p, w, testDeployment(), "bag", "seapointish"); err != nil {
t.Fatalf("Teardown: %v", err)
}
if _, ok := p.Identities[identityKey("eu-west-1", "seapointish.bag.oncarlos.com")]; ok {
@@ -206,6 +413,12 @@ func TestTeardownRemovesTheIdentityWhenTheLastAppGoes(t *testing.T) {
if _, ok := p.Users[UserName("bag", "seapointish")]; ok {
t.Fatal("teardown left the delivered IAM user behind")
}
+ if _, ok := p.Users[standaloneUser]; ok {
+ t.Fatal("teardown left the standalone IAM user behind")
+ }
+ if len(w.Records) != 0 {
+ t.Fatalf("teardown left DNS records behind: %v", w.Records)
+ }
rec, _, err := Get(ctx, s, "bag", "seapointish")
if err != nil {
t.Fatalf("Get: %v", err)