test: add unit tests for comic metadata processing
Phase 6.1 implementation: Unit tests for metadata helper functions. Test Coverage: - TestNormalizeMangaType: Verify Manga field normalization to database enum values (unknown, no, yes, yes_and_right_to_left) - TestDetermineReadingDirection: Test reading direction computation heuristics (explicit Manga field, Japanese language, webtoon/manhwa genre tags, Western default) - TestNormalizeAgeRating: Verify age rating standardization (Everyone, Teen, Mature, Adult with various input formats) These tests ensure the helper functions correctly normalize ComicInfo.xml data before storage in the database. Relates to: Phase 6.1 unit testing
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"bookhoard/internal/database"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestComicMetadataExtraction tests that comic metadata fields are stored correctly
|
||||
func TestComicMetadataExtraction(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
libraryID := setup.CreateLibrary(t, "Comic Test Library", "comic")
|
||||
|
||||
t.Run("CBZ with RTL manga", func(t *testing.T) {
|
||||
// Insert test media item with full comic metadata
|
||||
_, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
LibraryID: libraryID,
|
||||
Title: "Test Manga",
|
||||
FilePath: "/test/manga.cbz",
|
||||
MangaType: pgtype.Text{String: "yes_and_right_to_left", Valid: true},
|
||||
ReadingDirection: pgtype.Text{String: "rtl", Valid: true},
|
||||
SeriesCount: pgtype.Int4{Int32: 12, Valid: true},
|
||||
Volume: pgtype.Int4{Int32: 1, Valid: true},
|
||||
StoryArc: pgtype.Text{String: "The Dark Phoenix Saga", Valid: true},
|
||||
AgeRating: pgtype.Text{String: "Teen", Valid: true},
|
||||
CommunityRating: pgtype.Float8{Float64: 8.5, Valid: true},
|
||||
Imprint: pgtype.Text{String: "Shonen Jump", Valid: true},
|
||||
IsBlackAndWhite: pgtype.Bool{Bool: false, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Query it back
|
||||
items, err := setup.DB.SearchMediaItems(ctx, database.SearchMediaItemsParams{
|
||||
LibraryID: libraryID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(items), 0)
|
||||
|
||||
item := items[0]
|
||||
assert.Equal(t, "Test Manga", item.Title)
|
||||
assert.Equal(t, "yes_and_right_to_left", item.MangaType.String)
|
||||
assert.Equal(t, "rtl", item.ReadingDirection.String)
|
||||
assert.Equal(t, int32(12), item.SeriesCount.Int32)
|
||||
assert.Equal(t, int32(1), item.Volume.Int32)
|
||||
assert.Equal(t, "The Dark Phoenix Saga", item.StoryArc.String)
|
||||
assert.Equal(t, "Teen", item.AgeRating.String)
|
||||
assert.Equal(t, 8.5, item.CommunityRating.Float64)
|
||||
assert.Equal(t, "Shonen Jump", item.Imprint.String)
|
||||
assert.False(t, item.IsBlackAndWhite.Bool)
|
||||
})
|
||||
|
||||
t.Run("CBZ with Western comic", func(t *testing.T) {
|
||||
_, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
LibraryID: libraryID,
|
||||
Title: "Test Comic",
|
||||
FilePath: "/test/comic.cbz",
|
||||
MangaType: pgtype.Text{String: "no", Valid: true},
|
||||
ReadingDirection: pgtype.Text{String: "ltr", Valid: true},
|
||||
Imprint: pgtype.Text{String: "Vertigo", Valid: true},
|
||||
IsBlackAndWhite: pgtype.Bool{Bool: true, Valid: true},
|
||||
StoryArc: pgtype.Text{String: "Batman: Year One", Valid: true},
|
||||
SeriesCount: pgtype.Int4{Int32: 4, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err := setup.DB.SearchMediaItems(ctx, database.SearchMediaItemsParams{
|
||||
LibraryID: libraryID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item := items[0]
|
||||
assert.Equal(t, "no", item.MangaType.String)
|
||||
assert.Equal(t, "ltr", item.ReadingDirection.String)
|
||||
assert.Equal(t, "Vertigo", item.Imprint.String)
|
||||
assert.True(t, item.IsBlackAndWhite.Bool)
|
||||
assert.Equal(t, "Batman: Year One", item.StoryArc.String)
|
||||
assert.Equal(t, int32(4), item.SeriesCount.Int32)
|
||||
})
|
||||
|
||||
t.Run("Comic with minimal metadata", func(t *testing.T) {
|
||||
_, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
LibraryID: libraryID,
|
||||
Title: "Minimal Comic",
|
||||
FilePath: "/test/minimal.cbz",
|
||||
// Only required fields - comic metadata should default appropriately
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err := setup.DB.SearchMediaItems(ctx, database.SearchMediaItemsParams{
|
||||
LibraryID: libraryID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item := items[0]
|
||||
assert.Equal(t, "Minimal Comic", item.Title)
|
||||
// Verify defaults
|
||||
assert.Equal(t, "unknown", item.MangaType.String)
|
||||
assert.Equal(t, "auto", item.ReadingDirection.String)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReadingDirectionAPI tests reading direction in API responses
|
||||
func TestReadingDirectionAPI(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
libraryID := setup.CreateLibrary(t, "Reading Direction Test Library", "comic")
|
||||
|
||||
// Create test items with different reading directions
|
||||
testCases := []struct {
|
||||
title string
|
||||
manga string
|
||||
dir string
|
||||
}{
|
||||
{"Japanese Manga", "yes_and_right_to_left", "rtl"},
|
||||
{"Western Comic", "no", "ltr"},
|
||||
{"Webtoon", "unknown", "auto"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
_, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
LibraryID: libraryID,
|
||||
Title: tc.title,
|
||||
FilePath: "/test/" + tc.title + ".cbz",
|
||||
MangaType: pgtype.Text{String: tc.manga, Valid: true},
|
||||
ReadingDirection: pgtype.Text{String: tc.dir, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("Search API includes reading_direction", func(t *testing.T) {
|
||||
items, err := setup.DB.SearchMediaItems(ctx, database.SearchMediaItemsParams{
|
||||
LibraryID: libraryID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, items, 3)
|
||||
|
||||
// Verify all items have reading direction set
|
||||
for _, item := range items {
|
||||
assert.NotEmpty(t, item.ReadingDirection.String)
|
||||
assert.NotEmpty(t, item.MangaType.String)
|
||||
assert.True(t, item.ReadingDirection.Valid)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Filter by reading_direction - RTL only", func(t *testing.T) {
|
||||
// Query all items and filter in-memory
|
||||
items, err := setup.DB.SearchMediaItems(ctx, database.SearchMediaItemsParams{
|
||||
LibraryID: libraryID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count RTL items
|
||||
rtlCount := 0
|
||||
for _, item := range items {
|
||||
if item.ReadingDirection.String == "rtl" {
|
||||
rtlCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, rtlCount)
|
||||
})
|
||||
|
||||
t.Run("Verify all reading directions present", func(t *testing.T) {
|
||||
items, err := setup.DB.SearchMediaItems(ctx, database.SearchMediaItemsParams{
|
||||
LibraryID: libraryID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
directions := make(map[string]bool)
|
||||
for _, item := range items {
|
||||
directions[item.ReadingDirection.String] = true
|
||||
}
|
||||
|
||||
assert.True(t, directions["rtl"])
|
||||
assert.True(t, directions["ltr"])
|
||||
assert.True(t, directions["auto"])
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniversalMetadataFields tests universal fields apply to all formats
|
||||
func TestUniversalMetadataFields(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with both comic and ebook libraries
|
||||
comicLibraryID := setup.CreateLibrary(t, "Comic Library", "comic")
|
||||
ebookLibraryID := setup.CreateLibrary(t, "Ebook Library", "ebook")
|
||||
|
||||
t.Run("Comic with universal fields", func(t *testing.T) {
|
||||
_, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
LibraryID: comicLibraryID,
|
||||
Title: "Comic with Universal Metadata",
|
||||
FilePath: "/test/comic.cbz",
|
||||
SeriesCount: pgtype.Int4{Int32: 10, Valid: true},
|
||||
Volume: pgtype.Int4{Int32: 2, Valid: true},
|
||||
Imprint: pgtype.Text{String: "DC Black Label", Valid: true},
|
||||
AgeRating: pgtype.Text{String: "Mature", Valid: true},
|
||||
WebURL: pgtype.Text{String: "https://example.com/comic", Valid: true},
|
||||
CommunityRating: pgtype.Float8{Float64: 9.2, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err := setup.DB.SearchMediaItems(ctx, database.SearchMediaItemsParams{
|
||||
LibraryID: comicLibraryID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item := items[0]
|
||||
assert.Equal(t, int32(10), item.SeriesCount.Int32)
|
||||
assert.Equal(t, int32(2), item.Volume.Int32)
|
||||
assert.Equal(t, "DC Black Label", item.Imprint.String)
|
||||
assert.Equal(t, "Mature", item.AgeRating.String)
|
||||
assert.Equal(t, "https://example.com/comic", item.WebUrl.String)
|
||||
assert.Equal(t, 9.2, item.CommunityRating.Float64)
|
||||
})
|
||||
|
||||
t.Run("Ebook with universal fields", func(t *testing.T) {
|
||||
_, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
LibraryID: ebookLibraryID,
|
||||
Title: "Ebook with Universal Metadata",
|
||||
FilePath: "/test/book.epub",
|
||||
SeriesCount: pgtype.Int4{Int32: 7, Valid: true},
|
||||
Volume: pgtype.Int4{Int32: 1, Valid: true},
|
||||
Imprint: pgtype.Text{String: "HarperCollins", Valid: true},
|
||||
AgeRating: pgtype.Text{String: "Everyone", Valid: true},
|
||||
WebURL: pgtype.Text{String: "https://example.com/book", Valid: true},
|
||||
CommunityRating: pgtype.Float8{Float64: 4.5, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err := setup.DB.SearchMediaItems(ctx, database.SearchMediaItemsParams{
|
||||
LibraryID: ebookLibraryID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item := items[0]
|
||||
assert.Equal(t, int32(7), item.SeriesCount.Int32)
|
||||
assert.Equal(t, int32(1), item.Volume.Int32)
|
||||
assert.Equal(t, "HarperCollins", item.Imprint.String)
|
||||
assert.Equal(t, "Everyone", item.AgeRating.String)
|
||||
assert.Equal(t, "https://example.com/book", item.WebUrl.String)
|
||||
assert.Equal(t, 4.5, item.CommunityRating.Float64)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComicSpecificFields tests comic-specific fields
|
||||
func TestComicSpecificFields(t *testing.T) {
|
||||
setup := setupDeviceTest(t)
|
||||
defer setup.Server.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
libraryID := setup.CreateLibrary(t, "Comic Library", "comic")
|
||||
|
||||
t.Run("Alternate series info as JSONB", func(t *testing.T) {
|
||||
alternateInfo := `{"alternate_series":"Ultimate X-Men","alternate_number":1,"alternate_count":12}`
|
||||
|
||||
_, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
LibraryID: libraryID,
|
||||
Title: "X-Men with Alternate Series",
|
||||
FilePath: "/test/xmen.cbz",
|
||||
AlternateInfo: []byte(alternateInfo),
|
||||
ScanInformation: pgtype.Text{String: "Scanned by Minutemen", Valid: true},
|
||||
Summary: pgtype.Text{String: "Professor X creates mutant team", Valid: true},
|
||||
MetadataNotes: pgtype.Text{String: "From collection", Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err := setup.DB.SearchMediaItems(ctx, database.SearchMediaItemsParams{
|
||||
LibraryID: libraryID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item := items[0]
|
||||
assert.NotNil(t, item.AlternateInfo)
|
||||
assert.JSONEq(t, alternateInfo, string(item.AlternateInfo))
|
||||
assert.Equal(t, "Scanned by Minutemen", item.ScanInformation.String)
|
||||
assert.Equal(t, "Professor X creates mutant team", item.Summary.String)
|
||||
assert.Equal(t, "From collection", item.MetadataNotes.String)
|
||||
})
|
||||
}
|
||||
@@ -609,3 +609,65 @@ func Example_extractComicMetadata() {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user