diff --git a/cmd/server/tests/comic_metadata_display_test.go b/cmd/server/tests/comic_metadata_display_test.go new file mode 100644 index 0000000..52a5fe2 --- /dev/null +++ b/cmd/server/tests/comic_metadata_display_test.go @@ -0,0 +1,750 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "testing" + + "bookhoard/internal/database" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestComicMetadataDisplay_ComicDetailPage tests that comic metadata fields display correctly on the book detail page +func TestComicMetadataDisplay_ComicDetailPage(t *testing.T) { + setup := setupTestServer(t) + client := &http.Client{} + + ctx := context.Background() + + // Create a test comics library + createLibrary := func(t *testing.T, name, libraryType string) uuid.UUID { + t.Helper() + + // Get the library_type_id for the specified type + libraryTypeRow, err := setup.DB.GetLibraryTypeByName(ctx, libraryType) + require.NoError(t, err, "Should find library type") + + adminUUID := getTestUserID(t, setup.DB) + + library, err := setup.DB.CreateLibrary(ctx, database.CreateLibraryParams{ + Name: name, + Description: pgtype.Text{String: "Test library description", Valid: true}, + LibraryTypeID: libraryTypeRow.ID, + CreatedByAdminID: pgtype.UUID{Bytes: adminUUID, Valid: true}, + }) + require.NoError(t, err, "Should create library") + + libraryUUID, err := uuid.FromBytes(library.ID.Bytes[0:16]) + require.NoError(t, err, "Should parse library UUID") + + t.Cleanup(func() { + setup.DB.DeleteLibrary(ctx, library.ID) + }) + + return libraryUUID + } + + libraryID := createLibrary(t, "Comic Metadata Display Test Library", "comics") + parsedLibraryID := libraryID + + // Helper function to create a test media item with specific metadata + createComicMediaItem := func(t *testing.T, title string, metadata map[string]interface{}) string { + t.Helper() + + // Build media item params + params := database.CreateMediaItemParams{ + LibraryID: pgtype.UUID{Bytes: parsedLibraryID, Valid: true}, + Title: title, + FilePath: "/test/" + title + ".cbz", + FileSize: pgtype.Int8{Int64: 1024000, Valid: true}, + MimeType: pgtype.Text{String: "application/x-cbz", Valid: true}, + MangaType: pgtype.Text{String: "unknown", Valid: true}, + ReadingDirection: pgtype.Text{String: "auto", Valid: true}, + } + + // Apply metadata overrides + if v, ok := metadata["manga_type"].(string); ok { + params.MangaType = pgtype.Text{String: v, Valid: true} + } + if v, ok := metadata["reading_direction"].(string); ok { + params.ReadingDirection = pgtype.Text{String: v, Valid: true} + } + if v, ok := metadata["series_count"].(int32); ok { + params.SeriesCount = pgtype.Int4{Int32: v, Valid: true} + } + if v, ok := metadata["volume"].(int32); ok { + params.Volume = pgtype.Int4{Int32: v, Valid: true} + } + if v, ok := metadata["imprint"].(string); ok { + params.Imprint = pgtype.Text{String: v, Valid: true} + } + if v, ok := metadata["age_rating"].(string); ok { + params.AgeRating = pgtype.Text{String: v, Valid: true} + } + if v, ok := metadata["web_url"].(string); ok { + params.WebUrl = pgtype.Text{String: v, Valid: true} + } + if v, ok := metadata["community_rating"].(float64); ok { + params.CommunityRating = pgtype.Float8{Float64: v, Valid: true} + } + if v, ok := metadata["story_arc"].(string); ok { + params.StoryArc = pgtype.Text{String: v, Valid: true} + } + if v, ok := metadata["is_black_and_white"].(bool); ok { + params.IsBlackAndWhite = pgtype.Bool{Bool: v, Valid: true} + } + if v, ok := metadata["scan_information"].(string); ok { + params.ScanInformation = pgtype.Text{String: v, Valid: true} + } + if v, ok := metadata["summary"].(string); ok { + params.Summary = pgtype.Text{String: v, Valid: true} + } + if v, ok := metadata["metadata_notes"].(string); ok { + params.MetadataNotes = pgtype.Text{String: v, Valid: true} + } + if v, ok := metadata["alternate_info"].(string); ok { + params.AlternateInfo = []byte(v) + } + + mediaItem, err := setup.DB.CreateMediaItem(ctx, params) + require.NoError(t, err, "Should create media item") + + mediaUUID, err := uuid.FromBytes(mediaItem.ID.Bytes[0:16]) + require.NoError(t, err, "Should parse media UUID") + + return mediaUUID.String() + } + + t.Run("Step 1: Reading Direction Badge - RTL Manga", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "RTL Manga Test", map[string]interface{}{ + "manga_type": "yes_and_right_to_left", + "reading_direction": "rtl", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // Verify reading direction badge is displayed + assert.Contains(t, body, "📖", "Should display reading direction icon") + assert.Contains(t, body, "RTL", "Should display RTL reading direction") + }) + + t.Run("Step 1: Reading Direction Badge - Western Comic (LTR)", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Western Comic Test", map[string]interface{}{ + "manga_type": "no", + "reading_direction": "ltr", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "LTR", "Should display LTR reading direction") + }) + + t.Run("Step 1: Reading Direction Badge - Hidden when auto", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Auto Direction Test", map[string]interface{}{ + "reading_direction": "auto", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // When reading direction is "auto", the badge should not be displayed + // Check for specific badge text instead of emoji (which appears elsewhere) + assert.NotContains(t, body, "RTL", "Should not display RTL badge when auto") + assert.NotContains(t, body, "LTR", "Should not display LTR badge when auto") + assert.NotContains(t, body, "VERTICAL", "Should not display VERTICAL badge when auto") + }) + + t.Run("Step 2: Community Rating Display", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Community Rating Test", map[string]interface{}{ + "community_rating": 8.5, + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // Verify community rating is displayed + assert.Contains(t, body, "Community Rating", "Should display community rating label") + assert.Contains(t, body, "8.5 / 10", "Should display formatted rating out of 10") + assert.Contains(t, body, "★", "Should render star rating") + }) + + t.Run("Step 3: Comic-Specific Badges - Age Rating", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Age Rating Test", map[string]interface{}{ + "age_rating": "Teen", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "Teen", "Should display age rating badge") + }) + + t.Run("Step 3: Comic-Specific Badges - Black and White", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "B&W Test", map[string]interface{}{ + "is_black_and_white": true, + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "B&W", "Should display black and white badge") + }) + + t.Run("Step 3: Comic-Specific Badges - Story Arc", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Story Arc Test", map[string]interface{}{ + "story_arc": "The Dark Phoenix Saga", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "📚", "Should display story arc icon") + assert.Contains(t, body, "The Dark Phoenix Saga", "Should display story arc name") + }) + + t.Run("Step 4: Universal Series Info - Series Count", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Series Count Test", map[string]interface{}{ + "series_count": int32(12), + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "Series Count", "Should display series count label") + assert.Contains(t, body, "12 items", "Should display series count") + }) + + t.Run("Step 4: Universal Series Info - Volume", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Volume Test", map[string]interface{}{ + "volume": int32(3), + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "Volume", "Should display volume label") + assert.Contains(t, body, "Vol. 3", "Should display volume number") + }) + + t.Run("Step 4: Universal Series Info - Imprint", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Imprint Test", map[string]interface{}{ + "imprint": "Vertigo", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "Imprint", "Should display imprint label") + assert.Contains(t, body, "Vertigo", "Should display imprint name") + }) + + t.Run("Step 5: Comic-Specific Metadata - Manga Type", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Manga Type Test", map[string]interface{}{ + "manga_type": "yes_and_right_to_left", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "Manga Type", "Should display manga type label") + assert.Contains(t, body, "yes and right to left", "Should display formatted manga type") + }) + + t.Run("Step 5: Comic-Specific Metadata - Scan Information", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Scan Info Test", map[string]interface{}{ + "scan_information": "Scanned by Minutemen", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "Scan Info", "Should display scan info label") + assert.Contains(t, body, "Scanned by Minutemen", "Should display scan information") + }) + + t.Run("Step 5: Comic-Specific Metadata - Alternate Series", func(t *testing.T) { + alternateInfo := `{"alternate_series":"Ultimate X-Men","alternate_number":1,"alternate_count":12}` + mediaUUID := createComicMediaItem(t, "Alternate Series Test", map[string]interface{}{ + "alternate_info": alternateInfo, + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "Alternate Series", "Should display alternate series label") + assert.Contains(t, body, "Ultimate X-Men", "Should display alternate series name") + }) + + t.Run("Step 6: Summary Section - Different from description", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Summary Test", map[string]interface{}{ + "summary": "Professor X creates mutant team", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "Comic Summary", "Should display comic summary section") + assert.Contains(t, body, "Professor X creates mutant team", "Should display summary content") + }) + + t.Run("Step 7: Metadata Notes Section", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Metadata Notes Test", map[string]interface{}{ + "metadata_notes": "From collection: John's Comics", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // DEBUG: Print the actual HTML to see what we're dealing with + t.Logf("HTML body length: %d bytes", len(body)) + + // Find the Metadata Notes section in HTML + startIdx := strings.Index(body, "Metadata Notes") + if startIdx > 0 { + // Print 500 characters around the Metadata Notes section + endIdx := startIdx + 500 + if endIdx > len(body) { + endIdx = len(body) + } + if startIdx > 250 { + t.Logf("Metadata Notes section (context):\n%s", body[startIdx-250:startIdx+250]) + } else { + t.Logf("Metadata Notes section (context):\n%s", body[0:endIdx]) + } + } else { + t.Logf("WARNING: 'Metadata Notes' string not found in HTML") + // Print first 1000 chars to see what we got + if len(body) > 1000 { + t.Logf("First 1000 chars of HTML:\n%s", body[0:1000]) + } else { + t.Logf("Full HTML:\n%s", body) + } + } + + // Check that the metadata notes section is present + assert.Contains(t, body, "Metadata Notes", "Should display metadata notes section header") + + // The content is HTML-escaped by templ, so apostrophes become ' + assert.Contains(t, body, "John's Comics", "Should display HTML-escaped metadata notes content") + // Also check for a simpler substring without special characters + assert.Contains(t, body, "collection", "Should display metadata notes content") + }) + + t.Run("Step 8: Web URL Link - External Link", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Web URL Test", map[string]interface{}{ + "web_url": "https://comicvine.gamespot.com/batman-year-one/1", + "isbn": "9781234567890", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + assert.Contains(t, body, "comicvine.gamespot.com", "Should display web URL domain") + assert.Contains(t, body, "https://comicvine.gamespot.com/batman-year-one/1", "Should include full URL in href") + assert.Contains(t, body, "target=\"_blank\"", "Should open in new tab") + assert.Contains(t, body, "rel=\"noopener noreferrer\"", "Should have security attributes") + }) + + t.Run("Test Case 1: Japanese Manga - Complete Display", func(t *testing.T) { + alternateInfo := `{"alternate_series":"Ultimate X-Men","alternate_number":1,"alternate_count":12}` + mediaUUID := createComicMediaItem(t, "Japanese Manga Complete", map[string]interface{}{ + "manga_type": "yes_and_right_to_left", + "reading_direction": "rtl", + "series_count": int32(12), + "age_rating": "Teen", + "community_rating": 8.5, + "story_arc": "The Dark Phoenix Saga", + "alternate_info": alternateInfo, + "scan_information": "Scanned by Minutemen", + "imprint": "Shonen Jump", + "is_black_and_white": false, + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // Verify all manga-specific fields are displayed + assert.Contains(t, body, "RTL", "Should display RTL reading direction") + assert.Contains(t, body, "Teen", "Should display age rating") + assert.Contains(t, body, "8.5 / 10", "Should display community rating") + assert.Contains(t, body, "The Dark Phoenix Saga", "Should display story arc") + assert.Contains(t, body, "Ultimate X-Men", "Should display alternate series") + assert.Contains(t, body, "Shonen Jump", "Should display imprint") + }) + + t.Run("Test Case 2: Western Comic - Complete Display", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Western Comic Complete", map[string]interface{}{ + "manga_type": "no", + "reading_direction": "ltr", + "age_rating": "Mature", + "community_rating": 9.2, + "story_arc": "Batman: Year One", + "series_count": int32(4), + "volume": int32(1), + "imprint": "Vertigo", + "is_black_and_white": true, + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // Verify Western comic fields + assert.Contains(t, body, "LTR", "Should display LTR reading direction") + assert.Contains(t, body, "Mature", "Should display age rating") + assert.Contains(t, body, "B&W", "Should display black and white badge") + assert.Contains(t, body, "Batman: Year One", "Should display story arc") + assert.Contains(t, body, "Vol. 1", "Should display volume") + assert.Contains(t, body, "Vertigo", "Should display imprint") + }) + + t.Run("Test Case 3: Webtoon/Manhwa - Complete Display", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Webtoon Complete", map[string]interface{}{ + "manga_type": "unknown", + "reading_direction": "vertical", + "age_rating": "Teen", + "community_rating": 7.8, + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // Verify webtoon fields + assert.Contains(t, body, "VERTICAL", "Should display vertical reading direction") + assert.Contains(t, body, "Teen", "Should display age rating") + }) + + t.Run("Test Case 4: Regular Ebook - No Comic Metadata", func(t *testing.T) { + // Create ebook library for this test + ebookLibraryID := createLibrary(t, "Ebook Test Library", "ebooks") + parsedEbookLibraryID := ebookLibraryID + + // Create ebook with only universal metadata + params := database.CreateMediaItemParams{ + LibraryID: pgtype.UUID{Bytes: parsedEbookLibraryID, Valid: true}, + Title: "Regular Ebook", + FilePath: "/test/book.epub", + FileSize: pgtype.Int8{Int64: 512000, Valid: true}, + MimeType: pgtype.Text{String: "application/epub+zip", Valid: true}, + 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://goodreads.com/book", Valid: true}, + CommunityRating: pgtype.Float8{Float64: 4.5, Valid: true}, + Isbn: pgtype.Text{String: "9781234567890", Valid: true}, + } + + mediaItem, err := setup.DB.CreateMediaItem(ctx, params) + require.NoError(t, err, "Should create ebook") + + mediaUUID, err := uuid.FromBytes(mediaItem.ID.Bytes[0:16]) + require.NoError(t, err, "Should parse media UUID") + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID.String(), nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // Verify universal fields are displayed + assert.Contains(t, body, "7 items", "Should display series count") + assert.Contains(t, body, "Vol. 1", "Should display volume") + assert.Contains(t, body, "HarperCollins", "Should display imprint") + assert.Contains(t, body, "Everyone", "Should display age rating") + assert.Contains(t, body, "goodreads.com", "Should display web URL") + + // Verify comic-specific fields are NOT displayed + assert.NotContains(t, body, "Manga Type", "Should not display manga type for ebook") + assert.NotContains(t, body, "Scan Info", "Should not display scan info for ebook") + assert.NotContains(t, body, "B&W", "Should not display B&W badge for ebook") + }) + + t.Run("Authentication Required", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Auth Test", map[string]interface{}{ + "age_rating": "Teen", + }) + + // Test without authentication + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "Should require authentication") + }) + + t.Run("Regular User Can View Metadata", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "User View Test", map[string]interface{}{ + "manga_type": "yes_and_right_to_left", + "reading_direction": "rtl", + "age_rating": "Teen", + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.RegularToken) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // Regular users should see all metadata + assert.Contains(t, body, "RTL", "Regular user should see reading direction") + assert.Contains(t, body, "Teen", "Regular user should see age rating") + }) + + t.Run("Minimal Metadata - Only Required Fields", func(t *testing.T) { + mediaUUID := createComicMediaItem(t, "Minimal Comic", map[string]interface{}{ + // No optional metadata set + }) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID, nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // Page should still render successfully + assert.Contains(t, body, "Minimal Comic", "Should display title") + // Use "Community Rating:" with colon to avoid matching the HTML comment + assert.NotContains(t, body, "Community Rating:", "Should not show community rating when not set") + assert.NotContains(t, body, "Manga Type", "Should not show manga type when unknown") + }) +} + +// readBody is a helper to read response body +func readBody(resp *http.Response) string { + body, _ := io.ReadAll(resp.Body) + return string(body) +} + +// TestComicMetadataDisplay_AllFieldsTogether tests all metadata fields together +func TestComicMetadataDisplay_AllFieldsTogether(t *testing.T) { + setup := setupTestServer(t) + client := &http.Client{} + + ctx := context.Background() + var err error + + // Helper function to create library + createLibrary := func(t *testing.T, name, libraryType string) uuid.UUID { + t.Helper() + + libraryTypeRow, err := setup.DB.GetLibraryTypeByName(ctx, libraryType) + require.NoError(t, err, "Should find library type") + + adminUUID := getTestUserID(t, setup.DB) + + library, err := setup.DB.CreateLibrary(ctx, database.CreateLibraryParams{ + Name: name, + Description: pgtype.Text{String: "Test library description", Valid: true}, + LibraryTypeID: libraryTypeRow.ID, + CreatedByAdminID: pgtype.UUID{Bytes: adminUUID, Valid: true}, + }) + require.NoError(t, err, "Should create library") + + libraryUUID, err := uuid.FromBytes(library.ID.Bytes[0:16]) + require.NoError(t, err, "Should parse library UUID") + + t.Cleanup(func() { + setup.DB.DeleteLibrary(ctx, library.ID) + }) + + return libraryUUID + } + + libraryID := createLibrary(t, "Complete Metadata Test Library", "comics") + parsedLibraryID := libraryID + + // Create a media item with ALL metadata fields populated + alternateInfoStr := `{"alternate_series":"Ultimate X-Men","alternate_number":1,"alternate_count":12}` + var mediaItem database.MediaItems + mediaItem, err = setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{ + LibraryID: pgtype.UUID{Bytes: parsedLibraryID, Valid: true}, + Title: "Complete Metadata Comic", + FilePath: "/test/complete.cbz", + FileSize: pgtype.Int8{Int64: 2048000, Valid: true}, + MimeType: pgtype.Text{String: "application/x-cbz", Valid: true}, + 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}, + Imprint: pgtype.Text{String: "Marvel", Valid: true}, + AgeRating: pgtype.Text{String: "Teen", Valid: true}, + WebUrl: pgtype.Text{String: "https://marvel.com/comics/ultimate-xmen", Valid: true}, + CommunityRating: pgtype.Float8{Float64: 8.7, Valid: true}, + StoryArc: pgtype.Text{String: "The Dark Phoenix Saga", Valid: true}, + IsBlackAndWhite: pgtype.Bool{Bool: false, Valid: true}, + ScanInformation: pgtype.Text{String: "Scanned by Minutemen-HDC", Valid: true}, + Summary: pgtype.Text{String: "Professor X creates mutant team to protect humanity", Valid: true}, + MetadataNotes: pgtype.Text{String: "From personal collection", Valid: true}, + AlternateInfo: []byte(alternateInfoStr), + }) + require.NoError(t, err) + + mediaUUID, err := uuid.FromBytes(mediaItem.ID.Bytes[0:16]) + require.NoError(t, err) + + req, _ := http.NewRequest("GET", setup.Server.URL+"/media/"+mediaUUID.String(), nil) + req.Header.Set("Authorization", "Bearer "+setup.Token) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body := readBody(resp) + + // Verify all fields are present in the rendered page + expectedFields := []struct { + field string + expected string + }{ + {"Reading Direction", "RTL"}, + {"Community Rating", "8.7 / 10"}, + {"Age Rating", "Teen"}, + {"Story Arc", "The Dark Phoenix Saga"}, + {"Series Count", "12 items"}, + {"Volume", "Vol. 1"}, + {"Imprint", "Marvel"}, + {"Manga Type", "yes and right to left"}, + {"Scan Info", "Scanned by Minutemen-HDC"}, + {"Alternate Series", "Ultimate X-Men"}, + {"Comic Summary", "Professor X creates mutant team"}, + {"Metadata Notes", "From personal collection"}, + {"Web URL", "marvel.com"}, + } + + for _, field := range expectedFields { + assert.Contains(t, body, field.expected, fmt.Sprintf("Should display %s", field.field)) + } +}