package storage import ( "bytes" "context" "crypto/sha256" "encoding/hex" "io" "os" "path/filepath" "strings" "testing" ) func newTestStore(t *testing.T) *FilesystemStore { t.Helper() fs, err := NewFilesystemStore(t.TempDir()) if err != nil { t.Fatalf("NewFilesystemStore: %v", err) } return fs } func TestNewFilesystemStore(t *testing.T) { dir := t.TempDir() sub := filepath.Join(dir, "a", "b") fs, err := NewFilesystemStore(sub) if err != nil { t.Fatalf("unexpected error: %v", err) } if !filepath.IsAbs(fs.root) { t.Errorf("root is not absolute: %s", fs.root) } info, err := os.Stat(sub) if err != nil { t.Fatalf("root dir missing: %v", err) } if !info.IsDir() { t.Error("root is not a directory") } } func TestPut(t *testing.T) { fs := newTestStore(t) ctx := context.Background() data := []byte("hello world") h := sha256.Sum256(data) wantChecksum := hex.EncodeToString(h[:]) result, err := fs.Put(ctx, "items/P001/rev1.FCStd", bytes.NewReader(data), int64(len(data)), "application/octet-stream") if err != nil { t.Fatalf("Put: %v", err) } if result.Key != "items/P001/rev1.FCStd" { t.Errorf("Key = %q, want %q", result.Key, "items/P001/rev1.FCStd") } if result.Size != int64(len(data)) { t.Errorf("Size = %d, want %d", result.Size, len(data)) } if result.Checksum != wantChecksum { t.Errorf("Checksum = %q, want %q", result.Checksum, wantChecksum) } // Verify file on disk. got, err := os.ReadFile(fs.path("items/P001/rev1.FCStd")) if err != nil { t.Fatalf("reading file: %v", err) } if !bytes.Equal(got, data) { t.Error("file content mismatch") } } func TestPutAtomicity(t *testing.T) { fs := newTestStore(t) ctx := context.Background() key := "test/atomic.bin" // Write an initial file. if _, err := fs.Put(ctx, key, strings.NewReader("original"), 8, ""); err != nil { t.Fatalf("initial Put: %v", err) } // Write with a reader that fails partway through. failing := io.MultiReader(strings.NewReader("partial"), &errReader{}) _, err := fs.Put(ctx, key, failing, 100, "") if err == nil { t.Fatal("expected error from failing reader") } // Original file should still be intact. got, err := os.ReadFile(fs.path(key)) if err != nil { t.Fatalf("reading file after failed put: %v", err) } if string(got) != "original" { t.Errorf("file content = %q, want %q", got, "original") } } type errReader struct{} func (e *errReader) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF } func TestGet(t *testing.T) { fs := newTestStore(t) ctx := context.Background() data := []byte("test content") if _, err := fs.Put(ctx, "f.txt", bytes.NewReader(data), int64(len(data)), ""); err != nil { t.Fatalf("Put: %v", err) } rc, err := fs.Get(ctx, "f.txt") if err != nil { t.Fatalf("Get: %v", err) } defer rc.Close() got, err := io.ReadAll(rc) if err != nil { t.Fatalf("ReadAll: %v", err) } if !bytes.Equal(got, data) { t.Error("content mismatch") } } func TestGetMissing(t *testing.T) { fs := newTestStore(t) _, err := fs.Get(context.Background(), "no/such/file") if err == nil { t.Fatal("expected error for missing file") } } func TestGetVersion(t *testing.T) { fs := newTestStore(t) ctx := context.Background() data := []byte("versioned") if _, err := fs.Put(ctx, "v.txt", bytes.NewReader(data), int64(len(data)), ""); err != nil { t.Fatalf("Put: %v", err) } // GetVersion ignores versionID, returns same file. rc, err := fs.GetVersion(ctx, "v.txt", "ignored-version-id") if err != nil { t.Fatalf("GetVersion: %v", err) } defer rc.Close() got, err := io.ReadAll(rc) if err != nil { t.Fatalf("ReadAll: %v", err) } if !bytes.Equal(got, data) { t.Error("content mismatch") } } func TestDelete(t *testing.T) { fs := newTestStore(t) ctx := context.Background() if _, err := fs.Put(ctx, "del.txt", strings.NewReader("x"), 1, ""); err != nil { t.Fatalf("Put: %v", err) } if err := fs.Delete(ctx, "del.txt"); err != nil { t.Fatalf("Delete: %v", err) } if _, err := os.Stat(fs.path("del.txt")); !os.IsNotExist(err) { t.Error("file still exists after delete") } } func TestDeleteMissing(t *testing.T) { fs := newTestStore(t) if err := fs.Delete(context.Background(), "no/such/file"); err != nil { t.Fatalf("Delete missing file should not error: %v", err) } } func TestExists(t *testing.T) { fs := newTestStore(t) ctx := context.Background() ok, err := fs.Exists(ctx, "nope") if err != nil { t.Fatalf("Exists: %v", err) } if ok { t.Error("Exists returned true for missing file") } if _, err := fs.Put(ctx, "yes.txt", strings.NewReader("y"), 1, ""); err != nil { t.Fatalf("Put: %v", err) } ok, err = fs.Exists(ctx, "yes.txt") if err != nil { t.Fatalf("Exists: %v", err) } if !ok { t.Error("Exists returned false for existing file") } } func TestCopy(t *testing.T) { fs := newTestStore(t) ctx := context.Background() data := []byte("copy me") if _, err := fs.Put(ctx, "src.bin", bytes.NewReader(data), int64(len(data)), ""); err != nil { t.Fatalf("Put: %v", err) } if err := fs.Copy(ctx, "src.bin", "deep/nested/dst.bin"); err != nil { t.Fatalf("Copy: %v", err) } got, err := os.ReadFile(fs.path("deep/nested/dst.bin")) if err != nil { t.Fatalf("reading copied file: %v", err) } if !bytes.Equal(got, data) { t.Error("copied content mismatch") } // Source should still exist. if _, err := os.Stat(fs.path("src.bin")); err != nil { t.Error("source file missing after copy") } } func TestPresignPut(t *testing.T) { fs := newTestStore(t) _, err := fs.PresignPut(context.Background(), "key", 5*60) if err != ErrPresignNotSupported { t.Errorf("PresignPut error = %v, want ErrPresignNotSupported", err) } } func TestPing(t *testing.T) { fs := newTestStore(t) if err := fs.Ping(context.Background()); err != nil { t.Fatalf("Ping: %v", err) } } func TestPingBadRoot(t *testing.T) { fs := &FilesystemStore{root: "/nonexistent/path/that/should/not/exist"} if err := fs.Ping(context.Background()); err == nil { t.Fatal("expected Ping to fail with invalid root") } } func TestPutOverwrite(t *testing.T) { fs := newTestStore(t) ctx := context.Background() if _, err := fs.Put(ctx, "ow.txt", strings.NewReader("first"), 5, ""); err != nil { t.Fatalf("Put: %v", err) } if _, err := fs.Put(ctx, "ow.txt", strings.NewReader("second"), 6, ""); err != nil { t.Fatalf("Put overwrite: %v", err) } got, _ := os.ReadFile(fs.path("ow.txt")) if string(got) != "second" { t.Errorf("content = %q, want %q", got, "second") } }