package submit import ( "context" "errors" "io" "os" "path/filepath" "strings" "syscall" "testing" ) func TestLocalContextStorePutAndExists(t *testing.T) { base := t.TempDir() s := &LocalContextStore{Base: base} ctx := context.Background() if ok, err := s.Exists(ctx, "sub-abc"); err != nil || ok { t.Fatalf("Exists before Put = (%v, %v), want (false, nil)", ok, err) } payload := "\x1f\x8b\x08\x00the modpack context" n, err := s.Put(ctx, "sub-abc", strings.NewReader(payload)) if err != nil { t.Fatalf("Put: %v", err) } if n != int64(len(payload)) { t.Fatalf("Put returned %d bytes, want %d", n, len(payload)) } // The blob lands at exactly {base}/{id}/context.tar.gz — where deriveContextRef // points Kaniko's --context. dest := filepath.Join(base, "sub-abc", contextBlobName) got, err := os.ReadFile(dest) if err != nil { t.Fatalf("read stored blob: %v", err) } if string(got) != payload { t.Fatalf("stored %q, want %q", got, payload) } if ok, err := s.Exists(ctx, "sub-abc"); err != nil || !ok { t.Fatalf("Exists after Put = (%v, %v), want (true, nil)", ok, err) } // The write is atomic: no leftover temp files beside the committed blob. entries, err := os.ReadDir(filepath.Join(base, "sub-abc")) if err != nil { t.Fatalf("read dir: %v", err) } if len(entries) != 1 || entries[0].Name() != contextBlobName { var names []string for _, e := range entries { names = append(names, e.Name()) } t.Fatalf("dir entries = %v, want only %q (no temp files)", names, contextBlobName) } } // Open is the internal context-fetch route's read path: it serves exactly the // stored bytes, and a missing blob is ErrBlobNotFound (404), never a bare os error. func TestLocalContextStoreOpen(t *testing.T) { base := t.TempDir() s := &LocalContextStore{Base: base} ctx := context.Background() if _, err := s.Open(ctx, "sub-gone"); !errors.Is(err, ErrBlobNotFound) { t.Fatalf("Open of a missing blob = %v, want ErrBlobNotFound", err) } payload := "\x1f\x8b\x08\x00the modpack context" if _, err := s.Put(ctx, "sub-abc", strings.NewReader(payload)); err != nil { t.Fatalf("Put: %v", err) } rc, err := s.Open(ctx, "sub-abc") if err != nil { t.Fatalf("Open: %v", err) } defer rc.Close() got, err := io.ReadAll(rc) if err != nil { t.Fatalf("read: %v", err) } if string(got) != payload { t.Fatalf("Open served %q, want %q", got, payload) } // The same path guard as Put: an id that could escape Base is refused. if _, err := s.Open(ctx, "../etc/passwd"); err == nil { t.Fatal("Open must reject an unsafe id") } } func TestLocalContextStorePutOverwrites(t *testing.T) { base := t.TempDir() s := &LocalContextStore{Base: base} ctx := context.Background() if _, err := s.Put(ctx, "sub-1", strings.NewReader("\x1f\x8bfirst")); err != nil { t.Fatalf("first Put: %v", err) } if _, err := s.Put(ctx, "sub-1", strings.NewReader("\x1f\x8bsecond upload")); err != nil { t.Fatalf("second Put: %v", err) } got, err := os.ReadFile(filepath.Join(base, "sub-1", contextBlobName)) if err != nil { t.Fatalf("read: %v", err) } if string(got) != "\x1f\x8bsecond upload" { t.Fatalf("stored %q, want the second upload (a re-upload supersedes)", got) } } // Put flushes the new bytes to disk before the rename publishes them, then the // directory entries the rename and a first upload's mkdir wrote. A crash at any // point leaves the previous blob or the whole new one, never an empty file behind // a digest the database already recorded. func TestLocalContextStorePutFlushesAroundTheRename(t *testing.T) { base := t.TempDir() s := &LocalContextStore{Base: base} blob := filepath.Join(base, "sub-abc", contextBlobName) var synced []string s.sync = func(f *os.File) error { name := f.Name() if strings.HasSuffix(name, ".tmp") { name = "temp" } published := "none" if b, err := os.ReadFile(blob); err == nil { published = string(b) } synced = append(synced, name+" published="+published) return f.Sync() } if _, err := s.Put(context.Background(), "sub-abc", strings.NewReader("\x1f\x8bbytes")); err != nil { t.Fatalf("Put: %v", err) } want := []string{ "temp published=none", filepath.Join(base, "sub-abc") + " published=\x1f\x8bbytes", base + " published=\x1f\x8bbytes", } if strings.Join(synced, "\n") != strings.Join(want, "\n") { t.Fatalf("flushes =\n%q\nwant\n%q", synced, want) } } // A flush that fails refuses the upload. Before the rename the previous context // stays in place and no temp file is left; after it the new bytes are in place // but not promised, so the error still reaches the caller. A filesystem that // cannot sync a directory at all has no stronger promise to give. func TestLocalContextStorePutFlushFailures(t *testing.T) { boom := errors.New("disk said no") for _, tc := range []struct { name string fail func(f *os.File) error wantErr error want string }{ {"file", func(f *os.File) error { if strings.HasSuffix(f.Name(), ".tmp") { return boom } return nil }, boom, "\x1f\x8bold"}, {"directory", func(f *os.File) error { if strings.HasSuffix(f.Name(), ".tmp") { return nil } return boom }, boom, "\x1f\x8bnew"}, {"directory sync invalid", func(f *os.File) error { if strings.HasSuffix(f.Name(), ".tmp") { return nil } return &os.PathError{Op: "sync", Path: f.Name(), Err: syscall.EINVAL} }, nil, "\x1f\x8bnew"}, {"directory sync not supported", func(f *os.File) error { if strings.HasSuffix(f.Name(), ".tmp") { return nil } return &os.PathError{Op: "sync", Path: f.Name(), Err: syscall.ENOTSUP} }, nil, "\x1f\x8bnew"}, } { t.Run(tc.name, func(t *testing.T) { base := t.TempDir() s := &LocalContextStore{Base: base} ctx := context.Background() if _, err := s.Put(ctx, "sub-abc", strings.NewReader("\x1f\x8bold")); err != nil { t.Fatalf("first Put: %v", err) } s.sync = tc.fail if _, err := s.Put(ctx, "sub-abc", strings.NewReader("\x1f\x8bnew")); !errors.Is(err, tc.wantErr) { t.Fatalf("Put = %v, want %v", err, tc.wantErr) } got, err := os.ReadFile(filepath.Join(base, "sub-abc", contextBlobName)) if err != nil || string(got) != tc.want { t.Fatalf("stored = %q, %v; want %q", got, err, tc.want) } entries, _ := os.ReadDir(filepath.Join(base, "sub-abc")) if len(entries) != 1 { t.Fatalf("dir holds %d entries, want only %s", len(entries), contextBlobName) } }) } } // Delete removes the blob and its id-namespaced directory, and is idempotent — // the retry-safety the withdraw/delete cleanup depends on. func TestLocalContextStoreDelete(t *testing.T) { base := t.TempDir() s := &LocalContextStore{Base: base} ctx := context.Background() if _, err := s.Put(ctx, "sub-abc", strings.NewReader("\x1f\x8bbytes")); err != nil { t.Fatalf("Put: %v", err) } if err := s.Delete(ctx, "sub-abc"); err != nil { t.Fatalf("Delete: %v", err) } if ok, err := s.Exists(ctx, "sub-abc"); err != nil || ok { t.Fatalf("Exists after Delete = (%v, %v), want (false, nil)", ok, err) } if _, err := os.Stat(filepath.Join(base, "sub-abc")); !os.IsNotExist(err) { t.Fatalf("per-submission dir still present after Delete (err=%v)", err) } // Idempotent: deleting nothing is success, so a retried cleanup cannot fail. if err := s.Delete(ctx, "sub-abc"); err != nil { t.Fatalf("second Delete = %v, want nil (idempotent)", err) } // The same path guard as Put/Open. if err := s.Delete(ctx, "../etc"); err == nil { t.Fatal("Delete must reject an unsafe id") } } func TestLocalContextStoreRejectsUnsafeID(t *testing.T) { base := t.TempDir() s := &LocalContextStore{Base: base} ctx := context.Background() for _, id := range []string{"../evil", "sub/../../etc", "SUB-UPPER", "has space", "", "a/b"} { if _, err := s.Put(ctx, id, strings.NewReader("\x1f\x8bx")); err == nil { t.Errorf("Put(%q) succeeded, want rejection", id) } if _, err := s.Exists(ctx, id); err == nil { t.Errorf("Exists(%q) succeeded, want rejection", id) } } // Nothing escaped the base directory. if _, err := os.Stat(filepath.Join(filepath.Dir(base), "evil")); !os.IsNotExist(err) { t.Fatal("an unsafe id wrote outside Base") } } func TestLocalContextStoreCheckRoom(t *testing.T) { s := &LocalContextStore{Base: t.TempDir(), MinFree: 1e-9} if err := s.CheckRoom(1); err != nil { t.Fatalf("CheckRoom(1 byte) = %v", err) } if err := s.CheckRoom(1 << 62); !errors.Is(err, ErrUploadsFull) { t.Fatalf("CheckRoom(4 EiB) = %v, want ErrUploadsFull", err) } s.MinFree = 1 if err := s.CheckRoom(0); !errors.Is(err, ErrUploadsFull) { t.Fatalf("CheckRoom with a 100%% floor = %v, want ErrUploadsFull", err) } }