package main import ( "bookhoard/internal/database" "bookhoard/internal/services" "context" "os" "path/filepath" "testing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/require" ) // TestCalibreLibraryScan tests importing books from a Calibre library with metadata.opf sidecar files func TestCalibreLibraryScan(t *testing.T) { setup := setupTestServer(t) defer setup.Close() ctx := context.Background() // Get admin user ID from database adminUser, err := setup.DB.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal") require.NoError(t, err, "Failed to get admin user") adminUUID, err := uuid.FromBytes(adminUser.ID.Bytes[:]) require.NoError(t, err, "Failed to parse admin UUID") adminID := pgtype.UUID{Bytes: adminUUID, Valid: true} // Create test Calibre library structure tmpDir := t.TempDir() // Create author directory authorDir := filepath.Join(tmpDir, "Test Author") require.NoError(t, os.Mkdir(authorDir, 0755), "Failed to create author directory") // Create book directory bookDir := filepath.Join(authorDir, "Test Book") require.NoError(t, os.Mkdir(bookDir, 0755), "Failed to create book directory") // Create metadata.opf with Calibre metadata opfPath := filepath.Join(bookDir, "metadata.opf") opfContent := ` Test Book Test Author Fantasy Adventure Test description Test Publisher 2024-01-15 en 978-0-123456-78-9 Contributor Name ` require.NoError(t, os.WriteFile(opfPath, []byte(opfContent), 0644), "Failed to create metadata.opf") // Create dummy EPUB file epubPath := filepath.Join(bookDir, "Test Book.epub") require.NoError(t, os.WriteFile(epubPath, []byte("dummy epub content"), 0644), "Failed to create EPUB file") // Create library via API libID := createTestLibraryWithFolder(t, setup.Server, setup.Token, "Calibre Test Library", true) // Update the folder path to our temp directory libUUID, err := uuid.Parse(libID) require.NoError(t, err, "Failed to parse library UUID") // Get and delete the default folder, then add our temp directory folders, err := setup.DB.GetLibraryFolders(ctx, pgtype.UUID{Bytes: libUUID, Valid: true}) require.NoError(t, err, "Failed to list library folders") if len(folders) > 0 { _, err = setup.DB.DeleteLibraryFolder(ctx, database.DeleteLibraryFolderParams{ LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, FolderPath: folders[0].FolderPath, }) require.NoError(t, err, "Failed to delete default folder") } _, err = setup.DB.AddLibraryFolder(ctx, database.AddLibraryFolderParams{ LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, FolderPath: tmpDir, }) require.NoError(t, err, "Failed to add folder to library") // Create scanner and configure it scanner := services.NewMediaScanner(setup.DB) scanner.SetAdminID(adminID) err = scanner.SetFolders([]string{tmpDir}, false) require.NoError(t, err, "Failed to set scanner folders") // Scan library err = scanner.ScanFolders(ctx) require.NoError(t, err, "ScanFolders should succeed") // Verify imported book books, err := setup.DB.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: libUUID, Valid: true}) require.NoError(t, err, "ListMediaItemsByLibrary should succeed") require.Len(t, books, 1, "Should have imported 1 book") book := books[0] // Verify metadata from sidecar require.Equal(t, "Test Book", book.Title, "Title should match sidecar") require.Equal(t, "Test Author", book.Author.String, "Author should match sidecar") require.Equal(t, "Test Series", book.Series.String, "Series should match sidecar") require.Equal(t, int32(1), book.SeriesNumber.Int32, "Series number should match sidecar") require.Equal(t, "Test Publisher", book.Publisher.String, "Publisher should match sidecar") require.Len(t, book.Tags, 2, "Should have 2 tags from sidecar") require.Contains(t, book.Tags, "Fantasy", "Should have Fantasy tag") require.Contains(t, book.Tags, "Adventure", "Should have Adventure tag") } // TestCalibreLibraryScanWithoutSidecar tests that books without metadata.opf still work (backward compatibility) func TestCalibreLibraryScanWithoutSidecar(t *testing.T) { setup := setupTestServer(t) defer setup.Close() ctx := context.Background() // Get admin user ID from database adminUser, err := setup.DB.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal") require.NoError(t, err, "Failed to get admin user") adminUUID, err := uuid.FromBytes(adminUser.ID.Bytes[:]) require.NoError(t, err, "Failed to parse admin UUID") adminID := pgtype.UUID{Bytes: adminUUID, Valid: true} // Create test directory structure (non-Calibre) tmpDir := t.TempDir() // Create book directory bookDir := filepath.Join(tmpDir, "Plain Book") require.NoError(t, os.Mkdir(bookDir, 0755), "Failed to create book directory") // Create EPUB file WITHOUT metadata.opf sidecar epubPath := filepath.Join(bookDir, "Plain Book.epub") require.NoError(t, os.WriteFile(epubPath, []byte("dummy epub content"), 0644), "Failed to create EPUB file") // Create library via API libID := createTestLibraryWithFolder(t, setup.Server, setup.Token, "Non-Calibre Test Library", true) // Update the folder path to our temp directory libUUID, err := uuid.Parse(libID) require.NoError(t, err, "Failed to parse library UUID") // Get and delete the default folder, then add our temp directory folders, err := setup.DB.GetLibraryFolders(ctx, pgtype.UUID{Bytes: libUUID, Valid: true}) require.NoError(t, err, "Failed to list library folders") if len(folders) > 0 { _, err = setup.DB.DeleteLibraryFolder(ctx, database.DeleteLibraryFolderParams{ LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, FolderPath: folders[0].FolderPath, }) require.NoError(t, err, "Failed to delete default folder") } _, err = setup.DB.AddLibraryFolder(ctx, database.AddLibraryFolderParams{ LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, FolderPath: tmpDir, }) require.NoError(t, err, "Failed to add folder to library") // Create scanner and configure it scanner := services.NewMediaScanner(setup.DB) scanner.SetAdminID(adminID) err = scanner.SetFolders([]string{tmpDir}, false) require.NoError(t, err, "Failed to set scanner folders") // Scan library err = scanner.ScanFolders(ctx) require.NoError(t, err, "ScanFolders should succeed") // Verify imported book (using fallback metadata) books, err := setup.DB.ListMediaItemsByLibrary(ctx, pgtype.UUID{Bytes: libUUID, Valid: true}) require.NoError(t, err, "ListMediaItemsByLibrary should succeed") require.Len(t, books, 1, "Should have imported 1 book") book := books[0] // Verify fallback metadata (from filename) require.Equal(t, "Plain Book", book.Title, "Title should fallback to filename") }