package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" ) // TestISBNNormalization tests ISBN normalization with various formats func TestISBNNormalization(t *testing.T) { t.Run("ISBN-13 with hyphens should be normalized", func(t *testing.T) { // Test cases for ISBN-13 with various hyphen placements 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": "Test Ebook", "isbn": tc.input, "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") req.Header.Set("X-User-Id", uuid.New().String()) rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate successful creation with normalized ISBN w.WriteHeader(http.StatusCreated) response := map[string]interface{}{ "id": uuid.New().String(), "title": "Test Ebook", "isbn": tc.expected, } json.NewEncoder(w).Encode(response) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusCreated, rr.Code) var response map[string]interface{} json.Unmarshal(rr.Body.Bytes(), &response) assert.Equal(t, tc.expected, response["isbn"]) }) } }) t.Run("Empty ISBN should be handled", func(t *testing.T) { payload := map[string]interface{}{ "title": "Test Ebook", "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusCreated, rr.Code) }) t.Run("Invalid ISBN format should still be stored as-is", func(t *testing.T) { // Test that very short or obviously invalid ISBNs are still accepted // The database function will normalize what it can testCases := []struct { name string isbn string valid bool }{ {"Too short ISBN", "123", true}, {"Valid ISBN-13", "9780123456789", true}, {"Valid ISBN-10", "0123456789", true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { payload := map[string]interface{}{ "title": "Test Ebook", "isbn": tc.isbn, "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if tc.valid { w.WriteHeader(http.StatusCreated) } else { w.WriteHeader(http.StatusBadRequest) } }) handler.ServeHTTP(rr, req) if tc.valid { assert.Equal(t, http.StatusCreated, rr.Code) } else { assert.Equal(t, http.StatusBadRequest, rr.Code) } }) } }) } // TestEbookLibraryRequirement tests that ebooks require an existing library func TestEbookLibraryRequirement(t *testing.T) { t.Run("Create ebook without any libraries should fail gracefully", func(t *testing.T) { payload := map[string]interface{}{ "title": "Test Ebook", "isbn": "978-0123456789", "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") req.Header.Set("X-User-Id", uuid.New().String()) rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate no library found scenario w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"no ebook library found. Please create an ebook library first"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Contains(t, rr.Body.String(), "no ebook library found") }) t.Run("Create ebook with existing library should succeed", func(t *testing.T) { libraryID := uuid.New() payload := map[string]interface{}{ "title": "Test Ebook", "isbn": "978-0123456789", "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") req.Header.Set("X-User-Id", uuid.New().String()) req.Header.Set("X-Library-Id", libraryID.String()) rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate successful creation w.WriteHeader(http.StatusCreated) response := map[string]interface{}{ "id": uuid.New().String(), "title": "Test Ebook", "isbn": "9780123456789", "library_id": libraryID.String(), } json.NewEncoder(w).Encode(response) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusCreated, rr.Code) var response map[string]interface{} json.Unmarshal(rr.Body.Bytes(), &response) assert.Equal(t, "9780123456789", response["isbn"]) assert.Equal(t, libraryID.String(), response["library_id"]) }) t.Run("Update ebook with normalized ISBN", func(t *testing.T) { ebookID := uuid.New() payload := map[string]interface{}{ "title": "Updated Ebook", "isbn": "978-987654321-0", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/ebooks/"+ebookID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) response := map[string]interface{}{ "id": ebookID.String(), "title": "Updated Ebook", "isbn": "9789876543210", } json.NewEncoder(w).Encode(response) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) var response map[string]interface{} json.Unmarshal(rr.Body.Bytes(), &response) assert.Equal(t, "9789876543210", response["isbn"]) }) } // TestISBNEdgeCases tests edge cases for ISBN handling func TestISBNEdgeCases(t *testing.T) { t.Run("ISBN with special characters should be normalized", func(t *testing.T) { testCases := []struct { name string input string expected string }{ { name: "ISBN with multiple hyphens", input: "978-0-123-45678-9", expected: "9780123456789", }, { name: "ISBN with trailing hyphen", input: "9780123456789-", expected: "9780123456789", }, { name: "ISBN with leading hyphen", input: "-9780123456789", expected: "9780123456789", }, { name: "ISBN with multiple spaces", input: "978 0123456789", expected: "9780123456789", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { payload := map[string]interface{}{ "title": "Test Ebook", "isbn": tc.input, "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) response := map[string]interface{}{ "id": uuid.New().String(), "title": "Test Ebook", "isbn": tc.expected, } json.NewEncoder(w).Encode(response) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusCreated, rr.Code) var response map[string]interface{} json.Unmarshal(rr.Body.Bytes(), &response) assert.Equal(t, tc.expected, response["isbn"]) }) } }) t.Run("ISBN length validation", func(t *testing.T) { testCases := []struct { name string isbn string shouldAccept bool }{ { name: "Empty ISBN", isbn: "", shouldAccept: true, }, { name: "Valid ISBN-10", isbn: "0123456789", shouldAccept: true, }, { name: "Valid ISBN-13", isbn: "9780123456789", shouldAccept: true, }, { name: "ISBN-13 with hyphens", isbn: "978-0-12345-678-9", shouldAccept: true, }, { name: "Maximum length (17 chars with hyphens)", isbn: "978-0-12345-678-9", shouldAccept: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { payload := map[string]interface{}{ "title": "Test Ebook", "isbn": tc.isbn, "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if tc.shouldAccept { w.WriteHeader(http.StatusCreated) } else { w.WriteHeader(http.StatusBadRequest) } }) handler.ServeHTTP(rr, req) if tc.shouldAccept { assert.Equal(t, http.StatusCreated, rr.Code) } else { assert.Equal(t, http.StatusBadRequest, rr.Code) } }) } }) } // TestLibraryAutoSelection tests automatic library selection by resource type func TestLibraryAutoSelection(t *testing.T) { t.Run("Auto-select first available ebook library", func(t *testing.T) { libraryID := uuid.New() payload := map[string]interface{}{ "title": "Test Ebook", "isbn": "9780123456789", "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") req.Header.Set("X-User-Id", uuid.New().String()) req.Header.Set("X-Library-Id", libraryID.String()) rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate auto-selection of first ebook library w.WriteHeader(http.StatusCreated) response := map[string]interface{}{ "id": uuid.New().String(), "title": "Test Ebook", "isbn": "9780123456789", "library_id": libraryID.String(), } json.NewEncoder(w).Encode(response) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusCreated, rr.Code) var response map[string]interface{} json.Unmarshal(rr.Body.Bytes(), &response) assert.Equal(t, libraryID.String(), response["library_id"]) }) t.Run("Multiple ebook libraries - should select first", func(t *testing.T) { firstLibraryID := uuid.New() secondLibraryID := uuid.New() payload := map[string]interface{}{ "title": "Test Ebook", "isbn": "9780123456789", "file_path": "/path/to/file.epub", "file_size": 1024, "mime_type": "application/epub+zip", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") req.Header.Set("X-User-Id", uuid.New().String()) rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate auto-selection of first ebook library w.WriteHeader(http.StatusCreated) response := map[string]interface{}{ "id": uuid.New().String(), "title": "Test Ebook", "isbn": "9780123456789", "library_id": firstLibraryID.String(), } json.NewEncoder(w).Encode(response) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusCreated, rr.Code) var response map[string]interface{} json.Unmarshal(rr.Body.Bytes(), &response) // Should select first library, not second assert.Equal(t, firstLibraryID.String(), response["library_id"]) assert.NotEqual(t, secondLibraryID.String(), response["library_id"]) }) }