diff --git a/cmd/server/tests/media_item_isbn_test.go b/cmd/server/tests/media_item_isbn_test.go new file mode 100644 index 0000000..815e4e3 --- /dev/null +++ b/cmd/server/tests/media_item_isbn_test.go @@ -0,0 +1,445 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "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) { + ts, db, _, _ := setupTestServer(t) + defer ts.Close() + + // Create an ebook library first + token := loginTestUser(t, ts, db) + libID := createTestLibrary(t, ts, token, "test-isbn-lib") + + // 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: "0123456789", + }, + { + name: "ISBN-10 without hyphens", + input: "0123456789", + expected: "0123456789", + }, + { + name: "ISBN-10 with X", + input: "0-12345-678-X", + expected: "012345678X", + }, + } + + 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", ts.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) + + // Verify ISBN was normalized + assert.Equal(t, tc.expected, response["isbn"]) + }) + } +} + +// TestMediaItemISBNEdgeCases tests ISBN edge cases +func TestMediaItemISBNEdgeCases(t *testing.T) { + ts, db, _, _ := setupTestServer(t) + defer ts.Close() + + token := loginTestUser(t, ts, db) + libID := createTestLibrary(t, ts, token, "test-isbn-edge-lib") + + 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", ts.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-123-45678-9", + "library_id": libID, + "file_path": "/test/path.epub", + "file_size": 1024, + "mime_type": "application/epub+zip", + } + + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", ts.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, "9780123456789", response["isbn"]) + }) + + t.Run("ISBN with trailing hyphen", func(t *testing.T) { + payload := map[string]interface{}{ + "title": "Trailing Hyphen ISBN", + "isbn": "9780123456789-", + "library_id": libID, + "file_path": "/test/path.epub", + "file_size": 1024, + "mime_type": "application/epub+zip", + } + + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", ts.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, "9780123456789", response["isbn"]) + }) +} + +// TestMediaItemsPagination tests pagination with media-items endpoint +func TestMediaItemsPagination(t *testing.T) { + ts, db, _, _ := setupTestServer(t) + defer ts.Close() + + token := loginTestUser(t, ts, db) + libID := createTestLibrary(t, ts, token, "test-pagination-lib") + + // Create some test media items + for i := 1; i <= 5; i++ { + payload := map[string]interface{}{ + "title": fmt.Sprintf("Book %d", i), + "isbn": fmt.Sprintf("97801234567%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", ts.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() + } + + t.Run("Valid pagination parameters", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.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) + + // Should get 2 items + assert.Equal(t, 2, len(response)) + }) + + t.Run("Pagination with offset", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.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) + + // Should get 2 items starting from offset 2 + assert.Equal(t, 2, len(response)) + }) + + t.Run("Negative limit should fail", func(t *testing.T) { + req, _ := http.NewRequest("GET", ts.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", ts.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", ts.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) { + ts, db, _, _ := setupTestServer(t) + defer ts.Close() + + token := loginTestUser(t, ts, db) + + 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", ts.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, ts, token, "test-req-lib") + + payload := map[string]interface{}{ + "title": "Valid Book", + "isbn": "978-0123456789", + "library_id": libID, + "file_path": "/test/path.epub", + "file_size": 1024, + "mime_type": "application/epub+zip", + } + + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", ts.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) + + // Verify ISBN was normalized + assert.Equal(t, "9780123456789", response["isbn"]) + assert.Equal(t, libID, response["library_id"]) + }) +} + +// TestUpdateMediaItemISBN tests updating media-item ISBN +func TestUpdateMediaItemISBN(t *testing.T) { + ts, db, _, _ := setupTestServer(t) + defer ts.Close() + + token := loginTestUser(t, ts, db) + libID := createTestLibrary(t, ts, token, "test-update-lib") + + // First create a media item + createPayload := map[string]interface{}{ + "title": "Original Title", + "isbn": "9780123456789", + "library_id": libID, + "file_path": "/test/path.epub", + "file_size": 1024, + "mime_type": "application/epub+zip", + } + + body, _ := json.Marshal(createPayload) + req, _ := http.NewRequest("POST", ts.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 normalized ISBN", func(t *testing.T) { + updatePayload := map[string]interface{}{ + "title": "Updated Title", + "isbn": "978-987654321-0", + } + + body, _ := json.Marshal(updatePayload) + req, _ := http.NewRequest("PUT", ts.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.StatusOK, resp.StatusCode) + + var response map[string]interface{} + json.NewDecoder(resp.Body).Decode(&response) + + // Verify ISBN was normalized + assert.Equal(t, "9789876543210", response["isbn"]) + assert.Equal(t, "Updated Title", response["title"]) + }) +}