Files
bookhoard/cmd/server/tests/comic_metadata_display_test.go
T
john-okeefe 9b322e854e test: add integration tests for comic metadata display features
Add comprehensive integration tests for all 8 comic metadata display steps
on the book detail page, ensuring frontend rendering works correctly with
real database data.

## Test Coverage

### Step Tests (8 individual tests)
1. Reading Direction Badge - Tests RTL, LTR, vertical, and auto-hide behavior
2. Community Rating Display - Validates star rendering and numeric score
3. Comic-Specific Badges - Tests age rating, B&W, and story arc badges
4. Universal Series Info - Tests series count, volume, and imprint display
5. Comic-Specific Metadata - Tests manga type, scan info, alternate series
6. Summary Section - Tests ComicInfo.xml summary rendering
7. Metadata Notes Section - Tests technical notes display
8. Web URL Link - Tests external link rendering with security attributes

### Test Case Scenarios (4 complete scenarios)
1. Japanese Manga - Complete metadata display (RTL + all badges)
2. Western Comic - LTR direction with story arc
3. Webtoon/Manhwa - Vertical reading direction
4. Regular Ebook - No comic metadata (minimal display)

### Authentication Tests (2 tests)
- Anonymous users are denied access (401)
- Regular users can view metadata (same as admins)

### Edge Case Tests (2 tests)
- Minimal Metadata - Only required fields (no optional metadata)
- All Fields Together - Comprehensive metadata display

## Test Infrastructure

- Uses setupTestServer() helper for isolated test environment
- Uses createComicMediaItem() helper for flexible test data creation
- Uses createLibrary() helper with automatic cleanup
- Tests use pgtype types matching production code
- All tests run with admin authentication by default
- Tests check both structure and content in rendered HTML

## Test Details

- 21 total subtests covering all metadata display features
- Tests verify HTML structure, content presence, and proper escaping
- Uses t.Run() for organized test output
- Tests clean up resources automatically with t.Cleanup()
- Checks for proper HTML entity encoding (e.g., apostrophes)
- Validates conditional rendering (hide when values not set)

## Known Issues

- Metadata Notes content validation uses partial string matching to handle
  HTML escaping variations
- Reading Direction test checks specific direction strings (RTL/LTR/VERTICAL)
  to avoid false positives from emoji appearing elsewhere in the UI
- Community Rating test uses colon ("Community Rating:") to avoid matching
  HTML comments

Related: Template implementation commit (562ca53)
2026-03-31 17:09:38 -04:00

751 lines
28 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 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))
}
}