Files
bookhoard/cmd/server/tests/comic_metadata_display_test.go
T
john-okeefe a6700f73e0 fix(tests): handle all Close() and Decode() errors across integration tests
Replace all unhandled resp.Body.Close() calls throughout the test suite:

- Deferred calls: replace 'defer VAR.Body.Close()' with a closure that explicitly
  discards the error via 'defer func(Body io.ReadCloser) { _ = Body.Close() }(VAR.Body)'
- Immediate calls: replace 'VAR.Body.Close()' with '_ = VAR.Body.Close()'

Replace all unhandled json.NewDecoder(VAR.Body).Decode(&x) calls with error capture
and require.NoError assertion. Files using httptest.ResponseRecorder (collections_preview,
processing_issues) use 'err :=' declaration; suite-style tests (scanner_integration,
dashboard_integration) use s.T() instead of t.
2026-04-21 20:33:05 -04:00

799 lines
29 KiB
Go

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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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 func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
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))
}
}