package services import ( "archive/tar" "archive/zip" "bytes" "compress/gzip" "encoding/xml" "fmt" "image" "image/color" "image/png" "os" "path/filepath" "testing" "time" ) // TestExtractZipMetadata tests .cbz (ZIP) metadata extraction func TestExtractZipMetadata(t *testing.T) { tmpDir := t.TempDir() cbzFile := filepath.Join(tmpDir, "test.cbz") // Create a test ZIP archive with ComicInfo.xml and cover image if err := createTestCBZ(cbzFile); err != nil { t.Fatalf("Failed to create test CBZ: %v", err) } // Test metadata extraction comicInfo, coverImage, err := extractZipMetadata(cbzFile) if err != nil { t.Fatalf("extractZipMetadata failed: %v", err) } // Verify metadata if comicInfo == nil { t.Fatal("comicInfo is nil") } if comicInfo.Title != "Test Comic Title" { t.Errorf("Expected title 'Test Comic Title', got '%s'", comicInfo.Title) } if comicInfo.Series != "Test Series" { t.Errorf("Expected series 'Test Series', got '%s'", comicInfo.Series) } if comicInfo.Number != 1 { t.Errorf("Expected issue number 1, got %d", comicInfo.Number) } if comicInfo.Publisher != "Test Publisher" { t.Errorf("Expected publisher 'Test Publisher', got '%s'", comicInfo.Publisher) } if comicInfo.Writer != "Test Writer" { t.Errorf("Expected writer 'Test Writer', got '%s'", comicInfo.Writer) } // Verify cover image if len(coverImage) == 0 { t.Error("Cover image not extracted") // Debug: Check if the file was created properly t.Logf("Debug: Checking created CBZ file") r, err := zip.OpenReader(cbzFile) if err != nil { t.Logf("Failed to open CBZ for debug: %v", err) return } defer r.Close() for _, f := range r.File { t.Logf("File in CBZ: %s (isImage: %v)", f.Name, isImageFile(f.Name)) } } else { // Verify it's a valid image _, format, err := image.Decode(bytes.NewReader(coverImage)) if err != nil { t.Errorf("Extracted cover is not a valid image: %v", err) } t.Logf("Cover image format: %s, size: %d bytes", format, len(coverImage)) } t.Logf("ZIP metadata extraction test passed: title=%s, series=%s, issue=%d", comicInfo.Title, comicInfo.Series, comicInfo.Number) } // TestExtractZipMetadataWithoutComicInfo tests .cbz without ComicInfo.xml func TestExtractZipMetadataWithoutComicInfo(t *testing.T) { tmpDir := t.TempDir() cbzFile := filepath.Join(tmpDir, "test_no_metadata.cbz") // Create a ZIP without ComicInfo.xml if err := createTestCBZWithoutMetadata(cbzFile); err != nil { t.Fatalf("Failed to create test CBZ: %v", err) } // Test metadata extraction comicInfo, coverImage, err := extractZipMetadata(cbzFile) if err != nil { t.Fatalf("extractZipMetadata failed: %v", err) } // Should fall back to filename if comicInfo.Title != "test_no_metadata" { t.Errorf("Expected title 'test_no_metadata', got '%s'", comicInfo.Title) } // Should still extract cover if len(coverImage) == 0 { t.Error("Cover image not extracted") } t.Logf("ZIP without ComicInfo.xml test passed: title=%s (fallback)", comicInfo.Title) } // TestExtractRarMetadata tests .cbr (RAR) metadata extraction func TestExtractRarMetadata(t *testing.T) { if testing.Short() { t.Skip("Skipping RAR test in short mode (requires test archive)") } // Note: Creating RAR archives requires additional libraries // This test will use a pre-created test file if available // For now, we'll test with a fallback tmpDir := t.TempDir() cbrFile := filepath.Join(tmpDir, "test.cbr") // Create a mock RAR file (not a real RAR, just for error handling test) if err := os.WriteFile(cbrFile, []byte("not a real rar"), 0644); err != nil { t.Fatalf("Failed to create mock RAR: %v", err) } // Test error handling _, _, err := extractRarMetadata(cbrFile) if err == nil { t.Error("Expected error for invalid RAR file, got nil") } t.Logf("RAR error handling test passed") } // TestExtract7ZipMetadata tests .cb7 (7-Zip) metadata extraction func TestExtract7ZipMetadata(t *testing.T) { if testing.Short() { t.Skip("Skipping 7-Zip test in short mode (requires test archive)") } tmpDir := t.TempDir() cb7File := filepath.Join(tmpDir, "test.cb7") // Create a mock 7-Zip file for error handling test if err := os.WriteFile(cb7File, []byte("not a real 7z"), 0644); err != nil { t.Fatalf("Failed to create mock 7z: %v", err) } // Test error handling _, _, err := extract7ZipMetadata(cb7File) if err == nil { t.Error("Expected error for invalid 7-Zip file, got nil") } t.Logf("7-Zip error handling test passed") } // TestExtractTarMetadata tests .cbt (TAR) metadata extraction func TestExtractTarMetadata(t *testing.T) { tmpDir := t.TempDir() cbtFile := filepath.Join(tmpDir, "test.cbt") // Create a test TAR archive with ComicInfo.xml and cover image if err := createTestCBT(cbtFile); err != nil { t.Fatalf("Failed to create test CBT: %v", err) } // Test metadata extraction comicInfo, coverImage, err := extractTarMetadata(cbtFile) if err != nil { t.Fatalf("extractTarMetadata failed: %v", err) } // Verify metadata if comicInfo == nil { t.Fatal("comicInfo is nil") } if comicInfo.Title != "Test Tar Comic" { t.Errorf("Expected title 'Test Tar Comic', got '%s'", comicInfo.Title) } // Verify cover image if len(coverImage) == 0 { t.Error("Cover image not extracted") } t.Logf("TAR metadata extraction test passed: title=%s", comicInfo.Title) } // TestExtractTarGzMetadata tests .tar.gz metadata extraction func TestExtractTarGzMetadata(t *testing.T) { tmpDir := t.TempDir() targzFile := filepath.Join(tmpDir, "test.tar.gz") // Create a test TAR.GZ archive if err := createTestTarGz(targzFile); err != nil { t.Fatalf("Failed to create test TAR.GZ: %v", err) } // Test metadata extraction comicInfo, coverImage, err := extractTarMetadata(targzFile) if err != nil { t.Fatalf("extractTarMetadata failed for tar.gz: %v", err) } // Verify metadata if comicInfo == nil { t.Fatal("comicInfo is nil") } if comicInfo.Title != "Test Tar Gz Comic" { t.Errorf("Expected title 'Test Tar Gz Comic', got '%s'", comicInfo.Title) } // Verify cover image if len(coverImage) == 0 { t.Error("Cover image not extracted") } t.Logf("TAR.GZ metadata extraction test passed: title=%s", comicInfo.Title) } // TestExtractComicMetadata tests the router function func TestExtractComicMetadata(t *testing.T) { tests := []struct { name string fileExt string createFunc func(string) error shouldFail bool expectedTitle string }{ { name: "CBZ file", fileExt: ".cbz", createFunc: func(path string) error { return createTestCBZ(path) }, shouldFail: false, expectedTitle: "Test Comic Title", }, { name: "CBT file", fileExt: ".cbt", createFunc: func(path string) error { return createTestCBT(path) }, shouldFail: false, expectedTitle: "Test Tar Comic", }, { name: "Unsupported format", fileExt: ".xyz", createFunc: func(path string) error { return os.WriteFile(path, []byte("test"), 0644) }, shouldFail: true, expectedTitle: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "test"+tt.fileExt) if err := tt.createFunc(testFile); err != nil { t.Fatalf("Failed to create test file: %v", err) } comicInfo, coverImage, err := extractComicMetadata(testFile) if tt.shouldFail { if err == nil { t.Error("Expected error, got nil") } return } if err != nil { t.Fatalf("extractComicMetadata failed: %v", err) } if comicInfo == nil { t.Fatal("comicInfo is nil") } if comicInfo.Title != tt.expectedTitle { t.Errorf("Expected title '%s', got '%s'", tt.expectedTitle, comicInfo.Title) } if len(coverImage) == 0 { t.Error("Cover image not extracted") } t.Logf("Test passed: title=%s, cover=%d bytes", comicInfo.Title, len(coverImage)) }) } } // TestIsImageFile tests image file detection func TestIsImageFile(t *testing.T) { tests := []struct { name string filename string expected bool }{ {"JPG file", "cover.jpg", true}, {"JPEG file", "cover.jpeg", true}, {"PNG file", "cover.png", true}, {"GIF file", "cover.gif", true}, {"Uppercase JPG", "cover.JPG", true}, {"Mixed case", "cover.JpG", true}, {"XML file", "ComicInfo.xml", false}, {"Text file", "readme.txt", false}, {"No extension", "cover", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isImageFile(tt.filename) if result != tt.expected { t.Errorf("Expected %v for %s, got %v", tt.expected, tt.filename, result) } }) } } // BenchmarkExtractZipMetadata benchmarks ZIP extraction func BenchmarkExtractZipMetadata(b *testing.B) { tmpDir := b.TempDir() cbzFile := filepath.Join(tmpDir, "bench.cbz") if err := createTestCBZ(cbzFile); err != nil { b.Fatalf("Failed to create test CBZ: %v", err) } b.ResetTimer() for i := 0; i < b.N; i++ { _, _, _ = extractZipMetadata(cbzFile) } } // Helper functions to create test archives // createTestCBZ creates a test ZIP archive with ComicInfo.xml and cover image func createTestCBZ(path string) error { file, err := os.Create(path) if err != nil { return err } defer file.Close() zipWriter := zip.NewWriter(file) defer zipWriter.Close() // Add ComicInfo.xml comicInfo := ComicInfo{ Title: "Test Comic Title", Series: "Test Series", Number: 1, Volume: 1, Publisher: "Test Publisher", Year: 2024, Month: 1, Day: 1, Writer: "Test Writer", Penciller: "Test Penciller", Inker: "Test Inker", Genre: "Test Genre", } xmlData, err := xml.Marshal(comicInfo) if err != nil { return err } writer, err := zipWriter.Create("ComicInfo.xml") if err != nil { return err } if _, err := writer.Write(xmlData); err != nil { return err } // Create a simple valid PNG image (1x1 red pixel) using image/png package img := image.NewRGBA(image.Rect(0, 0, 1, 1)) img.Set(0, 0, color.RGBA{255, 0, 0, 255}) var pngBuf bytes.Buffer if err := png.Encode(&pngBuf, img); err != nil { return err } coverPNG := pngBuf.Bytes() // Write cover with .jpg extension (even though it's PNG data, for testing extension handling) writer, err = zipWriter.Create("cover.jpg") if err != nil { return err } if _, err := writer.Write(coverPNG); err != nil { return err } // Add a page image writer, err = zipWriter.Create("page001.jpg") if err != nil { return err } if _, err := writer.Write(coverPNG); err != nil { return err } return nil } // createTestCBZWithoutMetadata creates a ZIP without ComicInfo.xml func createTestCBZWithoutMetadata(path string) error { file, err := os.Create(path) if err != nil { return err } defer file.Close() zipWriter := zip.NewWriter(file) defer zipWriter.Close() // Create a simple valid PNG image img := image.NewRGBA(image.Rect(0, 0, 1, 1)) img.Set(0, 0, color.RGBA{0, 0, 255, 255}) var pngBuf bytes.Buffer if err := png.Encode(&pngBuf, img); err != nil { return err } coverPNG := pngBuf.Bytes() writer, err := zipWriter.Create("cover.jpg") if err != nil { return err } if _, err := writer.Write(coverPNG); err != nil { return err } return nil } // createTestCBT creates a test TAR archive func createTestCBT(path string) error { file, err := os.Create(path) if err != nil { return err } defer file.Close() tarWriter := tar.NewWriter(file) defer tarWriter.Close() // Add ComicInfo.xml comicInfo := ComicInfo{ Title: "Test Tar Comic", Series: "Test Tar Series", Number: 5, Publisher: "Test Publisher", } xmlData, err := xml.Marshal(comicInfo) if err != nil { return err } header := &tar.Header{ Name: "ComicInfo.xml", Mode: 0644, Size: int64(len(xmlData)), ModTime: time.Now(), } if err := tarWriter.WriteHeader(header); err != nil { return err } if _, err := tarWriter.Write(xmlData); err != nil { return err } // Create a simple valid PNG image img := image.NewRGBA(image.Rect(0, 0, 1, 1)) img.Set(0, 0, color.RGBA{0, 255, 0, 255}) var pngBuf bytes.Buffer if err := png.Encode(&pngBuf, img); err != nil { return err } coverPNG := pngBuf.Bytes() header = &tar.Header{ Name: "cover.jpg", Mode: 0644, Size: int64(len(coverPNG)), ModTime: time.Now(), } if err := tarWriter.WriteHeader(header); err != nil { return err } if _, err := tarWriter.Write(coverPNG); err != nil { return err } return nil } // createTestTarGz creates a test TAR.GZ archive func createTestTarGz(path string) error { // First create the TAR content in memory var tarBuffer bytes.Buffer tarWriter := tar.NewWriter(&tarBuffer) comicInfo := ComicInfo{ Title: "Test Tar Gz Comic", Series: "Test Tar Gz Series", Number: 10, } xmlData, err := xml.Marshal(comicInfo) if err != nil { return err } header := &tar.Header{ Name: "ComicInfo.xml", Mode: 0644, Size: int64(len(xmlData)), ModTime: time.Now(), } if err := tarWriter.WriteHeader(header); err != nil { return err } if _, err := tarWriter.Write(xmlData); err != nil { return err } // Create a simple valid PNG image img := image.NewRGBA(image.Rect(0, 0, 1, 1)) img.Set(0, 0, color.RGBA{255, 255, 0, 255}) var pngBuf bytes.Buffer if err := png.Encode(&pngBuf, img); err != nil { return err } coverPNG := pngBuf.Bytes() header = &tar.Header{ Name: "cover.jpg", Mode: 0644, Size: int64(len(coverPNG)), ModTime: time.Now(), } if err := tarWriter.WriteHeader(header); err != nil { return err } if _, err := tarWriter.Write(coverPNG); err != nil { return err } tarWriter.Close() // Now compress the TAR with gzip file, err := os.Create(path) if err != nil { return err } defer file.Close() gzWriter := gzip.NewWriter(file) defer gzWriter.Close() if _, err := gzWriter.Write(tarBuffer.Bytes()); err != nil { return err } return nil } // Example usage func Example_extractComicMetadata() { // Extract metadata from a comic archive comicInfo, coverImage, err := extractComicMetadata("/path/to/comic.cbz") if err != nil { fmt.Printf("Error: %v\n", err) return } fmt.Printf("Title: %s\n", comicInfo.Title) fmt.Printf("Series: %s\n", comicInfo.Series) fmt.Printf("Issue: %d\n", comicInfo.Number) fmt.Printf("Cover size: %d bytes\n", len(coverImage)) } func TestNormalizeMangaType(t *testing.T) { tests := []struct { name string input string expected string }{ {"Unknown", "Unknown", "unknown"}, {"No", "No", "no"}, {"Yes", "Yes", "yes"}, {"YesAndRightToLeft", "YesAndRightToLeft", "yes_and_right_to_left"}, {"Lowercase yesandrighttoleft", "yesandrighttoleft", "yes_and_right_to_left"}, {"With spaces", "Yes And Right To Left", "yes_and_right_to_left"}, {"Invalid", "invalid", "unknown"}, {"Empty", "", "unknown"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := normalizeMangaType(tt.input) if result != tt.expected { t.Errorf("normalizeMangaType(%q) = %q; want %q", tt.input, result, tt.expected) } }) } } func TestDetermineReadingDirection(t *testing.T) { tests := []struct { name string manga string language string tags string genre string expected string }{ {"Explicit RTL", "YesAndRightToLeft", "en", "", "", "rtl"}, {"Explicit LTR (Yes)", "Yes", "ja", "", "", "ltr"}, {"Explicit LTR (No)", "No", "en", "", "", "ltr"}, {"Japanese heuristic", "Unknown", "ja", "", "", "rtl"}, {"Japanese with full code", "Unknown", "jpn", "", "", "rtl"}, {"Webtoon Korean", "Unknown", "ko", "Webtoon", "", "vertical"}, {"Manhwa in tags", "Unknown", "ko", "", "Manhwa", "vertical"}, {"Manga + Japanese", "Unknown", "ja", "Manga", "", "rtl"}, {"Western default", "Unknown", "en", "", "", "ltr"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { comicInfo := &ComicInfo{ Manga: tt.manga, LanguageISO: tt.language, Tags: tt.tags, Genre: tt.genre, } result := determineReadingDirection(comicInfo) if result != tt.expected { t.Errorf("determineReadingDirection() = %q; want %q", result, tt.expected) } }) } }