package uploads import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path/filepath" "regexp" "testing" "time" "spark-mcp-go/internal/storage" ) func TestStore_SaveAndRetrieve(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } data := []byte("hello uploads") sum := sha256.Sum256(data) wantSHA := hex.EncodeToString(sum[:]) fileID, name, size, sha256Hex, absPath, err := store.Save(data, "hello.txt") if err != nil { t.Fatalf("save: %v", err) } if fileID == "" || !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(fileID) { t.Errorf("fileID=%q, want 32 lowercase hex chars", fileID) } if name != "hello.txt" { t.Errorf("name=%q, want hello.txt", name) } if size != int64(len(data)) { t.Errorf("size=%d, want %d", size, len(data)) } if sha256Hex != wantSHA { t.Errorf("sha256=%q, want %q", sha256Hex, wantSHA) } if !filepath.IsAbs(absPath) { t.Errorf("absPath=%q is not absolute", absPath) } if filepath.Base(absPath) != fileID { t.Errorf("absPath base=%q, want fileID %q", filepath.Base(absPath), fileID) } gotData, err := os.ReadFile(absPath) if err != nil { t.Fatalf("read data file: %v", err) } if string(gotData) != string(data) { t.Errorf("data=%q, want %q", gotData, data) } info, err := os.Stat(absPath) if err != nil { t.Fatalf("stat data file: %v", err) } if info.Mode().Perm() != 0o640 { t.Errorf("data mode=%o, want %o", info.Mode().Perm(), 0o640) } metaPath := absPath + ".meta.json" metaBytes, err := os.ReadFile(metaPath) if err != nil { t.Fatalf("read sidecar: %v", err) } var sc sidecar if err := json.Unmarshal(metaBytes, &sc); err != nil { t.Fatalf("unmarshal sidecar: %v", err) } if sc.Name != "hello.txt" { t.Errorf("sidecar name=%q, want hello.txt", sc.Name) } if sc.Size != int64(len(data)) { t.Errorf("sidecar size=%d, want %d", sc.Size, len(data)) } if sc.Sha256 != wantSHA { t.Errorf("sidecar sha256=%q, want %q", sc.Sha256, wantSHA) } if sc.UploadedAt.IsZero() { t.Errorf("sidecar uploaded_at is zero") } metaInfo, err := os.Stat(metaPath) if err != nil { t.Fatalf("stat sidecar: %v", err) } if metaInfo.Mode().Perm() != 0o600 { t.Errorf("sidecar mode=%o, want %o", metaInfo.Mode().Perm(), 0o600) } validatedID, err := store.Validate(absPath) if err != nil { t.Fatalf("validate: %v", err) } if validatedID != fileID { t.Errorf("validatedID=%q, want %q", validatedID, fileID) } } func TestStore_AbsPath_RejectsMalformed(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } validID := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" if _, err := store.AbsPath(validID); err != nil { t.Errorf("valid fileID rejected: %v", err) } cases := []string{ "", "abc", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "0123456789abcdef0123456789abcdefg", "0123456789abcdef0123456789abcde ", } for _, id := range cases { t.Run(fmt.Sprintf("id=%q", id), func(t *testing.T) { _, err := store.AbsPath(id) if err == nil { t.Errorf("AbsPath(%q) succeeded, want error", id) } }) } } func TestStore_Validate_RejectsOutsideRoot(t *testing.T) { if os.PathSeparator != '/' { t.Skip("Unix-style path test") } store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } fileID := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" if err := os.WriteFile(filepath.Join(store.Root, fileID), []byte("x"), 0o640); err != nil { t.Fatalf("create data file: %v", err) } if err := os.WriteFile(filepath.Join(store.Root, fileID+".meta.json"), []byte(`{"uploaded_at":"`+time.Now().Format(time.RFC3339Nano)+`"}`), 0o600); err != nil { t.Fatalf("create sidecar: %v", err) } cases := []struct { name string path string }{ {"parent", filepath.Join(store.Root, "..", "other", fileID)}, {"dotdot", filepath.Join(store.Root, "..", "..", "tmp", fileID)}, {"different_volume", "/other/volume/" + fileID}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { _, err := store.Validate(c.path) if err == nil { t.Errorf("Validate(%q) succeeded, want error", c.path) } }) } } func TestStore_Sweep_DeletesExpiredKeepsRecent(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } now := time.Now() expiredID := "00000000000000000000000000000001" freshID := "00000000000000000000000000000002" noMetaID := "00000000000000000000000000000003" create := func(id string, uploadedAt *time.Time, withData bool) { if withData { if err := os.WriteFile(filepath.Join(store.Root, id), []byte("x"), 0o640); err != nil { t.Fatalf("create data file %s: %v", id, err) } } if uploadedAt != nil { sc := map[string]any{ "name": "x", "size": 1, "sha256": "abc", "uploaded_at": uploadedAt.Format(time.RFC3339Nano), } b, _ := json.Marshal(sc) if err := os.WriteFile(filepath.Join(store.Root, id+".meta.json"), b, 0o600); err != nil { t.Fatalf("create sidecar %s: %v", id, err) } } } create(expiredID, timePtr(now.Add(-2*time.Hour)), true) create(freshID, timePtr(now), true) create(noMetaID, nil, true) deleted, err := store.Sweep(context.Background(), 1*time.Hour) if err != nil { t.Fatalf("sweep: %v", err) } if deleted != 1 { t.Errorf("deleted=%d, want 1", deleted) } if _, err := os.Stat(filepath.Join(store.Root, expiredID)); !os.IsNotExist(err) { t.Errorf("expired data file still exists") } if _, err := os.Stat(filepath.Join(store.Root, expiredID+".meta.json")); !os.IsNotExist(err) { t.Errorf("expired sidecar still exists") } if _, err := os.Stat(filepath.Join(store.Root, freshID)); err != nil { t.Errorf("fresh data file missing: %v", err) } if _, err := os.Stat(filepath.Join(store.Root, noMetaID)); err != nil { t.Errorf("no-sidecar data file missing: %v", err) } } func TestStore_Sweep_OrphanSidecarReaped(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } id := "00000000000000000000000000000004" sc := map[string]any{ "name": "orphan", "size": 1, "sha256": "abc", "uploaded_at": time.Now().Add(-2 * time.Hour).Format(time.RFC3339Nano), } b, _ := json.Marshal(sc) if err := os.WriteFile(filepath.Join(store.Root, id+".meta.json"), b, 0o600); err != nil { t.Fatalf("create orphan sidecar: %v", err) } deleted, err := store.Sweep(context.Background(), 1*time.Hour) if err != nil { t.Fatalf("sweep: %v", err) } if deleted != 1 { t.Errorf("deleted=%d, want 1", deleted) } if _, err := os.Stat(filepath.Join(store.Root, id+".meta.json")); !os.IsNotExist(err) { t.Errorf("orphan sidecar still exists") } } func timePtr(t time.Time) *time.Time { return &t } func TestStore_Validate_RejectsSidecarPath(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } fileID := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" if err := os.WriteFile(filepath.Join(store.Root, fileID), []byte("x"), 0o640); err != nil { t.Fatalf("create data file: %v", err) } if err := os.WriteFile(filepath.Join(store.Root, fileID+".meta.json"), []byte(`{"uploaded_at":"`+time.Now().Format(time.RFC3339Nano)+`"}`), 0o600); err != nil { t.Fatalf("create sidecar: %v", err) } _, err = store.Validate(filepath.Join(store.Root, fileID+".meta.json")) if err == nil { t.Errorf("Validate(sidecar) succeeded, want error") } } func TestStore_Sweep_HonorsContextCancel(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } id := "00000000000000000000000000000006" if err := os.WriteFile(filepath.Join(store.Root, id), []byte("x"), 0o640); err != nil { t.Fatalf("create data file: %v", err) } sc := map[string]any{ "name": "x", "size": 1, "sha256": "abc", "uploaded_at": time.Now().Add(-2 * time.Hour).Format(time.RFC3339Nano), } b, _ := json.Marshal(sc) if err := os.WriteFile(filepath.Join(store.Root, id+".meta.json"), b, 0o600); err != nil { t.Fatalf("create sidecar: %v", err) } ctx, cancel := context.WithCancel(context.Background()) cancel() deleted, err := store.Sweep(ctx, 1*time.Hour) if err != ctx.Err() { t.Fatalf("sweep err=%v, want %v", err, ctx.Err()) } if deleted != 0 { t.Errorf("deleted=%d, want 0", deleted) } if _, existErr := os.Stat(filepath.Join(store.Root, id)); os.IsNotExist(existErr) { t.Logf("cancelled sweep managed to delete the file before checking ctx; this is timing-dependent and acceptable") } } func TestStore_Sweep_DeletesDataWithCorruptSidecar(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } id := "00000000000000000000000000000005" dataPath := filepath.Join(store.Root, id) metaPath := dataPath + ".meta.json" if err := os.WriteFile(dataPath, []byte("stale data"), 0o640); err != nil { t.Fatalf("create data file: %v", err) } // Corrupt sidecar: not valid JSON. if err := os.WriteFile(metaPath, []byte("not valid json"), 0o600); err != nil { t.Fatalf("create sidecar: %v", err) } // Set mtime well in the past so the fallback triggers. past := time.Now().Add(-2 * time.Hour) if err := os.Chtimes(dataPath, past, past); err != nil { t.Fatalf("set mtime: %v", err) } deleted, err := store.Sweep(context.Background(), 1*time.Hour) if err != nil { t.Fatalf("sweep: %v", err) } if deleted != 1 { t.Errorf("deleted=%d, want 1", deleted) } if _, err := os.Stat(dataPath); !os.IsNotExist(err) { t.Errorf("data file with corrupt sidecar still exists") } if _, err := os.Stat(metaPath); !os.IsNotExist(err) { t.Errorf("corrupt sidecar still exists") } } type fakeUploadRepo struct { calls []storage.UploadMeta err error } func (f *fakeUploadRepo) Create(ctx context.Context, fileID, name string, size int64, sha256Hex string, uploadedAt time.Time) error { f.calls = append(f.calls, storage.UploadMeta{ FileID: fileID, Name: name, Size: size, SHA256: sha256Hex, UploadedAt: uploadedAt, }) return f.err } func (f *fakeUploadRepo) Get(ctx context.Context, fileID string) (storage.UploadMeta, error) { for _, m := range f.calls { if m.FileID == fileID { return m, nil } } return storage.UploadMeta{}, storage.ErrNotFound } func (f *fakeUploadRepo) List(ctx context.Context, search string, limit int) ([]storage.UploadMeta, error) { return f.calls, nil } func (f *fakeUploadRepo) Delete(ctx context.Context, fileID string) error { return nil } func TestStore_Save_DualWrite(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } repo := &fakeUploadRepo{} store.SetRepo(repo) data := []byte("dual-write test") fileID, _, size, sha256Hex, absPath, err := store.Save(data, "dual.txt") if err != nil { t.Fatalf("save: %v", err) } if absPath == "" { t.Fatal("absPath empty") } if len(repo.calls) != 1 { t.Fatalf("repo.Create calls: got %d, want 1", len(repo.calls)) } call := repo.calls[0] if call.FileID != fileID { t.Errorf("FileID: got %q, want %q", call.FileID, fileID) } if call.Name != "dual.txt" { t.Errorf("Name: got %q, want dual.txt", call.Name) } if call.Size != size { t.Errorf("Size: got %d, want %d", call.Size, size) } if call.SHA256 != sha256Hex { t.Errorf("SHA256: got %q, want %q", call.SHA256, sha256Hex) } if call.UploadedAt.IsZero() { t.Errorf("UploadedAt is zero") } } func TestStore_Save_RepoErrorDoesNotFailUpload(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } repo := &fakeUploadRepo{err: errors.New("db down")} store.SetRepo(repo) data := []byte("db error test") fileID, _, _, _, absPath, err := store.Save(data, "error.txt") if err != nil { t.Fatalf("save should not fail when repo errors: %v", err) } if _, err := os.Stat(absPath); err != nil { t.Errorf("data file missing: %v", err) } if _, err := os.Stat(absPath + ".meta.json"); err != nil { t.Errorf("sidecar missing: %v", err) } if len(repo.calls) != 1 { t.Errorf("repo.Create calls: got %d, want 1", len(repo.calls)) } if repo.calls[0].FileID != fileID { t.Errorf("repo call file_id: got %q, want %q", repo.calls[0].FileID, fileID) } } func TestStore_Backfill(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } repo := &fakeUploadRepo{} now := time.Now() ids := []string{ "00000000000000000000000000000001", "00000000000000000000000000000002", "00000000000000000000000000000003", } for i, id := range ids { sc := map[string]any{ "name": fmt.Sprintf("file%d.txt", i), "size": i + 1, "sha256": fmt.Sprintf("sha%d", i), "uploaded_at": now.Add(time.Duration(i) * time.Second).Format(time.RFC3339Nano), } b, _ := json.Marshal(sc) if err := os.WriteFile(filepath.Join(store.Root, id+".meta.json"), b, 0o600); err != nil { t.Fatalf("create sidecar %s: %v", id, err) } } inserted, err := store.Backfill(context.Background(), repo) if err != nil { t.Fatalf("backfill: %v", err) } if inserted != 3 { t.Errorf("inserted=%d, want 3", inserted) } if len(repo.calls) != 3 { t.Errorf("repo.Create calls: got %d, want 3", len(repo.calls)) } // Second backfill must skip existing rows. inserted, err = store.Backfill(context.Background(), repo) if err != nil { t.Fatalf("second backfill: %v", err) } if inserted != 0 { t.Errorf("second inserted=%d, want 0", inserted) } if len(repo.calls) != 3 { t.Errorf("repo.Create calls after second backfill: got %d, want 3", len(repo.calls)) } } func TestStore_Backfill_SkipsExisting(t *testing.T) { store, err := New(t.TempDir()) if err != nil { t.Fatalf("new store: %v", err) } repo := &fakeUploadRepo{} id := "00000000000000000000000000000004" now := time.Now() sc := map[string]any{ "name": "existing.txt", "size": 5, "sha256": "sha", "uploaded_at": now.Format(time.RFC3339Nano), } b, _ := json.Marshal(sc) if err := os.WriteFile(filepath.Join(store.Root, id+".meta.json"), b, 0o600); err != nil { t.Fatalf("create sidecar: %v", err) } // Pre-seed the repo so the sidecar is already indexed. if err := repo.Create(context.Background(), id, "existing.txt", 5, "sha", now); err != nil { t.Fatalf("seed repo: %v", err) } inserted, err := store.Backfill(context.Background(), repo) if err != nil { t.Fatalf("backfill: %v", err) } if inserted != 0 { t.Errorf("inserted=%d, want 0", inserted) } }