Files
honeyDueAPI/internal/services/storage_service_test.go
Trey T bec880886b Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests
- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests)
- Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests)
- Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests)
- Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests)
- Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests)
- Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:30:09 -05:00

341 lines
8.5 KiB
Go

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")
}
}