package files import ( "errors" "io/fs" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" ) type failingReader struct { sent bool } func (r *failingReader) Read(buffer []byte) (int, error) { if !r.sent { r.sent = true return copy(buffer, "partial"), nil } return 0, errors.New("copy failed") } type closeFailingFile struct { *os.File } func (f *closeFailingFile) Close() error { _ = f.File.Close() return errors.New("close failed") } func TestSaveStoresFileUnderProjectDirectory(t *testing.T) { service := NewService(t.TempDir()) stored, err := service.Save(12, "brief.md", strings.NewReader("hello")) require.NoError(t, err) require.Equal(t, "brief.md", stored.OriginalName) require.Contains(t, stored.RelativePath, "projects/12/") require.FileExists(t, stored.AbsolutePath) } func TestSaveNeutralizesPathTraversal(t *testing.T) { service := NewService(t.TempDir()) stored, err := service.Save(12, "..\\..\\secret.txt", strings.NewReader("hello")) require.NoError(t, err) require.Equal(t, "secret.txt", stored.OriginalName) require.Contains(t, stored.RelativePath, "projects/12/") } func TestSaveRemovesTemporaryAndPartialFilesWhenCopyFails(t *testing.T) { storageRoot := t.TempDir() service := NewService(storageRoot) _, err := service.Save(12, "broken.bin", &failingReader{}) require.ErrorContains(t, err, "copy failed") requireNoStoredFiles(t, storageRoot) } func TestSaveRemovesTemporaryFileWhenCloseFails(t *testing.T) { storageRoot := t.TempDir() service := NewService(storageRoot) service.createTemp = func(directory, pattern string) (stagedFile, error) { file, err := os.CreateTemp(directory, pattern) if err != nil { return nil, err } return &closeFailingFile{File: file}, nil } _, err := service.Save(12, "broken.bin", strings.NewReader("content")) require.ErrorContains(t, err, "close failed") requireNoStoredFiles(t, storageRoot) } func TestSaveRemovesTemporaryAndPartialFilesWhenRenameFails(t *testing.T) { storageRoot := t.TempDir() service := NewService(storageRoot) service.rename = func(oldPath, newPath string) error { require.NoError(t, os.WriteFile(newPath, []byte("partial final"), 0o600)) return errors.New("rename failed") } _, err := service.Save(12, "broken.bin", strings.NewReader("content")) require.ErrorContains(t, err, "rename failed") requireNoStoredFiles(t, storageRoot) } func requireNoStoredFiles(t *testing.T, storageRoot string) { t.Helper() require.NoError(t, filepath.WalkDir(storageRoot, func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } if path != storageRoot && !entry.IsDir() { t.Fatalf("unexpected stored file after failure: %s", path) } return nil })) }