carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "path"
8 "strings"
9 "sync/atomic"
10 "testing"
11 "time"
12)
13
14func manifestDigestOf(t *testing.T, s Store, acct, app, ver string) string {
15 t.Helper()
16 raw, _, err := s.Get(context.Background(), ManifestKey(acct, app, ver))
17 if err != nil {
18 t.Fatal(err)
19 }
20 return ManifestDigest(raw)
21}
22
23func readPtr(t *testing.T, s Store, pub []byte, ch string) *ChannelPointer {
24 t.Helper()
25 p, err := Channel(context.Background(), s, pub, "bab", "km", ch)
26 if err != nil {
27 t.Fatalf("Channel: %v", err)
28 }
29 return p
30}
31
32func TestWritersEmitNothingWithoutTheMarker(t *testing.T) {
33 s := NewDirStore(t.TempDir())
34 ctx := context.Background()
35 priv, pub := testKeys(t)
36 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
37 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
38 for _, v := range []string{"v1", "v2"} {
39 if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
40 t.Fatal(err)
41 }
42 }
43 if p := readPtr(t, s, pub, "stable"); p.Digest != "" || p.HistoryDigests != nil {
44 t.Fatalf("no marker, yet digests: %+v", p)
45 }
46 raw, _, _ := s.Get(ctx, ChannelKey("bab", "km", "stable"))
47 if strings.Contains(string(raw), "digest") {
48 t.Fatal("the wrapper mentions digests with the marker off")
49 }
50}
51
52func TestPromoteSignsTheShippedDigestAndInheritsNothing(t *testing.T) {
53 s := NewDirStore(t.TempDir())
54 ctx := context.Background()
55 priv, pub := testKeys(t)
56 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
57 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
58 shipRelease(t, s, "bab", "km", "v3", adoptPlatform, []byte("three"))
59 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil { // a legacy pointer
60 t.Fatal(err)
61 }
62 markerOn(t, s, priv, "bab")
63 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil {
64 t.Fatal(err)
65 }
66 p := readPtr(t, s, pub, "stable")
67 if p.Digest != manifestDigestOf(t, s, "bab", "km", "v2") {
68 t.Fatalf("head digest %q is not v2's shipped manifest", p.Digest)
69 }
70 // Spec §8: promoting over a legacy pointer leaves inherited entries
71 // without digests, never computed.
72 if len(p.HistoryDigests) != 1 || p.HistoryDigests[0] != "" {
73 t.Fatalf("v1 was inherited from a legacy pointer, yet history_digests = %q", p.HistoryDigests)
74 }
75 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v3"); err != nil {
76 t.Fatal(err)
77 }
78 p = readPtr(t, s, pub, "stable")
79 want := []string{manifestDigestOf(t, s, "bab", "km", "v2"), ""}
80 if p.History[0] != "v2" || p.HistoryDigests[0] != want[0] || p.HistoryDigests[1] != "" {
81 t.Fatalf("history %q / %q, want v2's signed digest copied and v1 still empty", p.History, p.HistoryDigests)
82 }
83}
84
85func TestHistoryCapTrimsDigestsWithVersions(t *testing.T) {
86 s := NewDirStore(t.TempDir())
87 ctx := context.Background()
88 priv, pub := testKeys(t)
89 markerOn(t, s, priv, "bab")
90 for i := 0; i < historyMax+3; i++ {
91 v := "v" + string(rune('a'+i))
92 shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
93 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
94 t.Fatal(err)
95 }
96 }
97 p := readPtr(t, s, pub, "canary/x")
98 if len(p.History) != historyMax || len(p.HistoryDigests) != historyMax {
99 t.Fatalf("history %d, digests %d; want %d each", len(p.History), len(p.HistoryDigests), historyMax)
100 }
101 for i, v := range p.History {
102 if p.HistoryDigests[i] != manifestDigestOf(t, s, "bab", "km", v) {
103 t.Fatalf("history[%d] %s carries another build's digest", i, v)
104 }
105 }
106}
107
108// Review Focus 2.
109func TestPromoteRefusesAnUnverifiableMarker(t *testing.T) {
110 s := NewDirStore(t.TempDir())
111 ctx := context.Background()
112 priv, pub := testKeys(t)
113 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
114 other, _ := testKeys(t)
115 markerOn(t, s, other, "bab")
116 err := Promote(ctx, s, priv, "bab", "km", "stable", "v1")
117 if err == nil || !strings.Contains(err.Error(), "marker") {
118 t.Fatalf("Promote with a foreign marker = %v; want a refusal naming the marker", err)
119 }
120 if _, err := Channel(ctx, s, pub, "bab", "km", "stable"); err == nil {
121 t.Fatal("the refused promotion still wrote a pointer")
122 }
123}
124
125func TestRetryCopiesTheHeadDigest(t *testing.T) {
126 s := NewDirStore(t.TempDir())
127 ctx := context.Background()
128 priv, pub := testKeys(t)
129 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
130 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil { // legacy head
131 t.Fatal(err)
132 }
133 markerOn(t, s, priv, "bab")
134 time.Sleep(1100 * time.Millisecond)
135 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "v1", PromoteOptions{Retry: true, Hotfix: true}); err != nil {
136 t.Fatal(err)
137 }
138 if p := readPtr(t, s, pub, "stable"); p.Digest != "" {
139 t.Fatalf("a retry computed a digest for the head it renewed: %q", p.Digest)
140 }
141}
142
143func TestScopedPromoteCarriesDigests(t *testing.T) {
144 s := NewDirStore(t.TempDir())
145 ctx := context.Background()
146 root, pub := testKeys(t)
147 markerOn(t, s, root, "bab")
148 seed, grant, err := MintKeyGrant(root, "ci", []string{"canary"}, time.Hour)
149 if err != nil {
150 t.Fatal(err)
151 }
152 key, _ := ParseSigningKey(seed, "test")
153 sg, err := ScopedSigner(key, grant, pub)
154 if err != nil {
155 t.Fatal(err)
156 }
157 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
158 if err := sg.Promote(ctx, s, "bab", "km", "canary/x", "v1"); err != nil {
159 t.Fatal(err)
160 }
161 if p := readPtr(t, s, pub, "canary/x"); p.Digest != manifestDigestOf(t, s, "bab", "km", "v1") {
162 t.Fatalf("a scoped promotion signed digest %q", p.Digest)
163 }
164}
165
166func TestRollbackSwapsDigestsWithVersions(t *testing.T) {
167 s := NewDirStore(t.TempDir())
168 ctx := context.Background()
169 priv, pub := testKeys(t)
170 markerOn(t, s, priv, "bab")
171 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
172 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
173 for _, v := range []string{"v1", "v2"} {
174 if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
175 t.Fatal(err)
176 }
177 }
178 if err := Rollback(ctx, s, priv, "bab", "km", "stable"); err != nil {
179 t.Fatal(err)
180 }
181 p := readPtr(t, s, pub, "stable")
182 if p.Version != "v1" || p.Digest != manifestDigestOf(t, s, "bab", "km", "v1") ||
183 p.History[0] != "v2" || p.HistoryDigests[0] != manifestDigestOf(t, s, "bab", "km", "v2") {
184 t.Fatalf("after rollback: %+v", p)
185 }
186}
187
188func TestRollbackOntoAnEntryWithoutADigestIsRefused(t *testing.T) {
189 s := NewDirStore(t.TempDir())
190 ctx := context.Background()
191 priv, pub := testKeys(t)
192 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
193 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
194 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
195 t.Fatal(err)
196 }
197 markerOn(t, s, priv, "bab")
198 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil {
199 t.Fatal(err)
200 }
201 err := Rollback(ctx, s, priv, "bab", "km", "stable")
202 var rn *ResignNeededError
203 if !errors.As(err, &rn) || rn.Version != "v1" || err.Error() != fmt.Sprintf(TextRollbackResign, "km", "stable", "v1", "km", "stable") {
204 t.Fatalf("rollback onto a digest-less v1 = %v; want ResignNeededError naming v1", err)
205 }
206 if p := readPtr(t, s, pub, "stable"); p.Version != "v2" {
207 t.Fatalf("the refused rollback moved the channel to %s", p.Version)
208 }
209}
210
211// manifestCountingStore counts manifest reads, so a test can prove a writer never
212// fetched one.
213type manifestCountingStore struct {
214 Store
215 manifests atomic.Int32
216}
217
218func (c *manifestCountingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
219 if strings.HasSuffix(key, "/manifest.json") {
220 c.manifests.Add(1)
221 }
222 return c.Store.Get(ctx, key)
223}
224
225func TestFastTrackKeepsTheSignedDigestsVerbatim(t *testing.T) {
226 base := NewDirStore(t.TempDir())
227 ctx := context.Background()
228 priv, pub := testKeys(t)
229 shipRelease(t, base, "bab", "km", "v1", adoptPlatform, []byte("one"))
230 shipRelease(t, base, "bab", "km", "v2", adoptPlatform, []byte("two"))
231 if err := Promote(ctx, base, priv, "bab", "km", "stable", "v1"); err != nil {
232 t.Fatal(err)
233 }
234 markerOn(t, base, priv, "bab")
235 if err := Promote(ctx, base, priv, "bab", "km", "stable", "v2"); err != nil {
236 t.Fatal(err)
237 }
238 before := readPtr(t, base, pub, "stable")
239 s := &manifestCountingStore{Store: base}
240 if _, err := FastTrack(ctx, s, priv, "bab", "km", "stable", "ops@example.com"); err != nil {
241 t.Fatal(err)
242 }
243 if n := s.manifests.Load(); n != 0 {
244 t.Fatalf("FastTrack read %d manifests; it must copy digests, never fetch them", n)
245 }
246 after := readPtr(t, base, pub, "stable")
247 if !after.FastTrack || after.Digest != before.Digest || len(after.HistoryDigests) != 1 || after.HistoryDigests[0] != before.HistoryDigests[0] {
248 t.Fatalf("FastTrack changed the digests: before %+v, after %+v", before, after)
249 }
250}
251
252func TestRebindKeepsDigests(t *testing.T) {
253 s := NewDirStore(t.TempDir())
254 ctx := context.Background()
255 priv, pub := testKeys(t)
256 markerOn(t, s, priv, "bab")
257 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
258 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
259 t.Fatal(err)
260 }
261 raw, _, _ := s.Get(ctx, ChannelKey("bab", "km", "stable"))
262 out, err := RebindPointer(priv, raw, "bab", "cdf", "km", "stable")
263 if err != nil {
264 t.Fatal(err)
265 }
266 p, err := verifyPointer(pub, out, "cdf", "km", "stable")
267 if err != nil || p.Digest != manifestDigestOf(t, s, "bab", "km", "v1") {
268 t.Fatalf("rebound pointer = %+v, %v; want the digest carried verbatim", p, err)
269 }
270}
271
272// reship deletes every object of a shipped version and ships the name again
273// with other bytes: the reused-name replay of #253.
274func reship(t *testing.T, s Store, acct, app, ver string, body []byte) {
275 t.Helper()
276 ctx := context.Background()
277 keys, err := s.List(ctx, path.Dir(ManifestKey(acct, app, ver))+"/")
278 if err != nil || len(keys) == 0 {
279 t.Fatalf("List %s: %v %v", ver, keys, err)
280 }
281 for _, k := range keys {
282 if err := s.Delete(ctx, k); err != nil {
283 t.Fatal(err)
284 }
285 }
286 shipRelease(t, s, acct, app, ver, adoptPlatform, body)
287}
288
289// Ruling P7: a promote onto a version in the predecessor's history is bound
290// to that entry's signed digest.
291func TestPromoteOfAHistoryEntryWithReshippedBytesIsRefused(t *testing.T) {
292 s := NewDirStore(t.TempDir())
293 ctx := context.Background()
294 priv, pub := testKeys(t)
295 markerOn(t, s, priv, "bab")
296 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
297 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
298 for _, v := range []string{"v1", "v2"} {
299 if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
300 t.Fatal(err)
301 }
302 }
303 before := readPtr(t, s, pub, "stable")
304 reship(t, s, "bab", "km", "v1", []byte("not what was promoted"))
305 err := Promote(ctx, s, priv, "bab", "km", "stable", "v1")
306 if !errors.Is(err, ErrManifestDigest) {
307 t.Fatalf("promote of a re-shipped history entry = %v; want ErrManifestDigest", err)
308 }
309 after := readPtr(t, s, pub, "stable")
310 if after.Version != before.Version || after.Digest != before.Digest || !after.PromotedAt.Equal(before.PromotedAt) {
311 t.Fatalf("the refused promote changed the pointer: %+v -> %+v", before, after)
312 }
313}
314
315func TestPromoteOfADigestlessHistoryEntryIsRefused(t *testing.T) {
316 s := NewDirStore(t.TempDir())
317 ctx := context.Background()
318 priv, pub := testKeys(t)
319 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
320 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
321 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
322 t.Fatal(err)
323 }
324 markerOn(t, s, priv, "bab")
325 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil {
326 t.Fatal(err)
327 }
328 before := readPtr(t, s, pub, "stable")
329 err := Promote(ctx, s, priv, "bab", "km", "stable", "v1")
330 var rn *ResignNeededError
331 if !errors.As(err, &rn) || rn.Version != "v1" || rn.App != "km" || rn.Channel != "stable" {
332 t.Fatalf("promote of a digest-less history entry = %v; want ResignNeededError naming v1", err)
333 }
334 after := readPtr(t, s, pub, "stable")
335 if after.Version != before.Version || !after.PromotedAt.Equal(before.PromotedAt) {
336 t.Fatalf("the refused promote changed the pointer: %+v -> %+v", before, after)
337 }
338}
339
340func TestPromoteOfAnUnchangedHistoryEntrySignsItsDigest(t *testing.T) {
341 s := NewDirStore(t.TempDir())
342 ctx := context.Background()
343 priv, pub := testKeys(t)
344 markerOn(t, s, priv, "bab")
345 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
346 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
347 for _, v := range []string{"v1", "v2"} {
348 if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
349 t.Fatal(err)
350 }
351 }
352 signed := readPtr(t, s, pub, "stable").HistoryDigests[0]
353 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
354 t.Fatal(err)
355 }
356 p := readPtr(t, s, pub, "stable")
357 if p.Version != "v1" || p.Digest != signed || p.Digest != manifestDigestOf(t, s, "bab", "km", "v1") {
358 t.Fatalf("promote back to v1: %+v; want the entry's signed digest %s", p, signed)
359 }
360}
361
362// digestChannelThenMarkerOff is stable with v1 promoted before the marker
363// (no digest), v2 promoted under it (digests), and then the marker removed:
364// emission suspended on a channel that already carries digests.
365func digestChannelThenMarkerOff(t *testing.T) (Store, []byte, func() *ChannelPointer) {
366 t.Helper()
367 s := NewDirStore(t.TempDir())
368 ctx := context.Background()
369 priv, pub := testKeys(t)
370 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
371 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
372 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
373 t.Fatal(err)
374 }
375 markerOn(t, s, priv, "bab")
376 if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil {
377 t.Fatal(err)
378 }
379 if err := RemoveDigestMarker(ctx, s, "bab"); err != nil {
380 t.Fatal(err)
381 }
382 if m, err := ReadDigestMarker(ctx, s, pub, "bab"); m != nil || err != nil {
383 t.Fatalf("marker after removal = %+v, %v", m, err)
384 }
385 return s, priv, func() *ChannelPointer { return readPtr(t, s, pub, "stable") }
386}
387
388// Astra r1 #3, ruling P9 reversed: suspending emission does not make a
389// digest-less history entry a rollback target on a channel with digests.
390func TestRollbackOntoADigestlessEntryIsRefusedWithTheMarkerOff(t *testing.T) {
391 s, priv, cur := digestChannelThenMarkerOff(t)
392 before := cur()
393 err := Rollback(context.Background(), s, priv, "bab", "km", "stable")
394 var rn *ResignNeededError
395 if !errors.As(err, &rn) || rn.Version != "v1" {
396 t.Fatalf("marker off, rollback onto a digest-less v1 = %v; want ResignNeededError naming v1", err)
397 }
398 if after := cur(); after.Version != before.Version || !after.PromotedAt.Equal(before.PromotedAt) {
399 t.Fatalf("the refused rollback changed the pointer: %+v -> %+v", before, after)
400 }
401}
402
403// The approval-gated rollback runs as a hotfix promote of History[0]; with
404// the marker off it is refused the same way.
405func TestApprovalGatedRollbackOntoADigestlessEntryIsRefusedWithTheMarkerOff(t *testing.T) {
406 s, priv, cur := digestChannelThenMarkerOff(t)
407 before := cur()
408 _, err := PromoteOnLadder(context.Background(), s, priv, "bab", "km", "stable", "v1", PromoteOptions{Hotfix: true})
409 var rn *ResignNeededError
410 if !errors.As(err, &rn) || rn.Version != "v1" {
411 t.Fatalf("marker off, hotfix promote of a digest-less v1 = %v; want ResignNeededError naming v1", err)
412 }
413 if after := cur(); after.Version != before.Version || !after.PromotedAt.Equal(before.PromotedAt) {
414 t.Fatalf("the refused promote changed the pointer: %+v -> %+v", before, after)
415 }
416}
417
418// With the marker off, a promote back onto a history entry whose signed
419// digest the bucket's bytes no longer match is still refused.
420func TestPromoteOfAReshippedHistoryEntryIsRefusedWithTheMarkerOff(t *testing.T) {
421 s := NewDirStore(t.TempDir())
422 ctx := context.Background()
423 priv, pub := testKeys(t)
424 markerOn(t, s, priv, "bab")
425 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
426 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
427 for _, v := range []string{"v1", "v2"} {
428 if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
429 t.Fatal(err)
430 }
431 }
432 if err := RemoveDigestMarker(ctx, s, "bab"); err != nil {
433 t.Fatal(err)
434 }
435 reship(t, s, "bab", "km", "v1", []byte("not what was promoted"))
436 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "v1", PromoteOptions{Hotfix: true}); !errors.Is(err, ErrManifestDigest) {
437 t.Fatalf("marker off, promote of a re-shipped history entry = %v; want ErrManifestDigest", err)
438 }
439 if p := readPtr(t, s, pub, "stable"); p.Version != "v2" {
440 t.Fatalf("the refused promote moved stable to %s", p.Version)
441 }
442}
443
444// Only emission depends on the marker: with it off, a rollback onto an
445// entry that carries a digest succeeds and writes no digest fields at all.
446func TestRollbackOntoADigestEntryWithTheMarkerOffWritesNoDigests(t *testing.T) {
447 s := NewDirStore(t.TempDir())
448 ctx := context.Background()
449 priv, pub := testKeys(t)
450 markerOn(t, s, priv, "bab")
451 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
452 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
453 for _, v := range []string{"v1", "v2"} {
454 if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
455 t.Fatal(err)
456 }
457 }
458 if err := RemoveDigestMarker(ctx, s, "bab"); err != nil {
459 t.Fatal(err)
460 }
461 if err := Rollback(ctx, s, priv, "bab", "km", "stable"); err != nil {
462 t.Fatalf("marker off, rollback onto a digest-bearing v1: %v", err)
463 }
464 p := readPtr(t, s, pub, "stable")
465 if p.Version != "v1" || p.Digest != "" || p.HistoryDigests != nil {
466 t.Fatalf("after a marker-off rollback: %+v; want v1 with no digest fields", p)
467 }
468 raw, _, _ := s.Get(ctx, ChannelKey("bab", "km", "stable"))
469 if strings.Contains(string(raw), "digest") {
470 t.Fatal("the marker-off rollback's wrapper mentions digests")
471 }
472}
473
474// Task 5's deferred minor: an unverifiable marker refuses rollback and
475// fast-track as it refuses promote, and the pointer is unchanged.
476func TestRollbackAndFastTrackRefuseAnUnverifiableMarker(t *testing.T) {
477 s := NewDirStore(t.TempDir())
478 ctx := context.Background()
479 priv, pub := testKeys(t)
480 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
481 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
482 for _, v := range []string{"v1", "v2"} {
483 if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
484 t.Fatal(err)
485 }
486 }
487 other, _ := testKeys(t)
488 markerOn(t, s, other, "bab")
489 before := readPtr(t, s, pub, "stable")
490 _, merr := ReadDigestMarker(ctx, s, pub, "bab")
491 if merr == nil {
492 t.Fatal("control: a foreign-key marker verified")
493 }
494 if err := Rollback(ctx, s, priv, "bab", "km", "stable"); err == nil || err.Error() != fmt.Sprintf(TextMarkerUnreadable, "rollback", "bab", merr) {
495 t.Fatalf("Rollback with a foreign marker = %v; want the marker refusal", err)
496 }
497 if _, err := FastTrack(ctx, s, priv, "bab", "km", "stable", "ops@example.com"); err == nil || err.Error() != fmt.Sprintf(TextMarkerUnreadable, "fast-track", "bab", merr) {
498 t.Fatalf("FastTrack with a foreign marker = %v; want the marker refusal", err)
499 }
500 if after := readPtr(t, s, pub, "stable"); after.Version != before.Version || after.FastTrack || !after.PromotedAt.Equal(before.PromotedAt) {
501 t.Fatalf("a refused write changed the pointer: %+v -> %+v", before, after)
502 }
503}
504