package main import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // createTestLibrary creates a test library and returns its ID func createTestLibrary(t *testing.T, ts *httptest.Server, token, name string) string { t.Helper() payload := map[string]interface{}{ "name": name, "description": "Test library for ISBN tests", "type": "ebooks", } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", ts.URL+"/api/libraries", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusCreated, resp.StatusCode) var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) return result["id"].(string) } // TestMediaItemISBNNormalization tests ISBN normalization with media-items endpoint func TestMediaItemISBNNormalization(t *testing.T) { setup := setupTestServer(t) // Create an ebook library first token := setup.Token libID := createTestLibrary(t, setup.Server, token, "test-isbn-lib") // Add a folder to the library (required before adding media items) addFolderToLibrary(t, setup, libID, "/app/uploads") // Test ISBN normalization cases testCases := []struct { name string input string expected string }{ { name: "ISBN-13 with hyphens (978-0-12345-678-9)", input: "978-0-12345-678-9", expected: "9780123456789", }, { name: "ISBN-13 with single hyphen (978-0123456789)", input: "978-0123456789", expected: "9780123456789", }, { name: "ISBN-13 without hyphens", input: "9780123456789", expected: "9780123456789", }, { name: "ISBN-13 with spaces", input: "978 0123456789", expected: "9780123456789", }, { name: "ISBN-13 with mixed hyphens and spaces", input: "978-0 1234-56789", expected: "9780123456789", }, { name: "ISBN-10 with hyphens", input: "0-12345-678-9", expected: "9780123456786", }, { name: "ISBN-10 without hyphens", input: "0123456789", expected: "9780123456786", }, { name: "ISBN-10 with X", input: "0-12345-678-X", expected: "9780123456786", }, { name: "ISBN-10 converts to ISBN-13", input: "0-306-40615-2", expected: "9780306406157", }, { name: "ISBN-13 with trailing hyphen", input: "978030640615-7-", expected: "9780306406157", }, { name: "ISBN-10 with trailing hyphen", input: "0306406152-", expected: "9780306406157", }, { name: "only hyphens converts to empty string", input: "---", expected: "", }, { name: "only spaces converts to empty string", input: " ", expected: "", }, { name: "ISBN-10 with X converts to ISBN-13", input: "0-596-00965-X", expected: "9780596009656", }, { name: "ISBN-13 with leading/trailing hyphens", input: "-978-0-306-40615-7-", expected: "9780306406157", }, { name: "ISBN-13 preserves case", input: "978-0-306-40615-7", expected: "9780306406157", }, { name: "ISBN-13 lowercase preserved", input: "978-0-306-40615-7", expected: "9780306406157", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { payload := map[string]interface{}{ "title": fmt.Sprintf("Test Book %s", tc.name), "isbn": tc.input, "library_id": libID, "file_path": "/test/path.epub", "file_size": 1024, "mime_type": "application/epub+zip", } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() // Check if this is an invalid ISBN case that should return 422 if tc.expected == "" && (tc.input == "---" || tc.input == " ") { assert.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) } else { assert.Equal(t, http.StatusCreated, resp.StatusCode) } var response map[string]interface{} json.NewDecoder(resp.Body).Decode(&response) // For valid ISBN responses, verify normalization worked correctly if resp.StatusCode == http.StatusCreated { assert.Equal(t, tc.expected, response["isbn"]) } }) } } // TestMediaItemISBNEdgeCases tests ISBN edge cases func TestMediaItemISBNEdgeCases(t *testing.T) { setup := setupTestServer(t) token := setup.Token libID := createTestLibrary(t, setup.Server, token, "test-isbn-edge-lib") addFolderToLibrary(t, setup, libID, "/app/uploads") t.Run("Empty ISBN should be accepted", func(t *testing.T) { payload := map[string]interface{}{ "title": "Book without ISBN", "library_id": libID, "file_path": "/test/path.epub", "file_size": 1024, "mime_type": "application/epub+zip", } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusCreated, resp.StatusCode) }) t.Run("ISBN with multiple hyphens", func(t *testing.T) { payload := map[string]interface{}{ "title": "Multi-Hyphen ISBN", "isbn": "978-0-306-40615-7", "library_id": libID, "file_path": "/test/path.epub", "file_size": 1024, "mime_type": "application/epub+zip", } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() var response map[string]interface{} json.NewDecoder(resp.Body).Decode(&response) assert.Equal(t, http.StatusCreated, resp.StatusCode) assert.Equal(t, "9780306406157", response["isbn"]) }) t.Run("ISBN with trailing hyphen", func(t *testing.T) { payload := map[string]interface{}{ "title": "Trailing Hyphen ISBN", "isbn": "978-0-596-00965-2", "library_id": libID, "file_path": "/test/path.epub", "file_size": 1024, "mime_type": "application/epub+zip", } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() var response map[string]interface{} json.NewDecoder(resp.Body).Decode(&response) assert.Equal(t, http.StatusCreated, resp.StatusCode) assert.Equal(t, "9780596009652", response["isbn"]) }) } // TestMediaItemsPagination tests pagination with media-items endpoint func TestMediaItemsPagination(t *testing.T) { setup := setupTestServer(t) token := setup.Token libID := createTestLibrary(t, setup.Server, token, "test-pagination-lib") addFolderToLibrary(t, setup, libID, "/app/uploads") // Create some test media items for i := 1; i <= 5; i++ { payload := map[string]interface{}{ "title": fmt.Sprintf("Book %d", i), "isbn": fmt.Sprintf("978030640615%d", i), "library_id": libID, "file_path": "/test/path.epub", "file_size": 1024, "mime_type": "application/epub+zip", } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) resp.Body.Close() } // Small delay to allow database to commit before pagination queries time.Sleep(100 * time.Millisecond) t.Run("Valid pagination parameters", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/media-items?library_id="+libID+"&limit=2&offset=0", nil) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) var response map[string]interface{} json.NewDecoder(resp.Body).Decode(&response) data := response["data"].([]interface{}) // Should get 2 items assert.Equal(t, 2, len(data)) }) t.Run("Pagination with offset", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/media-items?library_id="+libID+"&limit=2&offset=2", nil) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) var response map[string]interface{} json.NewDecoder(resp.Body).Decode(&response) data := response["data"].([]interface{}) // Should get 2 items starting from offset 2 assert.Equal(t, 2, len(data)) }) t.Run("Negative limit should fail", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/media-items?library_id="+libID+"&limit=-10&offset=0", nil) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() // Should return 400 Bad Request or handle it gracefully assert.NotEqual(t, http.StatusOK, resp.StatusCode) }) t.Run("Negative offset should fail", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/media-items?library_id="+libID+"&limit=10&offset=-5", nil) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() // Should return 400 Bad Request or handle it gracefully assert.NotEqual(t, http.StatusOK, resp.StatusCode) }) t.Run("Limit exceeds maximum", func(t *testing.T) { req, _ := http.NewRequest("GET", setup.Server.URL+"/api/media-items?library_id="+libID+"&limit=10000&offset=0", nil) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() // Should be capped at maximum or return error // The application uses maxPaginationLimit = 1000 assert.NotEqual(t, http.StatusInternalServerError, resp.StatusCode) }) } // TestMediaItemLibraryRequirement tests that media items require a library func TestMediaItemLibraryRequirement(t *testing.T) { setup := setupTestServer(t) token := setup.Token t.Run("Create media-item without library should fail gracefully", func(t *testing.T) { payload := map[string]interface{}{ "title": "Orphan Book", "isbn": "9780123456789", "file_path": "/test/path.epub", "file_size": 1024, "mime_type": "application/epub+zip", } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() // Should fail - library_id is required assert.NotEqual(t, http.StatusCreated, resp.StatusCode) }) t.Run("Create media-item with existing library should succeed", func(t *testing.T) { libID := createTestLibrary(t, setup.Server, token, "test-req-lib") addFolderToLibrary(t, setup, libID, "/app/uploads") payload := map[string]interface{}{ "title": "Valid Book", "isbn": "978-0-306-40615-7", "library_id": libID, "file_path": "/test/path.epub", "file_size": 1024, "mime_type": "application/epub+zip", } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusCreated, resp.StatusCode) var response map[string]interface{} json.NewDecoder(resp.Body).Decode(&response) // VerifyISBN was normalized assert.Equal(t, "9780306406157", response["isbn"]) assert.Equal(t, libID, response["library_id"]) }) } // TestUpdateMediaItemISBN tests updating media-item ISBN func TestUpdateMediaItemISBN(t *testing.T) { setup := setupTestServer(t) token := setup.Token libID := createTestLibrary(t, setup.Server, token, "test-update-lib") addFolderToLibrary(t, setup, libID, "/app/uploads") // First create a media item createPayload := map[string]interface{}{ "title": "Original Title", "isbn": "978-0-596-00965-2", "library_id": libID, "file_path": "/test/path.epub", "file_size": 1024, "mime_type": "application/epub+zip", } body, _ := json.Marshal(createPayload) req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) var createResponse map[string]interface{} json.NewDecoder(resp.Body).Decode(&createResponse) resp.Body.Close() mediaItemID := createResponse["id"].(string) // Now update with new ISBN t.Run("Update with invalid ISBN rejects", func(t *testing.T) { updatePayload := map[string]interface{}{ "title": "Updated Title", "isbn": "978-9876543210-9", } body, _ := json.Marshal(updatePayload) req, _ := http.NewRequest("PUT", setup.Server.URL+"/api/media-items/"+mediaItemID, bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) }) }