carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostfiles
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "path/filepath"
9 "reflect"
10 "strings"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/release"
14)
15
16const pfx = "instances/shop.test/abc/files/g1/"
17
18func open(t *testing.T, s release.Store) *Publisher {
19 t.Helper()
20 p, err := Open(context.Background(), s, pfx)
21 if err != nil {
22 t.Fatal(err)
23 }
24 return p
25}
26
27func stage(t *testing.T, p *Publisher, path, body string) {
28 t.Helper()
29 if err := p.Stage(context.Background(), path, []byte(body), 0o600, false); err != nil {
30 t.Fatal(err)
31 }
32}
33
34func commit(t *testing.T, p *Publisher) int64 {
35 t.Helper()
36 seq, err := p.Commit(context.Background())
37 if err != nil {
38 t.Fatal(err)
39 }
40 return seq
41}
42
43func read(t *testing.T, dir, rel string) string {
44 t.Helper()
45 b, err := os.ReadFile(filepath.Join(dir, rel))
46 if err != nil {
47 t.Fatal(err)
48 }
49 return string(b)
50}
51
52func TestPublishThenRestoreRoundTrips(t *testing.T) {
53 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
54 p := open(t, s)
55 stage(t, p, "instance.key", "k1")
56 stage(t, p, "uploads/a/b.txt", "hello")
57 if seq := commit(t, p); seq != 2 {
58 t.Fatalf("seq = %d, want 2", seq)
59 }
60 dir := t.TempDir()
61 m, st, err := Restore(ctx, s, pfx, dir, nil)
62 if err != nil {
63 t.Fatal(err)
64 }
65 if m.Seq != 2 || st.Written != 2 {
66 t.Fatalf("manifest seq %d, written %d", m.Seq, st.Written)
67 }
68 if read(t, dir, "instance.key") != "k1" || read(t, dir, "uploads/a/b.txt") != "hello" {
69 t.Fatal("content differs")
70 }
71 fi, _ := os.Stat(filepath.Join(dir, "instance.key"))
72 if fi.Mode().Perm() != 0o600 {
73 t.Fatalf("mode = %v, want 0600", fi.Mode().Perm())
74 }
75}
76
77func TestALaterWriteWinsAndAnUnchangedOneTakesNoSequence(t *testing.T) {
78 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
79 p := open(t, s)
80 stage(t, p, "instance.key", "old")
81 commit(t, p)
82 stage(t, p, "instance.key", "new")
83 stage(t, p, "instance.key", "new") // unchanged: no new sequence number
84 if seq := commit(t, p); seq != 2 {
85 t.Fatalf("seq = %d, want 2", seq)
86 }
87 dir := t.TempDir()
88 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
89 t.Fatal(err)
90 }
91 if got := read(t, dir, "instance.key"); got != "new" {
92 t.Fatalf("restored %q, want new", got)
93 }
94}
95
96// A1-4: a stalled upload of the OLD content that lands after the new
97// content was published must not come back.
98func TestALateUploadCannotResurrectOldContent(t *testing.T) {
99 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
100 p := open(t, s)
101 stage(t, p, "instance.key", "new")
102 commit(t, p)
103 // The late arrival: any object at all under objects/, with a higher number.
104 if _, err := s.Put(ctx, pfx+"objects/0000000000000099-deadbeef", []byte("old"), ""); err != nil {
105 t.Fatal(err)
106 }
107 dir := t.TempDir()
108 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
109 t.Fatal(err)
110 }
111 if got := read(t, dir, "instance.key"); got != "new" {
112 t.Fatalf("restored %q, want new", got)
113 }
114}
115
116func TestADeleteIsATombstoneAndRestoreRemovesTheFile(t *testing.T) {
117 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
118 p := open(t, s)
119 stage(t, p, "a.txt", "a")
120 stage(t, p, "b.txt", "b")
121 commit(t, p)
122 dir := t.TempDir()
123 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
124 t.Fatal(err)
125 }
126 if err := p.StageDelete("a.txt"); err != nil {
127 t.Fatal(err)
128 }
129 commit(t, p)
130 _, st, err := Restore(ctx, s, pfx, dir, nil)
131 if err != nil {
132 t.Fatal(err)
133 }
134 if _, err := os.Stat(filepath.Join(dir, "a.txt")); !os.IsNotExist(err) {
135 t.Fatalf("a.txt still there: %v", err)
136 }
137 if st.Removed != 1 || read(t, dir, "b.txt") != "b" {
138 t.Fatalf("removed %d", st.Removed)
139 }
140 if got := p.Paths(); len(got) != 1 || got[0] != "b.txt" {
141 t.Fatalf("Paths = %v", got)
142 }
143}
144
145func TestASecondWriterIsSupersededNotMerged(t *testing.T) {
146 s := release.NewDirStore(t.TempDir())
147 a, b := open(t, s), open(t, s)
148 stage(t, a, "x", "from a")
149 commit(t, a)
150 stage(t, b, "x", "from b")
151 if _, err := b.Commit(context.Background()); !errors.Is(err, ErrSuperseded) {
152 t.Fatalf("err = %v, want ErrSuperseded", err)
153 }
154 dir := t.TempDir()
155 if _, _, err := Restore(context.Background(), s, pfx, dir, nil); err != nil {
156 t.Fatal(err)
157 }
158 if got := read(t, dir, "x"); got != "from a" {
159 t.Fatalf("restored %q, want from a", got)
160 }
161}
162
163func TestAReopenedPublisherContinuesTheSequence(t *testing.T) {
164 s := release.NewDirStore(t.TempDir())
165 p := open(t, s)
166 stage(t, p, "x", "1")
167 commit(t, p)
168 q := open(t, s)
169 stage(t, q, "y", "2")
170 if seq := commit(t, q); seq != 2 {
171 t.Fatalf("seq = %d, want 2", seq)
172 }
173}
174
175// Review Focus 1.
176func TestCleanPathRefusesEscapes(t *testing.T) {
177 for _, bad := range []string{"", "/etc/passwd", "../x", "a/../../x", "a/../b", "./a", "a//b", "a/", ".", "..", "a\x00b"} {
178 if _, err := CleanPath(bad); err == nil {
179 t.Errorf("CleanPath(%q) accepted", bad)
180 }
181 }
182 for _, ok := range []string{"a", "a/b.txt", ".hidden", "a b/c", `back\slash`} {
183 if got, err := CleanPath(ok); err != nil || got != ok {
184 t.Errorf("CleanPath(%q) = %q, %v", ok, got, err)
185 }
186 }
187}
188
189func TestStageAndRestoreRefuseAnEscapingPath(t *testing.T) {
190 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
191 p := open(t, s)
192 if err := p.Stage(ctx, "../evil", []byte("x"), 0o600, false); err == nil {
193 t.Fatal("Stage accepted ../evil")
194 }
195 // A manifest written by something else with a hostile path.
196 body := `{"v":2,"seq":1,"entries":{"../evil":{"object":"objects/0000000000000001-aa","sha256":"aa","seq":1}},"platform":{}}`
197 hostile := "instances/h/i/files/g/"
198 if _, err := s.Put(ctx, ManifestKey(hostile), []byte(body), ""); err != nil {
199 t.Fatal(err)
200 }
201 parent := t.TempDir()
202 dir := filepath.Join(parent, "files")
203 if err := os.Mkdir(dir, 0o755); err != nil {
204 t.Fatal(err)
205 }
206 if _, _, err := Restore(ctx, s, hostile, dir, nil); err == nil {
207 t.Fatal("Restore accepted a manifest with ../evil")
208 }
209 if _, err := os.Stat(filepath.Join(parent, "evil")); !os.IsNotExist(err) {
210 t.Fatal("../evil was written")
211 }
212}
213
214func TestRestoreRefusesContentThatDoesNotMatchItsHash(t *testing.T) {
215 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
216 p := open(t, s)
217 stage(t, p, "x", "good")
218 commit(t, p)
219 m, _, err := Load(ctx, s, pfx)
220 if err != nil {
221 t.Fatal(err)
222 }
223 key := m.Entries["x"].Object
224 _, etag, _ := s.Get(ctx, key)
225 if _, err := s.Put(ctx, key, []byte("tampered"), etag); err != nil {
226 t.Fatal(err)
227 }
228 if _, _, err := Restore(ctx, s, pfx, t.TempDir(), nil); err == nil || !strings.Contains(err.Error(), "sha256") {
229 t.Fatalf("err = %v, want a sha256 mismatch", err)
230 }
231}
232
233func TestAnAbsentManifestRestoresNothing(t *testing.T) {
234 m, st, err := Restore(context.Background(), release.NewDirStore(t.TempDir()), pfx, t.TempDir(), nil)
235 if err != nil || m.Seq != 0 || st.Written != 0 {
236 t.Fatalf("m=%+v st=%+v err=%v", m, st, err)
237 }
238}
239
240func TestPlatformItemsAreNotWrittenIntoTheFilesDirectory(t *testing.T) {
241 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
242 p := open(t, s)
243 if err := p.StagePlatform(ctx, "wake.json", []byte(`{"v":1}`)); err != nil {
244 t.Fatal(err)
245 }
246 commit(t, p)
247 dir := t.TempDir()
248 m, _, err := Restore(ctx, s, pfx, dir, nil)
249 if err != nil {
250 t.Fatal(err)
251 }
252 if entries, _ := os.ReadDir(dir); len(entries) != 0 {
253 t.Fatalf("files directory has %d entries, want 0", len(entries))
254 }
255 got, err := ReadPlatform(ctx, s, pfx, m, "wake.json", nil)
256 if err != nil || string(got) != `{"v":1}` {
257 t.Fatalf("ReadPlatform = %q, %v", got, err)
258 }
259 if _, err := ReadPlatform(ctx, s, pfx, m, "absent", nil); !errors.Is(err, os.ErrNotExist) {
260 t.Fatalf("absent item: err = %v, want os.ErrNotExist", err)
261 }
262}
263
264// Review Focus 3: a skipped file stays in the manifest, visibly.
265func TestASkippedFileIsRecordedAndReportedOnRestore(t *testing.T) {
266 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
267 p := open(t, s)
268 if err := p.StageSkipped("huge.bin", "too-large"); err != nil {
269 t.Fatal(err)
270 }
271 commit(t, p)
272 _, st, err := Restore(ctx, s, pfx, t.TempDir(), nil)
273 if err != nil {
274 t.Fatal(err)
275 }
276 if len(st.Skipped) != 1 || st.Skipped[0] != "huge.bin" {
277 t.Fatalf("Skipped = %v", st.Skipped)
278 }
279}
280
281// A destination that already holds a symlink pointing elsewhere must not
282// let a write or a delete reach through it.
283func TestRestoreNeverWritesOrDeletesThroughASymlink(t *testing.T) {
284 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
285 p := open(t, s)
286 stage(t, p, "sub/x", "ours")
287 stage(t, p, "gone/y", "temp")
288 commit(t, p)
289 if err := p.StageDelete("gone/y"); err != nil {
290 t.Fatal(err)
291 }
292 commit(t, p)
293
294 outside, dir := t.TempDir(), t.TempDir()
295 if err := os.WriteFile(filepath.Join(outside, "y"), []byte("not ours"), 0o600); err != nil {
296 t.Fatal(err)
297 }
298 for _, link := range []string{"sub", "gone"} {
299 if err := os.Symlink(outside, filepath.Join(dir, link)); err != nil {
300 t.Fatal(err)
301 }
302 }
303 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
304 t.Fatal(err)
305 }
306 if _, err := os.Stat(filepath.Join(outside, "x")); !os.IsNotExist(err) {
307 t.Fatal("a file was written through the symlink")
308 }
309 if got := read(t, outside, "y"); got != "not ours" {
310 t.Fatalf("a file outside was changed to %q", got)
311 }
312 if fi, err := os.Lstat(filepath.Join(dir, "sub")); err != nil || !fi.IsDir() {
313 t.Fatalf("sub is not a real directory: %v %v", fi, err)
314 }
315 if got := read(t, dir, "sub/x"); got != "ours" {
316 t.Fatalf("sub/x = %q", got)
317 }
318}
319
320func TestRestoreHandlesADirectoryBecomingAFileAndBack(t *testing.T) {
321 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
322 p := open(t, s)
323 stage(t, p, "a/b", "child")
324 commit(t, p)
325 prior := t.TempDir() // a copy restored while "a" was a directory
326 if _, _, err := Restore(ctx, s, pfx, prior, nil); err != nil {
327 t.Fatal(err)
328 }
329
330 // "a" becomes a file.
331 if err := p.StageDelete("a/b"); err != nil {
332 t.Fatal(err)
333 }
334 stage(t, p, "a", "now a file")
335 commit(t, p)
336 for name, dir := range map[string]string{"an empty destination": t.TempDir(), "a previously restored one": prior} {
337 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
338 t.Fatalf("%s: %v", name, err)
339 }
340 if got := read(t, dir, "a"); got != "now a file" {
341 t.Fatalf("%s: a = %q", name, got)
342 }
343 }
344
345 // And back: "a" becomes a directory again.
346 if err := p.StageDelete("a"); err != nil {
347 t.Fatal(err)
348 }
349 stage(t, p, "a/c", "child again")
350 commit(t, p)
351 for name, dir := range map[string]string{"an empty destination": t.TempDir(), "a previously restored one": prior} {
352 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
353 t.Fatalf("%s: %v", name, err)
354 }
355 if got := read(t, dir, "a/c"); got != "child again" {
356 t.Fatalf("%s: a/c = %q", name, got)
357 }
358 }
359}
360
361func TestRestoreIntoAMissingDirectoryIsAnError(t *testing.T) {
362 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
363 p := open(t, s)
364 stage(t, p, "x", "x")
365 commit(t, p)
366 if _, _, err := Restore(ctx, s, pfx, filepath.Join(t.TempDir(), "absent"), nil); err == nil {
367 t.Fatal("restored into a directory that does not exist")
368 }
369}
370
371// Plan round 2, finding 3: a restore that returns nil has made its names
372// durable, and one that could not must say so.
373func TestRestoreSyncsEveryDirectoryItChangedBeforeReturning(t *testing.T) {
374 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
375 p := open(t, s)
376 stage(t, p, "top.txt", "t")
377 stage(t, p, "a/b/deep.txt", "d")
378 stage(t, p, "gone/old.txt", "o")
379 commit(t, p)
380 dir := t.TempDir()
381 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
382 t.Fatal(err)
383 }
384 if err := p.StageDelete("gone/old.txt"); err != nil {
385 t.Fatal(err)
386 }
387 commit(t, p)
388
389 var synced []string
390 orig := syncDir
391 t.Cleanup(func() { syncDir = orig })
392 syncDir = func(root *os.Root, rel string) error {
393 // Everything must already be in place when the first sync runs.
394 if len(synced) == 0 {
395 if _, err := os.Stat(filepath.Join(dir, "gone", "old.txt")); !os.IsNotExist(err) {
396 t.Error("a directory was synced before the tombstone was applied")
397 }
398 }
399 synced = append(synced, rel)
400 return orig(root, rel)
401 }
402 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
403 t.Fatal(err)
404 }
405 want := []string{"a/b", "a", "gone", "."}
406 if !reflect.DeepEqual(synced, want) {
407 t.Fatalf("synced %v, want %v (deepest first, the root last)", synced, want)
408 }
409
410 syncDir = func(*os.Root, string) error { return errors.New("disk says no") }
411 if _, _, err := Restore(ctx, s, pfx, dir, nil); err == nil || !strings.Contains(err.Error(), "disk says no") {
412 t.Fatalf("err = %v; a failed directory sync must fail the restore", err)
413 }
414}
415
416// Plan round 3: a restore that failed at its directory sync has already
417// removed the file. The retry finds nothing to remove, and must still
418// make that removal durable before it reports success.
419func TestARetriedRestoreStillSyncsADeletionAnEarlierAttemptMade(t *testing.T) {
420 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
421 p := open(t, s)
422 stage(t, p, "gone/old.txt", "o")
423 commit(t, p)
424 dir := t.TempDir()
425 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
426 t.Fatal(err)
427 }
428 if err := p.StageDelete("gone/old.txt"); err != nil {
429 t.Fatal(err)
430 }
431 commit(t, p)
432
433 orig := syncDir
434 t.Cleanup(func() { syncDir = orig })
435 syncDir = func(*os.Root, string) error { return errors.New("disk says no") }
436 if _, _, err := Restore(ctx, s, pfx, dir, nil); err == nil {
437 t.Fatal("the first attempt must fail")
438 }
439 if _, err := os.Stat(filepath.Join(dir, "gone", "old.txt")); !os.IsNotExist(err) {
440 t.Fatal("the first attempt should have removed the file before failing")
441 }
442 var synced []string
443 syncDir = func(root *os.Root, rel string) error { synced = append(synced, rel); return orig(root, rel) }
444 _, st, err := Restore(ctx, s, pfx, dir, nil)
445 if err != nil {
446 t.Fatal(err)
447 }
448 if st.Removed != 0 {
449 t.Fatalf("Removed = %d; the retry had nothing left to remove", st.Removed)
450 }
451 if want := []string{"gone", "."}; !reflect.DeepEqual(synced, want) {
452 t.Fatalf("the retry synced %v, want %v", synced, want)
453 }
454}
455
456// A deeper directory replaced by a file: the directories that vanish
457// under the new file are not synced (they do not exist), and the restore
458// still succeeds and syncs what survives.
459func TestRestoreHandlesADeepDirectoryBecomingAFile(t *testing.T) {
460 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
461 p := open(t, s)
462 stage(t, p, "a/b/c", "leaf")
463 commit(t, p)
464 dir := t.TempDir()
465 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
466 t.Fatal(err)
467 }
468 if err := p.StageDelete("a/b/c"); err != nil {
469 t.Fatal(err)
470 }
471 stage(t, p, "a", "now a file")
472 commit(t, p)
473
474 var synced []string
475 orig := syncDir
476 t.Cleanup(func() { syncDir = orig })
477 syncDir = func(root *os.Root, rel string) error { synced = append(synced, rel); return orig(root, rel) }
478 for name, d := range map[string]string{"a previously restored destination": dir, "an empty one": t.TempDir()} {
479 synced = nil
480 if _, _, err := Restore(ctx, s, pfx, d, nil); err != nil {
481 t.Fatalf("%s: %v", name, err)
482 }
483 if got := read(t, d, "a"); got != "now a file" {
484 t.Fatalf("%s: a = %q", name, got)
485 }
486 if want := []string{"."}; !reflect.DeepEqual(synced, want) {
487 t.Fatalf("%s: synced %v, want %v: a/b and a are gone, the root records it", name, synced, want)
488 }
489 }
490}
491
492// A store whose manifest Put is applied and then reports a lost response, once.
493type lostResponseStore struct {
494 release.Store
495 armed bool
496 getFail bool // the next Get of the manifest fails once
497 putFail bool // the next Put of the manifest fails once, unapplied
498}
499
500func (l *lostResponseStore) Get(ctx context.Context, key string) ([]byte, string, error) {
501 if l.getFail && strings.HasSuffix(key, "manifest.json") {
502 l.getFail = false
503 return nil, "", errors.New("read timed out")
504 }
505 return l.Store.Get(ctx, key)
506}
507
508func (l *lostResponseStore) Put(ctx context.Context, key string, body []byte, etag string) (string, error) {
509 if l.putFail && strings.HasSuffix(key, "manifest.json") {
510 l.putFail = false
511 return "", errors.New("connection refused")
512 }
513 got, err := l.Store.Put(ctx, key, body, etag)
514 if err == nil && l.armed && strings.HasSuffix(key, "manifest.json") {
515 l.armed = false
516 return "", errors.New("connection reset")
517 }
518 return got, err
519}
520
521func TestALostCommitResponseIsNotASupersede(t *testing.T) {
522 ctx := context.Background()
523 s := &lostResponseStore{Store: release.NewDirStore(t.TempDir())}
524 p := open(t, s)
525 stage(t, p, "x", "1")
526 s.armed = true
527 if _, err := p.Commit(ctx); err == nil || errors.Is(err, ErrSuperseded) {
528 t.Fatalf("first commit err = %v, want the transport error", err)
529 }
530 seq, err := p.Commit(ctx)
531 if err != nil || seq != 1 {
532 t.Fatalf("second commit = %d, %v", seq, err)
533 }
534 stage(t, p, "y", "2")
535 if seq := commit(t, p); seq != 2 {
536 t.Fatalf("seq = %d, want 2", seq)
537 }
538}
539
540func TestRestoreMasksTheModeToPermissionBits(t *testing.T) {
541 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
542 p := open(t, s)
543 stage(t, p, "x", "x")
544 commit(t, p)
545 m, etag, _, err := load(ctx, s, pfx)
546 if err != nil {
547 t.Fatal(err)
548 }
549 e := m.Entries["x"]
550 e.Mode = 0o4755
551 m.Entries["x"] = e
552 body, _ := json.Marshal(m)
553 if _, err := s.Put(ctx, ManifestKey(pfx), body, etag); err != nil {
554 t.Fatal(err)
555 }
556 dir := t.TempDir()
557 if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil {
558 t.Fatal(err)
559 }
560 fi, err := os.Stat(filepath.Join(dir, "x"))
561 if err != nil {
562 t.Fatal(err)
563 }
564 if fi.Mode() != 0o755 {
565 t.Fatalf("mode = %v, want -rwxr-xr-x with no special bits", fi.Mode())
566 }
567}
568
569func TestOverlappingLiveEntriesKeepTheNewerOne(t *testing.T) {
570 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
571 p := open(t, s)
572 stage(t, p, "a/b", "child")
573 stage(t, p, "a", "file") // newer than the child
574 commit(t, p)
575 dir := t.TempDir()
576 _, st, err := Restore(ctx, s, pfx, dir, nil)
577 if err != nil {
578 t.Fatal(err)
579 }
580 if got := read(t, dir, "a"); got != "file" || !reflect.DeepEqual(st.Superseded, []string{"a/b"}) || st.Written != 1 {
581 t.Fatalf("a = %q, superseded %v, written %d", got, st.Superseded, st.Written)
582 }
583
584 q := open(t, release.NewDirStore(t.TempDir()))
585 stage(t, q, "a", "file")
586 stage(t, q, "a/b", "child") // newer than the file
587 commit(t, q)
588 dir2 := t.TempDir()
589 _, st, err = Restore(ctx, q.s, pfx, dir2, nil)
590 if err != nil {
591 t.Fatal(err)
592 }
593 if got := read(t, dir2, "a/b"); got != "child" || !reflect.DeepEqual(st.Superseded, []string{"a"}) || st.Written != 1 {
594 t.Fatalf("a/b = %q, superseded %v, written %d", got, st.Superseded, st.Written)
595 }
596}
597
598func TestAFailedReReadAfterALostResponseIsNotASupersede(t *testing.T) {
599 ctx := context.Background()
600 s := &lostResponseStore{Store: release.NewDirStore(t.TempDir())}
601 p := open(t, s)
602 stage(t, p, "x", "1")
603 s.armed = true
604 if _, err := p.Commit(ctx); err == nil {
605 t.Fatal("first commit should fail")
606 }
607 s.getFail = true
608 if _, err := p.Commit(ctx); err == nil || errors.Is(err, ErrSuperseded) {
609 t.Fatalf("second commit err = %v, want the read error, not ErrSuperseded", err)
610 }
611 if seq, err := p.Commit(ctx); err != nil || seq != 1 {
612 t.Fatalf("third commit = %d, %v", seq, err)
613 }
614}
615
616func TestThreeDeepOverlapKeepsTheNewestEntry(t *testing.T) {
617 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
618 p := open(t, s)
619 stage(t, p, "a", "one")
620 stage(t, p, "a/b/c", "three")
621 stage(t, p, "a/b", "two") // the newest
622 commit(t, p)
623 dir := t.TempDir()
624 _, st, err := Restore(ctx, s, pfx, dir, nil)
625 if err != nil {
626 t.Fatal(err)
627 }
628 if got := read(t, dir, "a/b"); got != "two" || st.Written != 1 || !reflect.DeepEqual(st.Superseded, []string{"a", "a/b/c"}) {
629 t.Fatalf("a/b = %q, written %d, superseded %v", got, st.Written, st.Superseded)
630 }
631}
632
633// A Stage between a commit whose response was lost and its retry changes
634// the body; the retry must still recognise the stored manifest as its own.
635func TestALostCommitResponseFollowedByAStageIsNotASupersede(t *testing.T) {
636 ctx := context.Background()
637 s := &lostResponseStore{Store: release.NewDirStore(t.TempDir())}
638 p := open(t, s)
639 stage(t, p, "x", "1")
640 s.armed = true
641 if _, err := p.Commit(ctx); err == nil || errors.Is(err, ErrSuperseded) {
642 t.Fatalf("first commit err = %v, want the transport error", err)
643 }
644 stage(t, p, "y", "2")
645 seq, err := p.Commit(ctx)
646 if err != nil || seq != 2 {
647 t.Fatalf("second commit = %d, %v; want 2, nil", seq, err)
648 }
649 m, _, err := Load(ctx, s, pfx)
650 if err != nil {
651 t.Fatal(err)
652 }
653 if m.Seq != 2 || m.Entries["x"].Seq != 1 || m.Entries["y"].Seq != 2 {
654 t.Fatalf("stored manifest = %+v, want x@1 and y@2 at seq 2", m)
655 }
656 stage(t, p, "z", "3")
657 if seq := commit(t, p); seq != 3 {
658 t.Fatalf("third commit seq = %d, want 3", seq)
659 }
660}
661
662// Two failed attempts against the same stale etag: only one can have been
663// applied, and whichever it was, the publisher recognises it.
664func TestTwoFailedCommitsThenAStageRecognisesTheAppliedOne(t *testing.T) {
665 ctx := context.Background()
666 s := &lostResponseStore{Store: release.NewDirStore(t.TempDir())}
667 p := open(t, s)
668 stage(t, p, "x", "1")
669 s.armed = true
670 if _, err := p.Commit(ctx); err == nil {
671 t.Fatal("first commit should fail")
672 }
673 stage(t, p, "y", "2")
674 s.putFail = true // the second attempt never reaches the store
675 if _, err := p.Commit(ctx); err == nil || errors.Is(err, ErrSuperseded) {
676 t.Fatalf("second commit err = %v, want the transport error", err)
677 }
678 stage(t, p, "z", "3")
679 if seq, err := p.Commit(ctx); err != nil || seq != 3 {
680 t.Fatalf("third commit = %d, %v; want 3, nil", seq, err)
681 }
682}
683
684// putHostile publishes a good manifest with one file "a", lets bend alter
685// the stored manifest, and returns a files directory that already holds a
686// file Restore would remove or replace if it acted.
687func putHostile(t *testing.T, bend func(m *Manifest)) (release.Store, string) {
688 t.Helper()
689 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
690 p := open(t, s)
691 stage(t, p, "a", "good")
692 stage(t, p, "gone", "soon")
693 commit(t, p)
694 if err := p.StageDelete("gone"); err != nil {
695 t.Fatal(err)
696 }
697 if err := p.StagePlatform(ctx, "item", []byte("plat")); err != nil {
698 t.Fatal(err)
699 }
700 commit(t, p)
701 m, etag, _, err := load(ctx, s, pfx)
702 if err != nil {
703 t.Fatal(err)
704 }
705 bend(&m)
706 body, _ := json.Marshal(m)
707 if _, err := s.Put(ctx, ManifestKey(pfx), body, etag); err != nil {
708 t.Fatal(err)
709 }
710 dir := t.TempDir()
711 if err := os.WriteFile(filepath.Join(dir, "gone"), []byte("still here"), 0o600); err != nil {
712 t.Fatal(err)
713 }
714 return s, dir
715}
716
717func TestRestoreRefusesAnEntryWhoseObjectOrHashIsMalformedAndTouchesNothing(t *testing.T) {
718 cases := map[string]func(m *Manifest){
719 "uppercase sha": func(m *Manifest) {
720 e := m.Entries["a"]
721 e.SHA256 = strings.ToUpper(e.SHA256)
722 e.Object = pfx + objectKey(e.Seq, e.SHA256)
723 m.Entries["a"] = e
724 },
725 "short sha": func(m *Manifest) {
726 e := m.Entries["a"]
727 e.SHA256, e.Object = "aa", pfx+objectKey(e.Seq, "aa")
728 m.Entries["a"] = e
729 },
730 "object elsewhere": func(m *Manifest) {
731 e := m.Entries["a"]
732 e.Object = "instances/other/files/g1/" + objectKey(e.Seq, e.SHA256)
733 m.Entries["a"] = e
734 },
735 "object with another seq": func(m *Manifest) {
736 e := m.Entries["a"]
737 e.Object = pfx + objectKey(e.Seq+1, e.SHA256)
738 m.Entries["a"] = e
739 },
740 "platform object elsewhere": func(m *Manifest) {
741 e := m.Platform["item"]
742 e.Object = "objects/elsewhere"
743 m.Platform["item"] = e
744 },
745 "platform sha not hex": func(m *Manifest) {
746 e := m.Platform["item"]
747 e.SHA256 = strings.Repeat("g", 64)
748 e.Object = pfx + objectKey(e.Seq, e.SHA256)
749 m.Platform["item"] = e
750 },
751 }
752 for name, bend := range cases {
753 s, dir := putHostile(t, bend)
754 if _, _, err := Restore(context.Background(), s, pfx, dir, nil); err == nil {
755 t.Fatalf("%s: Restore accepted the manifest", name)
756 }
757 if got := read(t, dir, "gone"); got != "still here" {
758 t.Fatalf("%s: gone = %q, want it untouched", name, got)
759 }
760 if _, err := os.Stat(filepath.Join(dir, "a")); !os.IsNotExist(err) {
761 t.Fatalf("%s: a was written", name)
762 }
763 }
764}
765
766func TestReadPlatformRefusesAMalformedEntry(t *testing.T) {
767 s, _ := putHostile(t, func(m *Manifest) {
768 e := m.Platform["item"]
769 e.Object = pfx + "objects/elsewhere"
770 m.Platform["item"] = e
771 })
772 ctx := context.Background()
773 // The right bytes at the wrong key: only the key check can refuse it.
774 if _, err := s.Put(ctx, pfx+"objects/elsewhere", []byte("plat"), ""); err != nil {
775 t.Fatal(err)
776 }
777 m, _, err := Load(ctx, s, pfx)
778 if err != nil {
779 t.Fatal(err)
780 }
781 if _, err := ReadPlatform(ctx, s, pfx, m, "item", nil); err == nil {
782 t.Fatal("ReadPlatform accepted an object key that is not its own")
783 }
784}
785
786func TestRestoreRefusesAnEntryWhoseSizeIsOutOfRangeAndTouchesNothing(t *testing.T) {
787 for _, size := range []int64{-1, MaxFileBytes + 1} {
788 s, dir := putHostile(t, func(m *Manifest) {
789 e := m.Entries["a"]
790 e.Size = size
791 m.Entries["a"] = e
792 })
793 if _, _, err := Restore(context.Background(), s, pfx, dir, nil); err == nil {
794 t.Fatalf("size %d: Restore accepted the manifest", size)
795 }
796 if got := read(t, dir, "gone"); got != "still here" {
797 t.Fatalf("size %d: gone = %q, want it untouched", size, got)
798 }
799 }
800}
801
802func TestFetchRefusesAnObjectWhoseLengthIsNotTheRecordedSize(t *testing.T) {
803 s, dir := putHostile(t, func(m *Manifest) {
804 e := m.Entries["a"]
805 e.Size = 3 // the object is four bytes, and its hash is right
806 m.Entries["a"] = e
807 p := m.Platform["item"]
808 p.Size = 5
809 m.Platform["item"] = p
810 })
811 ctx := context.Background()
812 if _, _, err := Restore(ctx, s, pfx, dir, nil); err == nil || !strings.Contains(err.Error(), "size") {
813 t.Fatalf("Restore err = %v, want a size mismatch", err)
814 }
815 m, _, err := Load(ctx, s, pfx)
816 if err != nil {
817 t.Fatal(err)
818 }
819 if _, err := ReadPlatform(ctx, s, pfx, m, "item", nil); err == nil || !strings.Contains(err.Error(), "size") {
820 t.Fatalf("ReadPlatform err = %v, want a size mismatch", err)
821 }
822}
823