package services import ( "bytes" "os" "path/filepath" "testing" "github.com/treytartt/honeydue-api/internal/config" ) func setupTestStorage(t *testing.T, encrypt bool) (*StorageService, string) { t.Helper() tmpDir := t.TempDir() cfg := &config.StorageConfig{ UploadDir: tmpDir, BaseURL: "/uploads", MaxFileSize: 10 * 1024 * 1024, AllowedTypes: "image/jpeg,image/png,application/pdf", } svc, err := NewStorageService(cfg) if err != nil { t.Fatalf("failed to create storage service: %v", err) } if encrypt { encSvc, err := NewEncryptionService(validTestKey()) if err != nil { t.Fatalf("failed to create encryption service: %v", err) } svc.SetEncryptionService(encSvc) } return svc, tmpDir } func TestReadFile_PlainFile(t *testing.T) { svc, tmpDir := setupTestStorage(t, false) // Write a plain file content := []byte("hello world") dir := filepath.Join(tmpDir, "images") if err := os.WriteFile(filepath.Join(dir, "test.jpg"), content, 0644); err != nil { t.Fatal(err) } data, mimeType, err := svc.ReadFile("/uploads/images/test.jpg") if err != nil { t.Fatalf("ReadFile failed: %v", err) } if !bytes.Equal(data, content) { t.Fatalf("data mismatch: got %q, want %q", data, content) } if mimeType == "" { t.Fatal("expected non-empty MIME type") } } func TestReadFile_EncryptedFile(t *testing.T) { svc, tmpDir := setupTestStorage(t, true) // Encrypt and write a file manually (simulating what Upload does) originalContent := []byte("sensitive document content here - must be long enough for detection") encSvc, _ := NewEncryptionService(validTestKey()) encrypted, err := encSvc.Encrypt(originalContent) if err != nil { t.Fatal(err) } dir := filepath.Join(tmpDir, "documents") if err := os.WriteFile(filepath.Join(dir, "test.pdf.enc"), encrypted, 0644); err != nil { t.Fatal(err) } // ReadFile should find the .enc file and decrypt it data, _, err := svc.ReadFile("/uploads/documents/test.pdf") if err != nil { t.Fatalf("ReadFile failed: %v", err) } if !bytes.Equal(data, originalContent) { t.Fatalf("decrypted data mismatch: got %q, want %q", data, originalContent) } } func TestReadFile_EncFilePreferredOverPlain(t *testing.T) { svc, tmpDir := setupTestStorage(t, true) encSvc, _ := NewEncryptionService(validTestKey()) plainContent := []byte("plain version") encContent := []byte("encrypted version - the correct one") encrypted, _ := encSvc.Encrypt(encContent) dir := filepath.Join(tmpDir, "images") os.WriteFile(filepath.Join(dir, "photo.jpg"), plainContent, 0644) os.WriteFile(filepath.Join(dir, "photo.jpg.enc"), encrypted, 0644) // Should prefer the .enc file data, _, err := svc.ReadFile("/uploads/images/photo.jpg") if err != nil { t.Fatalf("ReadFile failed: %v", err) } if !bytes.Equal(data, encContent) { t.Fatalf("expected encrypted version content, got %q", data) } } func TestReadFile_EmptyURL(t *testing.T) { svc, _ := setupTestStorage(t, false) _, _, err := svc.ReadFile("") if err == nil { t.Fatal("expected error for empty URL") } } func TestReadFile_MissingFile(t *testing.T) { svc, _ := setupTestStorage(t, false) _, _, err := svc.ReadFile("/uploads/images/nonexistent.jpg") if err == nil { t.Fatal("expected error for missing file") } } func TestDelete_HandlesEncAndPlain(t *testing.T) { svc, tmpDir := setupTestStorage(t, false) dir := filepath.Join(tmpDir, "images") // Create both plain and .enc files os.WriteFile(filepath.Join(dir, "photo.jpg"), []byte("plain"), 0644) os.WriteFile(filepath.Join(dir, "photo.jpg.enc"), []byte("encrypted"), 0644) err := svc.Delete("/uploads/images/photo.jpg") if err != nil { t.Fatalf("Delete failed: %v", err) } // Both should be gone if _, err := os.Stat(filepath.Join(dir, "photo.jpg")); !os.IsNotExist(err) { t.Fatal("plain file should be deleted") } if _, err := os.Stat(filepath.Join(dir, "photo.jpg.enc")); !os.IsNotExist(err) { t.Fatal("encrypted file should be deleted") } } func TestDelete_NonexistentFile(t *testing.T) { svc, _ := setupTestStorage(t, false) // Should not error for non-existent files err := svc.Delete("/uploads/images/nope.jpg") if err != nil { t.Fatalf("Delete should not error for non-existent file: %v", err) } } // === isAllowedType === func TestIsAllowedType(t *testing.T) { cfg := &config.StorageConfig{ UploadDir: t.TempDir(), BaseURL: "/uploads", MaxFileSize: 10 * 1024 * 1024, AllowedTypes: "image/jpeg,image/png,application/pdf", } svc := NewStorageServiceForTest(cfg) if !svc.isAllowedType("image/jpeg") { t.Fatal("image/jpeg should be allowed") } if !svc.isAllowedType("image/png") { t.Fatal("image/png should be allowed") } if !svc.isAllowedType("application/pdf") { t.Fatal("application/pdf should be allowed") } if svc.isAllowedType("text/html") { t.Fatal("text/html should not be allowed") } if svc.isAllowedType("") { t.Fatal("empty MIME should not be allowed") } } // === mimeTypesCompatible === func TestMimeTypesCompatible(t *testing.T) { cfg := &config.StorageConfig{ UploadDir: t.TempDir(), BaseURL: "/uploads", MaxFileSize: 10 * 1024 * 1024, AllowedTypes: "image/jpeg", } svc := NewStorageServiceForTest(cfg) // Same primary type if !svc.mimeTypesCompatible("image/jpeg", "image/png") { t.Fatal("image/* types should be compatible") } // Different primary types if svc.mimeTypesCompatible("image/jpeg", "application/pdf") { t.Fatal("image and application should not be compatible") } // Same exact types if !svc.mimeTypesCompatible("application/pdf", "application/octet-stream") { t.Fatal("application/* types should be compatible") } } // === getExtensionFromMimeType === func TestGetExtensionFromMimeType(t *testing.T) { cfg := &config.StorageConfig{ UploadDir: t.TempDir(), BaseURL: "/uploads", MaxFileSize: 10 * 1024 * 1024, AllowedTypes: "image/jpeg", } svc := NewStorageServiceForTest(cfg) tests := []struct { mimeType string expected string }{ {"image/jpeg", ".jpg"}, {"image/png", ".png"}, {"image/gif", ".gif"}, {"image/webp", ".webp"}, {"application/pdf", ".pdf"}, {"text/html", ""}, {"unknown/type", ""}, } for _, tt := range tests { got := svc.getExtensionFromMimeType(tt.mimeType) if got != tt.expected { t.Fatalf("getExtensionFromMimeType(%q) = %q, want %q", tt.mimeType, got, tt.expected) } } } // === GetUploadDir === func TestGetUploadDir(t *testing.T) { svc, tmpDir := setupTestStorage(t, false) if svc.GetUploadDir() != tmpDir { t.Fatalf("GetUploadDir() = %q, want %q", svc.GetUploadDir(), tmpDir) } } // === SetEncryptionService === func TestSetEncryptionService(t *testing.T) { svc, _ := setupTestStorage(t, false) // Initially no encryption service if svc.encryptionSvc != nil && svc.encryptionSvc.IsEnabled() { t.Fatal("encryption should not be enabled initially in plain mode") } encSvc, err := NewEncryptionService(validTestKey()) if err != nil { t.Fatal(err) } svc.SetEncryptionService(encSvc) if svc.encryptionSvc == nil { t.Fatal("encryption service should be set") } } // === NewStorageServiceForTest === func TestNewStorageServiceForTest(t *testing.T) { cfg := &config.StorageConfig{ UploadDir: "/tmp/test", BaseURL: "/uploads", MaxFileSize: 5 * 1024 * 1024, AllowedTypes: "image/jpeg, image/png, application/pdf", } svc := NewStorageServiceForTest(cfg) // Should have 3 allowed types (whitespace trimmed) if !svc.isAllowedType("image/jpeg") { t.Fatal("image/jpeg should be allowed") } if !svc.isAllowedType("image/png") { t.Fatal("image/png should be allowed") } if !svc.isAllowedType("application/pdf") { t.Fatal("application/pdf should be allowed") } } // === Delete only plain file === func TestDelete_OnlyPlainFile(t *testing.T) { svc, tmpDir := setupTestStorage(t, false) dir := filepath.Join(tmpDir, "images") os.WriteFile(filepath.Join(dir, "only-plain.jpg"), []byte("plain"), 0644) err := svc.Delete("/uploads/images/only-plain.jpg") if err != nil { t.Fatalf("Delete failed: %v", err) } if _, err := os.Stat(filepath.Join(dir, "only-plain.jpg")); !os.IsNotExist(err) { t.Fatal("plain file should be deleted") } } // === Delete only enc file === func TestDelete_OnlyEncFile(t *testing.T) { svc, tmpDir := setupTestStorage(t, false) dir := filepath.Join(tmpDir, "documents") os.WriteFile(filepath.Join(dir, "secret.pdf.enc"), []byte("encrypted"), 0644) err := svc.Delete("/uploads/documents/secret.pdf") if err != nil { t.Fatalf("Delete failed: %v", err) } if _, err := os.Stat(filepath.Join(dir, "secret.pdf.enc")); !os.IsNotExist(err) { t.Fatal("encrypted file should be deleted") } }