Design correction discovered by the auth chicken-and-egg: the decorator-level refusal was re-created a convention gate (plan B thinking). ScopedDB now NEVER refuses — it declares the GUC when the context carries an identity and declares nothing otherwise; the ENGINE is the fail-loud layer (policies raise on governed tables, ungoverned tables like auth lookups simply work). rls.WithInternal removed as a dead concept. - search service's raw DBTX path now receives the ScopedDB itself (main.go + harness) — a bare pool bypassed the GUC. - database/schema.go: migration tx declares a synthetic identity (schema.sql's dedup DO block SELECTs media_items; with policies live, boot 2 raised 'unrecognized configuration parameter' — A11). - harness: TestServerSetup.DB is a raw-pool SEED handle (write policies permissive, other seeded tables ungoverned); the server under test keeps the scoped wiring. 33 CreateMediaItem sites use seedCtx() — a seed identity with no visibility rows — because RETURNING requires SELECT visibility of the new row. - invariant test: regex UUID-scraping replaced with a structured JSON decode of the sections payload. INTERRUPTION NOTE: an edit to this same effect (seedCtx) was applied, confirmed by the tool, and subsequently vanished from disk while an 'opencode serve' process (PID 84551) is alive on the machine. This commit pins the tree against further loss; implementation is PAUSED pending the user killing that process.
295 lines
11 KiB
Go
295 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"bookhoard/internal/database"
|
|
|
|
"github.com/google/uuid"
|
|
"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()
|
|
|
|
t.Run("CBZ with RTL manga", func(t *testing.T) {
|
|
libraryID := setup.CreateLibrary(t, "RTL Manga Test Library", "comics")
|
|
parsedLibraryID, err := uuid.Parse(libraryID)
|
|
require.NoError(t, err, "Should parse library UUID")
|
|
|
|
// Insert test media item with full comic metadata
|
|
_, err = setup.DB.CreateMediaItem(seedCtx(), database.CreateMediaItemParams{
|
|
LibraryID: pgtype.UUID{Bytes: parsedLibraryID, Valid: true},
|
|
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.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: parsedLibraryID, Valid: true})
|
|
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) {
|
|
libraryID := setup.CreateLibrary(t, "Western Comic Test Library", "comics")
|
|
parsedLibraryID, err := uuid.Parse(libraryID)
|
|
require.NoError(t, err, "Should parse library UUID")
|
|
|
|
_, err = setup.DB.CreateMediaItem(seedCtx(), database.CreateMediaItemParams{
|
|
LibraryID: pgtype.UUID{Bytes: parsedLibraryID, Valid: true},
|
|
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.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: parsedLibraryID, Valid: true})
|
|
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) {
|
|
libraryID := setup.CreateLibrary(t, "Minimal Metadata Test Library", "comics")
|
|
parsedLibraryID, err := uuid.Parse(libraryID)
|
|
require.NoError(t, err, "Should parse library UUID")
|
|
|
|
_, err = setup.DB.CreateMediaItem(seedCtx(), database.CreateMediaItemParams{
|
|
LibraryID: pgtype.UUID{Bytes: parsedLibraryID, Valid: true},
|
|
Title: "Minimal Comic",
|
|
FilePath: "/test/minimal.cbz",
|
|
MangaType: pgtype.Text{String: "unknown", Valid: true},
|
|
ReadingDirection: pgtype.Text{String: "auto", Valid: true},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
items, err := setup.DB.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: parsedLibraryID, Valid: true})
|
|
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", "comics")
|
|
parsedLibraryID, err := uuid.Parse(libraryID)
|
|
require.NoError(t, err, "Should parse library UUID")
|
|
|
|
// 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(seedCtx(), database.CreateMediaItemParams{
|
|
LibraryID: pgtype.UUID{Bytes: parsedLibraryID, Valid: true},
|
|
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.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: parsedLibraryID, Valid: true})
|
|
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.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: parsedLibraryID, Valid: true})
|
|
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.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: parsedLibraryID, Valid: true})
|
|
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 Test Library", "comics")
|
|
ebookLibraryID := setup.CreateLibrary(t, "Ebook Test Library", "ebooks")
|
|
parsedComicLibraryID, err := uuid.Parse(comicLibraryID)
|
|
require.NoError(t, err, "Should parse comic library UUID")
|
|
parsedEbookLibraryID, err := uuid.Parse(ebookLibraryID)
|
|
require.NoError(t, err, "Should parse ebook library UUID")
|
|
|
|
t.Run("Comic with universal fields", func(t *testing.T) {
|
|
_, err := setup.DB.CreateMediaItem(seedCtx(), database.CreateMediaItemParams{
|
|
LibraryID: pgtype.UUID{Bytes: parsedComicLibraryID, Valid: true},
|
|
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.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: parsedComicLibraryID, Valid: true})
|
|
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(seedCtx(), database.CreateMediaItemParams{
|
|
LibraryID: pgtype.UUID{Bytes: parsedEbookLibraryID, Valid: true},
|
|
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.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: parsedEbookLibraryID, Valid: true})
|
|
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 Test Library", "comics")
|
|
parsedLibraryID, err := uuid.Parse(libraryID)
|
|
require.NoError(t, err, "Should parse library UUID")
|
|
|
|
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(seedCtx(), database.CreateMediaItemParams{
|
|
LibraryID: pgtype.UUID{Bytes: parsedLibraryID, Valid: true},
|
|
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.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: parsedLibraryID, Valid: true})
|
|
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)
|
|
})
|
|
}
|