package release import ( "context" "crypto/ed25519" "errors" "os" "testing" ) // storeCall is one call a request made through hookStore. type storeCall struct { seq int op, key string ifMatch string // put only } // hookStore runs before(call) ahead of every call it forwards, and after() // once a get returns, so a test can make another actor's writes land at an // exact point inside a request. The other actor writes to inner directly: // the same handle, which is where G2 holds for a directory store (spec // §7.2). seq counts only the calls made through hookStore. type hookStore struct { inner Store before func(c storeCall) after func(c storeCall, body []byte, etag string, err error) seq int } func (h *hookStore) call(op, key, ifMatch string) storeCall { h.seq++ c := storeCall{seq: h.seq, op: op, key: key, ifMatch: ifMatch} if h.before != nil { h.before(c) } return c } func (h *hookStore) Get(ctx context.Context, key string) ([]byte, string, error) { c := h.call("get", key, "") body, etag, err := h.inner.Get(ctx, key) if h.after != nil { h.after(c, body, etag, err) } return body, etag, err } func (h *hookStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { h.call("put", key, ifMatch) return h.inner.Put(ctx, key, body, ifMatch) } func (h *hookStore) List(ctx context.Context, prefix string) ([]string, error) { h.call("list", prefix, "") return h.inner.List(ctx, prefix) } func (h *hookStore) ListDirs(ctx context.Context, prefix string) ([]string, error) { h.call("listdirs", prefix, "") return h.inner.ListDirs(ctx, prefix) } func (h *hookStore) Delete(ctx context.Context, key string) error { h.call("delete", key, "") return h.inner.Delete(ctx, key) } // onNth runs fn once, just before the nth (1-based) call matching op and key. func onNth(op, key string, n int, fn func()) func(storeCall) { seen := 0 return func(c storeCall) { if c.op == op && c.key == key { seen++ if seen == n { fn() } } } } // noPipelineOnly is R2's rule for a non-canary channel: refuse if the app // has a pipeline. func noPipelineOnly(p *Pipeline) error { if p != nil { return errors.New("pipelined") } return nil } var edgeKey = ChannelKey("bab", "myapp", "edge") func setupEdge(t *testing.T) (context.Context, Store, ed25519.PrivateKey, ed25519.PublicKey) { t.Helper() ctx := context.Background() inner := NewDirStore(t.TempDir()) priv, pub := testKeys(t) for _, v := range []string{"v1", "v2", "v3", "v4"} { shipRelease(t, inner, "bab", "myapp", v, "linux-arm64", []byte("bin "+v)) } return ctx, inner, priv, pub } func installPipeline(t *testing.T, ctx context.Context, s Store, priv ed25519.PrivateKey, names ...string) { t.Helper() var chans []PipelineChannel for _, n := range names { chans = append(chans, PipelineChannel{Name: n}) } if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", chans, "owner"); err != nil { t.Fatalf("SetPipeline(%v): %v", names, err) } } func ownerPromote(t *testing.T, ctx context.Context, s Store, priv ed25519.PrivateKey, v string) { t.Helper() if _, err := PromoteOnLadder(ctx, s, priv, "bab", "myapp", "edge", v, PromoteOptions{Hotfix: true}); err != nil { t.Fatalf("owner promote %s: %v", v, err) } } func edgeVersion(t *testing.T, ctx context.Context, s Store, pub ed25519.PublicKey) string { t.Helper() p, err := Channel(ctx, s, pub, "bab", "myapp", "edge") if errors.Is(err, os.ErrNotExist) { return "" } if err != nil { t.Fatal(err) } return p.Version } func wantAuthorizationError(t *testing.T, err error) { t.Helper() var ae *AuthorizationError if !errors.As(err, &ae) { t.Fatalf("err = %v; want *AuthorizationError", err) } } func TestAuthorizeSeesAPipelineInstalledBeforeTheRequest(t *testing.T) { ctx, inner, priv, pub := setupEdge(t) installPipeline(t, ctx, inner, priv, "edge", "stable") _, err := RootSigner(priv).PromoteOnLadder(ctx, inner, "bab", "myapp", "edge", "v1", PromoteOptions{Authorize: noPipelineOnly}) wantAuthorizationError(t, err) if got := edgeVersion(t, ctx, inner, pub); got != "" { t.Fatalf("edge = %q after a refusal", got) } } // The refusal is the hook's, not the ladder's order error, even when the // pipeline would make the ladder refuse too (spec §7.2 point 1, codex R3-3). func TestAuthorizeRefusesBeforeTheLadderDoes(t *testing.T) { ctx, inner, priv, _ := setupEdge(t) h := &hookStore{inner: inner} h.before = onNth("get", PipelineKey("bab", "myapp"), 1, func() { installPipeline(t, ctx, inner, priv, "preview") }) _, err := RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v1", PromoteOptions{Authorize: noPipelineOnly}) wantAuthorizationError(t, err) } // The ladder's read saw no pipeline; one installed before the authoritative // read (after the pointer read) is still seen. func TestAuthorizeSeesAPipelineInstalledBeforeTheAuthoritativeRead(t *testing.T) { ctx, inner, priv, pub := setupEdge(t) h := &hookStore{inner: inner} h.before = onNth("get", PipelineKey("bab", "myapp"), 2, func() { installPipeline(t, ctx, inner, priv, "edge", "stable") }) _, err := RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v1", PromoteOptions{Authorize: noPipelineOnly}) wantAuthorizationError(t, err) if got := edgeVersion(t, ctx, inner, pub); got != "" { t.Fatalf("edge = %q after a refusal", got) } // Astra I1 #1: the authoritative callback refused, so nothing may vouch // for v1 either. if _, err := ReadPromotionRecord(ctx, inner, pub, "bab", "myapp", "v1"); !errors.Is(err, ErrNoPromotionRecord) { t.Fatalf("a refused promotion left a record: %v", err) } } // Astra's round 2 counterexample: an owner promotes under a new pipeline // after the deploy request read the pointer. The deploy write must lose. func TestOwnerPromoteAfterInstallDefeatsTheDeployWrite(t *testing.T) { ctx, inner, priv, pub := setupEdge(t) ownerPromote(t, ctx, inner, priv, "v1") h := &hookStore{inner: inner} h.before = onNth("put", edgeKey, 1, func() { installPipeline(t, ctx, inner, priv, "edge", "stable") ownerPromote(t, ctx, inner, priv, "v3") }) _, err := RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v2", PromoteOptions{Authorize: noPipelineOnly}) if !errors.Is(err, ErrCASConflict) { t.Fatalf("err = %v; want ErrCASConflict", err) } if got := edgeVersion(t, ctx, inner, pub); got != "v3" { t.Fatalf("edge = %q; want the owner's v3", got) } } // R5, accepted: a pipeline installed after the authorization read, and the // pointer written and restored to identical bytes, before the write. G2 // holds (the bytes are what was read), so the write lands. func TestRestoredBytesLandAsTheAcceptedInFlightCase(t *testing.T) { ctx, inner, priv, pub := setupEdge(t) ownerPromote(t, ctx, inner, priv, "v1") ownerPromote(t, ctx, inner, priv, "v2") h := &hookStore{inner: inner} h.before = onNth("put", edgeKey, 1, func() { orig, _, err := inner.Get(ctx, edgeKey) if err != nil { t.Fatal(err) } installPipeline(t, ctx, inner, priv, "edge", "stable") ownerPromote(t, ctx, inner, priv, "v4") _, cur, err := inner.Get(ctx, edgeKey) if err != nil { t.Fatal(err) } if _, err := inner.Put(ctx, edgeKey, orig, cur); err != nil { t.Fatal(err) } }) if _, err := RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v3", PromoteOptions{Authorize: noPipelineOnly}); err != nil { t.Fatalf("err = %v; R5 accepts this write (spec §16)", err) } if got := edgeVersion(t, ctx, inner, pub); got != "v3" { t.Fatalf("edge = %q; want v3", got) } } // R5, accepted: a first promote onto an empty channel, with a pipeline // installed after the authorization read. func TestFirstPromoteOntoEmptyChannelLandsAsTheAcceptedInFlightCase(t *testing.T) { ctx, inner, priv, pub := setupEdge(t) h := &hookStore{inner: inner} h.before = onNth("put", edgeKey, 1, func() { installPipeline(t, ctx, inner, priv, "edge", "stable") }) if _, err := RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v1", PromoteOptions{Authorize: noPipelineOnly}); err != nil { t.Fatalf("err = %v; R5 accepts this write (spec §16)", err) } if got := edgeVersion(t, ctx, inner, pub); got != "v1" { t.Fatalf("edge = %q; want v1", got) } } func TestRepeatPromoteRunsTheHook(t *testing.T) { ctx, inner, priv, _ := setupEdge(t) ownerPromote(t, ctx, inner, priv, "v1") installPipeline(t, ctx, inner, priv, "edge", "stable") res, err := RootSigner(priv).PromoteOnLadder(ctx, inner, "bab", "myapp", "edge", "v1", PromoteOptions{Authorize: noPipelineOnly}) wantAuthorizationError(t, err) if res.NoOp { t.Fatal("a refused repeat reported NoOp") } } func TestRepeatPromoteWithNoPipelineIsANoOp(t *testing.T) { ctx, inner, priv, _ := setupEdge(t) ownerPromote(t, ctx, inner, priv, "v1") var seen []bool hook := func(p *Pipeline) error { seen = append(seen, p != nil); return noPipelineOnly(p) } res, err := RootSigner(priv).PromoteOnLadder(ctx, inner, "bab", "myapp", "edge", "v1", PromoteOptions{Authorize: hook}) if err != nil || !res.NoOp { t.Fatalf("= %+v, %v; want a no-op", res, err) } if len(seen) != 1 || seen[0] { t.Fatalf("hook calls = %v; want one, with no pipeline", seen) } } // A caller with no hook makes no extra pipeline reads (spec §7.2). func TestNoHookNoExtraReads(t *testing.T) { ctx, inner, priv, _ := setupEdge(t) count := func(run func(Store) error) int { n := 0 h := &hookStore{inner: inner, before: func(c storeCall) { if c.op == "get" && c.key == PipelineKey("bab", "myapp") { n++ } }} if err := run(h); err != nil { t.Fatal(err) } return n } fresh := count(func(s Store) error { _, err := RootSigner(priv).PromoteOnLadder(ctx, s, "bab", "myapp", "edge", "v1", PromoteOptions{}) return err }) repeat := count(func(s Store) error { _, err := RootSigner(priv).PromoteOnLadder(ctx, s, "bab", "myapp", "edge", "v1", PromoteOptions{}) return err }) ownerPromote(t, ctx, inner, priv, "v2") rollback := count(func(s Store) error { return RootSigner(priv).Rollback(ctx, s, "bab", "myapp", "edge") }) if fresh != 1 || repeat != 0 || rollback != 0 { t.Fatalf("pipeline reads: fresh %d, repeat %d, rollback %d; want 1, 0, 0", fresh, repeat, rollback) } } func TestRollbackAuthorized(t *testing.T) { ctx, inner, priv, pub := setupEdge(t) ownerPromote(t, ctx, inner, priv, "v1") ownerPromote(t, ctx, inner, priv, "v2") installPipeline(t, ctx, inner, priv, "edge", "stable") err := RootSigner(priv).RollbackAuthorized(ctx, inner, "bab", "myapp", "edge", noPipelineOnly) wantAuthorizationError(t, err) if got := edgeVersion(t, ctx, inner, pub); got != "v2" { t.Fatalf("edge = %q after a refused rollback", got) } if err := inner.Delete(ctx, PipelineKey("bab", "myapp")); err != nil { t.Fatal(err) } h := &hookStore{inner: inner} h.before = onNth("put", edgeKey, 1, func() { ownerPromote(t, ctx, inner, priv, "v3") }) err = RootSigner(priv).RollbackAuthorized(ctx, h, "bab", "myapp", "edge", noPipelineOnly) if !errors.Is(err, ErrCASConflict) { t.Fatalf("err = %v; want ErrCASConflict", err) } if got := edgeVersion(t, ctx, inner, pub); got != "v3" { t.Fatalf("edge = %q; want the owner's v3", got) } }