diff --git a/cmd/server/tests/dashboard_integration_test.go b/cmd/server/tests/dashboard_integration_test.go index b47d338..9235a7a 100644 --- a/cmd/server/tests/dashboard_integration_test.go +++ b/cmd/server/tests/dashboard_integration_test.go @@ -53,7 +53,7 @@ func (s *DashboardIntegrationTestSuite) TestGetSections_EndToEndFlow() { sections, ok := response["sections"].([]interface{}) require.True(s.T(), ok, "sections should be an array") - require.Len(s.T(), sections, 4, "Should have 4 system collections") + require.Len(s.T(), sections, 5, "Should have 5 system collections") // Verify response structure sectionMap := make(map[string]map[string]interface{}) @@ -74,6 +74,7 @@ func (s *DashboardIntegrationTestSuite) TestGetSections_EndToEndFlow() { assert.Contains(s.T(), sectionMap, "recently-added") assert.Contains(s.T(), sectionMap, "recently-read") assert.Contains(s.T(), sectionMap, "not-started") + assert.Contains(s.T(), sectionMap, "continue-series") // Verify continue-reading is a system collection continueReading := sectionMap["continue-reading"] @@ -226,7 +227,7 @@ func (s *DashboardIntegrationTestSuite) TestRestoreSystemCollection_Unauthorized func (s *DashboardIntegrationTestSuite) TestRestoreSystemCollection_ValidNames() { token := s.setup.Token - validCollections := []string{"Continue Reading", "Recently Added", "Recently Read", "Not Started"} + validCollections := []string{"Continue Reading", "Recently Added", "Recently Read", "Not Started", "Continue Series"} for _, collName := range validCollections { s.T().Run(collName, func(t *testing.T) { diff --git a/cmd/server/tests/series_integration_test.go b/cmd/server/tests/series_integration_test.go new file mode 100644 index 0000000..0b732ae --- /dev/null +++ b/cmd/server/tests/series_integration_test.go @@ -0,0 +1,360 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type SeriesIntegrationTestSuite struct { + suite.Suite + setup *TestServerSetup + token string + libraryID string +} + +func (s *SeriesIntegrationTestSuite) SetupSuite() { + s.setup = setupTestServer(s.T()) + s.token = s.setup.Token + s.libraryID = createTestLibraryWithFolder(s.T(), s.setup.Server, s.token, "Test Series Library", false) +} + +func (s *SeriesIntegrationTestSuite) TearDownSuite() { + s.setup.Close() +} + +func (s *SeriesIntegrationTestSuite) TestGetSeries_RequiresLibraryID() { + req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series", nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode) + + var body map[string]string + err = json.NewDecoder(resp.Body).Decode(&body) + require.NoError(s.T(), err) + assert.Equal(s.T(), "library_id required", body["error"]) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeries_InvalidLibraryID() { + req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series?library_id=not-a-uuid", nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode) + + var body map[string]string + err = json.NewDecoder(resp.Body).Decode(&body) + require.NoError(s.T(), err) + assert.Equal(s.T(), "invalid library_id", body["error"]) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeries_EmptyLibrary() { + url := fmt.Sprintf("%s/api/series?library_id=%s", s.setup.Server.URL, s.libraryID) + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + + var body map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&body) + require.NoError(s.T(), err) + + series, ok := body["series"].([]interface{}) + require.True(s.T(), ok, "series should be an array") + assert.Empty(s.T(), series, "empty library should have no series") + + total, ok := body["total"].(float64) + require.True(s.T(), ok, "total should be a number") + assert.Equal(s.T(), float64(0), total) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeries_Unauthorized() { + req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series?library_id="+s.libraryID, nil) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeries_PaginationParams() { + url := fmt.Sprintf("%s/api/series?library_id=%s&limit=5&offset=0", s.setup.Server.URL, s.libraryID) + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + + var body map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&body) + require.NoError(s.T(), err) + + limit, ok := body["limit"].(float64) + require.True(s.T(), ok) + assert.Equal(s.T(), float64(5), limit) + + offset, ok := body["offset"].(float64) + require.True(s.T(), ok) + assert.Equal(s.T(), float64(0), offset) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeries_LimitClampedTo100() { + url := fmt.Sprintf("%s/api/series?library_id=%s&limit=999", s.setup.Server.URL, s.libraryID) + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + + var body map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&body) + require.NoError(s.T(), err) + + limit, ok := body["limit"].(float64) + require.True(s.T(), ok) + assert.Equal(s.T(), float64(100), limit, "limit should be clamped to 100") +} + +func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_RequiresLibraryID() { + req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series/books?name=Test+Series", nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode) + + var body map[string]string + err = json.NewDecoder(resp.Body).Decode(&body) + require.NoError(s.T(), err) + assert.Equal(s.T(), "library_id required", body["error"]) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_RequiresName() { + url := fmt.Sprintf("%s/api/series/books?library_id=%s", s.setup.Server.URL, s.libraryID) + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode) + + var body map[string]string + err = json.NewDecoder(resp.Body).Decode(&body) + require.NoError(s.T(), err) + assert.Equal(s.T(), "name required", body["error"]) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_InvalidLibraryID() { + req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series/books?library_id=bad-uuid&name=Test", nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_NonexistentSeries() { + url := fmt.Sprintf("%s/api/series/books?library_id=%s&name=Nonexistent+Series", s.setup.Server.URL, s.libraryID) + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + + var body map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&body) + require.NoError(s.T(), err) + + books, ok := body["books"].([]interface{}) + require.True(s.T(), ok, "books should be an array") + assert.Empty(s.T(), books, "nonexistent series should return empty books array") + + assert.Equal(s.T(), "Nonexistent Series", body["name"]) + total, ok := body["total"].(float64) + require.True(s.T(), ok) + assert.Equal(s.T(), float64(0), total) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_Unauthorized() { + req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series/books?library_id="+s.libraryID+"&name=Test", nil) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeries_SpecialCharactersInName() { + seriesName := "Series: Book & Other (Vol. 1)" + url := fmt.Sprintf("%s/api/series/books?library_id=%s&name=%s", s.setup.Server.URL, s.libraryID, seriesName) + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) +} + +func (s *SeriesIntegrationTestSuite) TestGetSeries_ResponseStructure() { + url := fmt.Sprintf("%s/api/series?library_id=%s", s.setup.Server.URL, s.libraryID) + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + + var body map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&body) + require.NoError(s.T(), err) + + assert.Contains(s.T(), body, "series", "response should contain 'series' key") + assert.Contains(s.T(), body, "total", "response should contain 'total' key") + assert.Contains(s.T(), body, "limit", "response should contain 'limit' key") + assert.Contains(s.T(), body, "offset", "response should contain 'offset' key") + + _, ok := body["series"].([]interface{}) + assert.True(s.T(), ok, "'series' should be an array") +} + +func (s *SeriesIntegrationTestSuite) TestRestoreSystemCollection_ContinueSeries() { + reqBody := map[string]interface{}{ + "collection_name": "Continue Series", + } + body, _ := json.Marshal(reqBody) + + req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/dashboard/restore-system-collection", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(s.T(), err) + assert.Contains(s.T(), response, "message") +} + +func (s *SeriesIntegrationTestSuite) TestGetSections_IncludesContinueSeries() { + url := fmt.Sprintf("%s/api/dashboard/sections?library_id=%s", s.setup.Server.URL, s.libraryID) + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+s.token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(s.T(), err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(s.T(), err) + + sections, ok := response["sections"].([]interface{}) + require.True(s.T(), ok, "sections should be an array") + require.Len(s.T(), sections, 5, "Should have 5 system collections") + + sectionIDs := make(map[string]bool) + for _, sec := range sections { + section := sec.(map[string]interface{}) + sectionIDs[section["id"].(string)] = true + } + + assert.Contains(s.T(), sectionIDs, "continue-series", "dashboard should include continue-series section") + assert.Contains(s.T(), sectionIDs, "continue-reading") + assert.Contains(s.T(), sectionIDs, "recently-added") + assert.Contains(s.T(), sectionIDs, "recently-read") + assert.Contains(s.T(), sectionIDs, "not-started") +} + +func TestSeriesIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(SeriesIntegrationTestSuite)) +} diff --git a/cmd/server/tests/test_helpers_test.go b/cmd/server/tests/test_helpers_test.go index ae8507e..43e00a9 100644 --- a/cmd/server/tests/test_helpers_test.go +++ b/cmd/server/tests/test_helpers_test.go @@ -286,6 +286,7 @@ func createDefaultCollectionsForUser(t *testing.T, db *database.Queries, userID {"recently-added", "Newly added items to this library", "🆕", "#9ece6a", "recently-added", 2}, {"recently-read", "Books you've finished (progress >= 1)", "✅", "#e0af68", "recently-read", 3}, {"not-started", "Books you haven't read yet (progress = 0 or no record)", "📕", "#f7768e", "not-started", 4}, + {"continue-series", "Next book in series you're reading", "📚", "#bb9af7", "continue-series", 5}, } for _, col := range defaultCollections { @@ -478,6 +479,7 @@ func setupTestServer(t *testing.T) *TestServerSetup { filtersHandler := handlers.NewFiltersHandler(queries) dashboardService := services.NewDashboardService(queries) dashboardHandler := handlers.NewDashboardHandler(queries) + seriesHandler := handlers.NewSeriesHandler(queries) mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker) mediaHandler.SetProgressService(progressService) matchingHandler := handlers.NewMatchingHandler(queries, connManager) @@ -529,6 +531,7 @@ func setupTestServer(t *testing.T) *TestServerSetup { FiltersHandler: filtersHandler, DashboardHandler: dashboardHandler, DashboardService: dashboardService, + SeriesHandler: seriesHandler, OPDSHandler: opdsHandler, JobsHandler: jobsHandler, ConnManager: connManager, diff --git a/internal/handlers/series_test.go b/internal/handlers/series_test.go new file mode 100644 index 0000000..96ee8a5 --- /dev/null +++ b/internal/handlers/series_test.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "testing" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" +) + +func TestNewSeriesHandler_NilDB(t *testing.T) { + handler := NewSeriesHandler(nil) + assert.NotNil(t, handler, "Handler should not be nil even with nil DB") + assert.NotNil(t, handler.seriesService, "Internal service should be initialized") +} + +func TestSeriesHandler_TextToStringConversion(t *testing.T) { + tests := []struct { + name string + input pgtype.Text + expected string + }{ + { + name: "valid author text", + input: pgtype.Text{String: "Brandon Sanderson", Valid: true}, + expected: "Brandon Sanderson", + }, + { + name: "empty valid text", + input: pgtype.Text{String: "", Valid: true}, + expected: "", + }, + { + name: "null text returns empty", + input: pgtype.Text{String: "ignored", Valid: false}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := textToString(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/internal/services/series_service_test.go b/internal/services/series_service_test.go new file mode 100644 index 0000000..4e44936 --- /dev/null +++ b/internal/services/series_service_test.go @@ -0,0 +1,136 @@ +package services + +import ( + "bookhoard/internal/database" + "testing" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" +) + +func TestContinueSeriesRowToMediaItems_FieldsMappedCorrectly(t *testing.T) { + itemUUID := uuid.New() + libUUID := uuid.New() + adminUUID := uuid.New() + + row := database.GetContinueSeriesItemsRow{ + ID: pgtype.UUID{Bytes: itemUUID, Valid: true}, + LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, + Title: "Test Book", + Author: pgtype.Text{String: "Test Author", Valid: true}, + Isbn: pgtype.Text{String: "978-1234567890", Valid: true}, + Description: pgtype.Text{String: "A test book", Valid: true}, + FilePath: "/books/test.epub", + FileSize: pgtype.Int8{Int64: 1024, Valid: true}, + MimeType: pgtype.Text{String: "application/epub+zip", Valid: true}, + CoverImagePath: pgtype.Text{String: "/covers/test.jpg", Valid: true}, + Series: pgtype.Text{String: "Test Series", Valid: true}, + SeriesNumber: pgtype.Int4{Int32: 3, Valid: true}, + Tags: []string{"fantasy", "adventure"}, + FormatGroup: "epub", + AddedByAdminID: pgtype.UUID{Bytes: adminUUID, Valid: true}, + } + + result := continueSeriesRowToMediaItems(row) + + assert.Equal(t, pgtype.UUID{Bytes: itemUUID, Valid: true}, result.ID, "ID should match") + assert.Equal(t, pgtype.UUID{Bytes: libUUID, Valid: true}, result.LibraryID, "LibraryID should match") + assert.Equal(t, "Test Book", result.Title, "Title should match") + assert.Equal(t, pgtype.Text{String: "Test Author", Valid: true}, result.Author, "Author should match") + assert.Equal(t, pgtype.Text{String: "978-1234567890", Valid: true}, result.Isbn, "ISBN should match") + assert.Equal(t, "/books/test.epub", result.FilePath, "FilePath should match") + assert.Equal(t, pgtype.Text{String: "application/epub+zip", Valid: true}, result.MimeType, "MimeType should match") + assert.Equal(t, pgtype.Text{String: "/covers/test.jpg", Valid: true}, result.CoverImagePath, "CoverImagePath should match") + assert.Equal(t, pgtype.Text{String: "Test Series", Valid: true}, result.Series, "Series should match") + assert.Equal(t, pgtype.Int4{Int32: 3, Valid: true}, result.SeriesNumber, "SeriesNumber should match") + assert.Equal(t, []string{"fantasy", "adventure"}, result.Tags, "Tags should match") + assert.Equal(t, "epub", result.FormatGroup, "FormatGroup should match") +} + +func TestContinueSeriesRowToMediaItems_NullFieldsHandled(t *testing.T) { + itemUUID := uuid.New() + libUUID := uuid.New() + + row := database.GetContinueSeriesItemsRow{ + ID: pgtype.UUID{Bytes: itemUUID, Valid: true}, + LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, + Title: "No Metadata Book", + Author: pgtype.Text{Valid: false}, + Isbn: pgtype.Text{Valid: false}, + Description: pgtype.Text{Valid: false}, + FilePath: "/books/nometa.epub", + FileSize: pgtype.Int8{Valid: false}, + MimeType: pgtype.Text{Valid: false}, + CoverImagePath: pgtype.Text{Valid: false}, + Series: pgtype.Text{Valid: false}, + SeriesNumber: pgtype.Int4{Valid: false}, + Tags: nil, + FormatGroup: "epub", + } + + result := continueSeriesRowToMediaItems(row) + + assert.Equal(t, "No Metadata Book", result.Title) + assert.False(t, result.Author.Valid, "Author should be invalid/null") + assert.False(t, result.Series.Valid, "Series should be invalid/null") + assert.False(t, result.SeriesNumber.Valid, "SeriesNumber should be invalid/null") + assert.Nil(t, result.Tags, "Tags should be nil") +} + +func TestContinueSeriesRowToMediaItems_AllFieldsMapped(t *testing.T) { + itemUUID := uuid.New() + libUUID := uuid.New() + + row := database.GetContinueSeriesItemsRow{ + ID: pgtype.UUID{Bytes: itemUUID, Valid: true}, + LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, + Title: "Full Book", + Author: pgtype.Text{String: "Author", Valid: true}, + Isbn: pgtype.Text{String: "ISBN", Valid: true}, + Description: pgtype.Text{String: "Desc", Valid: true}, + FilePath: "/book.epub", + FileSize: pgtype.Int8{Int64: 2048, Valid: true}, + MimeType: pgtype.Text{String: "epub", Valid: true}, + CoverImagePath: pgtype.Text{String: "/cover.jpg", Valid: true}, + Series: pgtype.Text{String: "Series", Valid: true}, + SeriesNumber: pgtype.Int4{Int32: 1, Valid: true}, + Tags: []string{"tag1"}, + Asin: pgtype.Text{String: "ASIN", Valid: true}, + Publisher: pgtype.Text{String: "Pub", Valid: true}, + Language: pgtype.Text{String: "en", Valid: true}, + Edition: pgtype.Text{String: "1st", Valid: true}, + Genre: pgtype.Text{String: "Fiction", Valid: true}, + FormatGroup: "epub", + FormatMimetype: pgtype.Text{String: "application/epub+zip", Valid: true}, + IsReflowable: pgtype.Bool{Bool: true, Valid: true}, + HasFixedLayout: pgtype.Bool{Bool: false, Valid: true}, + TotalCharacters: pgtype.Int8{Int64: 500000, Valid: true}, + ChapterCount: pgtype.Int4{Int32: 20, Valid: true}, + MangaType: pgtype.Text{String: "manga", Valid: true}, + ReadingDirection: pgtype.Text{String: "rtl", Valid: true}, + SeriesCount: pgtype.Int4{Int32: 10, Valid: true}, + Volume: pgtype.Int4{Int32: 1, Valid: true}, + LibraryTypeName: pgtype.Text{String: "ebooks", Valid: true}, + FileSha256: pgtype.Text{String: "abc123", Valid: true}, + } + + result := continueSeriesRowToMediaItems(row) + + assert.Equal(t, pgtype.Text{String: "ASIN", Valid: true}, result.Asin) + assert.Equal(t, pgtype.Text{String: "Pub", Valid: true}, result.Publisher) + assert.Equal(t, pgtype.Text{String: "en", Valid: true}, result.Language) + assert.Equal(t, pgtype.Text{String: "1st", Valid: true}, result.Edition) + assert.Equal(t, pgtype.Text{String: "Fiction", Valid: true}, result.Genre) + assert.Equal(t, pgtype.Text{String: "application/epub+zip", Valid: true}, result.FormatMimetype) + assert.Equal(t, pgtype.Bool{Bool: true, Valid: true}, result.IsReflowable) + assert.Equal(t, pgtype.Bool{Bool: false, Valid: true}, result.HasFixedLayout) + assert.Equal(t, pgtype.Int8{Int64: 500000, Valid: true}, result.TotalCharacters) + assert.Equal(t, pgtype.Int4{Int32: 20, Valid: true}, result.ChapterCount) + assert.Equal(t, pgtype.Text{String: "manga", Valid: true}, result.MangaType) + assert.Equal(t, pgtype.Text{String: "rtl", Valid: true}, result.ReadingDirection) + assert.Equal(t, pgtype.Int4{Int32: 10, Valid: true}, result.SeriesCount) + assert.Equal(t, pgtype.Int4{Int32: 1, Valid: true}, result.Volume) + assert.Equal(t, pgtype.Text{String: "ebooks", Valid: true}, result.LibraryTypeName) + assert.Equal(t, pgtype.Text{String: "abc123", Valid: true}, result.FileSha256) +}