- Remove seed_helpers.go (SeedTestMessage, SeedTestMessages, SeedTestCursor — all unused, channel/message features gutted) - Rename channel- prefixed test paths to test- in storage tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
296 lines
6.5 KiB
Go
296 lines
6.5 KiB
Go
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")
|
|
}
|
|
}
|