package storage import ( "bytes" "context" "io" "os" "path/filepath" "testing" ) func tempStore(t *testing.T) *PVCStore { t.Helper() dir := t.TempDir() s, err := NewPVC(dir) if err != nil { t.Fatalf("NewPVC(%q): %v", dir, err) } return s } func TestPVC_PutGetRoundTrip(t *testing.T) { s := tempStore(t) ctx := context.Background() data := []byte("hello, storage world") key := "files/test-1/att-1_test.txt" // Put err := s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "text/plain") if err != nil { t.Fatalf("Put: %v", err) } // Get rc, size, ct, err := s.Get(ctx, key) if err != nil { t.Fatalf("Get: %v", err) } defer rc.Close() if size != int64(len(data)) { t.Errorf("size = %d, want %d", size, len(data)) } if ct != "text/plain" { t.Errorf("content_type = %q, want %q", ct, "text/plain") } got, _ := io.ReadAll(rc) if !bytes.Equal(got, data) { t.Errorf("data = %q, want %q", got, data) } } func TestPVC_PutAtomicWrite(t *testing.T) { s := tempStore(t) ctx := context.Background() key := "files/ch/file.bin" data := []byte("atomic test data") err := s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "application/octet-stream") if err != nil { t.Fatalf("Put: %v", err) } // File should exist at final path, no temp files left absPath := filepath.Join(s.basePath, "files", "ch") entries, _ := os.ReadDir(absPath) for _, e := range entries { if e.Name() != "file.bin" { t.Errorf("unexpected file in dir: %s", e.Name()) } } } func TestPVC_PutSizeMismatch(t *testing.T) { s := tempStore(t) ctx := context.Background() data := []byte("short") err := s.Put(ctx, "files/ch/f.txt", bytes.NewReader(data), 999, "text/plain") if err == nil { t.Fatal("expected size mismatch error") } } func TestPVC_GetNotFound(t *testing.T) { s := tempStore(t) ctx := context.Background() _, _, _, err := s.Get(ctx, "files/nonexistent/file.txt") if err != ErrNotFound { t.Errorf("err = %v, want ErrNotFound", err) } } func TestPVC_Delete(t *testing.T) { s := tempStore(t) ctx := context.Background() key := "files/ch/delete-me.txt" _ = s.Put(ctx, key, bytes.NewReader([]byte("bye")), 3, "text/plain") err := s.Delete(ctx, key) if err != nil { t.Fatalf("Delete: %v", err) } exists, _ := s.Exists(ctx, key) if exists { t.Error("file still exists after Delete") } } func TestPVC_DeleteIdempotent(t *testing.T) { s := tempStore(t) ctx := context.Background() // Delete a file that doesn't exist — should not error err := s.Delete(ctx, "files/no-such/file.txt") if err != nil { t.Errorf("Delete nonexistent: %v", err) } } func TestPVC_DeletePrefix(t *testing.T) { s := tempStore(t) ctx := context.Background() // Create several files under a channel prefix prefix := "files/test-abc/" for _, name := range []string{"a.txt", "b.png", "c.pdf"} { key := prefix + name _ = s.Put(ctx, key, bytes.NewReader([]byte("x")), 1, "text/plain") } // Also create a file in a different channel other := "files/test-other/keep.txt" _ = s.Put(ctx, other, bytes.NewReader([]byte("y")), 1, "text/plain") // Delete the channel prefix err := s.DeletePrefix(ctx, "files/test-abc") if err != nil { t.Fatalf("DeletePrefix: %v", err) } // Verify channel files are gone for _, name := range []string{"a.txt", "b.png", "c.pdf"} { exists, _ := s.Exists(ctx, prefix+name) if exists { t.Errorf("%s still exists after DeletePrefix", name) } } // Verify other channel is untouched exists, _ := s.Exists(ctx, other) if !exists { t.Error("other channel file was incorrectly deleted") } } func TestPVC_DeletePrefixNonexistent(t *testing.T) { s := tempStore(t) ctx := context.Background() // Should not error err := s.DeletePrefix(ctx, "files/does-not-exist/") if err != nil { t.Errorf("DeletePrefix nonexistent: %v", err) } } func TestPVC_Exists(t *testing.T) { s := tempStore(t) ctx := context.Background() key := "files/ch/exists.txt" _ = s.Put(ctx, key, bytes.NewReader([]byte("hi")), 2, "text/plain") exists, err := s.Exists(ctx, key) if err != nil { t.Fatalf("Exists: %v", err) } if !exists { t.Error("Exists returned false for existing file") } exists, err = s.Exists(ctx, "files/ch/nope.txt") if err != nil { t.Fatalf("Exists (missing): %v", err) } if exists { t.Error("Exists returned true for missing file") } } func TestPVC_Healthy(t *testing.T) { s := tempStore(t) if err := s.Healthy(context.Background()); err != nil { t.Errorf("Healthy: %v", err) } } func TestPVC_Stats(t *testing.T) { s := tempStore(t) ctx := context.Background() // Empty store stats, err := s.Stats(ctx) if err != nil { t.Fatalf("Stats: %v", err) } if stats.TotalFiles != 0 || stats.TotalBytes != 0 { t.Errorf("empty store: files=%d bytes=%d", stats.TotalFiles, stats.TotalBytes) } if !stats.Healthy || !stats.Configured { t.Error("expected healthy + configured") } // Add some files for i := 0; i < 3; i++ { key := filepath.Join("files", "ch", string(rune('a'+i))+".txt") data := bytes.Repeat([]byte("x"), 100*(i+1)) _ = s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "text/plain") } stats, err = s.Stats(ctx) if err != nil { t.Fatalf("Stats: %v", err) } if stats.TotalFiles != 3 { t.Errorf("files = %d, want 3", stats.TotalFiles) } if stats.TotalBytes != 600 { // 100 + 200 + 300 t.Errorf("bytes = %d, want 600", stats.TotalBytes) } } func TestPVC_PathTraversal(t *testing.T) { s := tempStore(t) ctx := context.Background() bad := []string{ "../etc/passwd", "files/../../etc/shadow", "/absolute/path", "", } for _, key := range bad { err := s.Put(ctx, key, bytes.NewReader([]byte("x")), 1, "text/plain") if err == nil { t.Errorf("Put(%q) should have failed", key) } _, _, _, err = s.Get(ctx, key) if err == nil { t.Errorf("Get(%q) should have failed", key) } err = s.Delete(ctx, key) if err == nil { t.Errorf("Delete(%q) should have failed", key) } } } func TestPVC_SubdirCreation(t *testing.T) { s := tempStore(t) // Verify well-known subdirs were created for _, sub := range []string{"files", "processing"} { info, err := os.Stat(filepath.Join(s.basePath, sub)) if err != nil { t.Errorf("subdir %q not created: %v", sub, err) continue } if !info.IsDir() { t.Errorf("%q is not a directory", sub) } } } func TestNewPVC_InvalidPath(t *testing.T) { // Path that can't be created (nested under a file) tmp := t.TempDir() filePath := filepath.Join(tmp, "blocker") os.WriteFile(filePath, []byte("x"), 0o644) _, err := NewPVC(filepath.Join(filePath, "subdir")) if err == nil { t.Error("expected error for invalid path") } }