package main import ( "bytes" "encoding/json" "io" "net/http" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestCollectionsBulkOperations tests bulk collection operations func TestCollectionsBulkOperations(t *testing.T) { setup := setupTestServer(t) client := &http.Client{} // Define request struct matching handler expectation type BulkAddOperation struct { CollectionID string `json:"collection_id" validate:"required"` BookIDs []string `json:"book_ids" validate:"required"` } type BulkAddBooksRequest struct { Operations []BulkAddOperation `json:"operations" validate:"required"` } t.Run("BulkAddBooks_WithoutAuth", func(t *testing.T) { bookID := createTestMediaItemID(t, setup) req := BulkAddBooksRequest{ Operations: []BulkAddOperation{ { CollectionID: uuid.New().String(), BookIDs: []string{bookID}, }, }, } body, _ := json.Marshal(req) httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(body)) httpReq.Header.Set("Content-Type", "application/json") resp, err := client.Do(httpReq) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) }) t.Run("BulkAddBooks_EmptyOperations", func(t *testing.T) { req := BulkAddBooksRequest{ Operations: []BulkAddOperation{}, } body, _ := json.Marshal(req) httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(body)) httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+setup.Token) resp, err := client.Do(httpReq) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) t.Run("BulkAddBooks_InvalidCollectionID", func(t *testing.T) { bookID := createTestMediaItemID(t, setup) req := BulkAddBooksRequest{ Operations: []BulkAddOperation{ { CollectionID: "invalid-uuid", BookIDs: []string{bookID}, }, }, } body, _ := json.Marshal(req) httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(body)) httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+setup.Token) resp, err := client.Do(httpReq) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) assert.Equal(t, http.StatusOK, resp.StatusCode) var result map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Contains(t, result, "results") assert.Contains(t, result, "total") assert.Contains(t, result, "added") assert.Contains(t, result, "failed") results := result["results"].([]interface{}) assert.True(t, len(results) > 0) firstResult := results[0].(map[string]interface{}) assert.Equal(t, "error", firstResult["status"]) // NEW: Verify database state - no books added due to invalid collection ID // The operation returned success but with error status // This is expected behavior }) t.Run("BulkAddBooks_InvalidBookID", func(t *testing.T) { // Create a collection first collectionReq := map[string]interface{}{ "name": "Test Collection - InvalidBookID", "description": "A test collection", } collectionBody, _ := json.Marshal(collectionReq) collectionHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections", bytes.NewBuffer(collectionBody)) collectionHTTP.Header.Set("Content-Type", "application/json") collectionHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err := client.Do(collectionHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) var collectionResult map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&collectionResult) require.NoError(t, err) collectionID := collectionResult["id"].(string) // Now try to add invalid book IDs addReq := map[string]interface{}{ "operations": []map[string]interface{}{ { "collection_id": collectionID, "book_ids": []string{"invalid-uuid"}, }, }, } addBody, _ := json.Marshal(addReq) addHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody)) addHTTP.Header.Set("Content-Type", "application/json") addHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err = client.Do(addHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) assert.Equal(t, http.StatusOK, resp.StatusCode) var result map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) results := result["results"].([]interface{}) firstResult := results[0].(map[string]interface{}) assert.Equal(t, "error", firstResult["status"]) }) t.Run("BulkAddBooks_SingleOperation", func(t *testing.T) { // Create a collection collectionReq := map[string]interface{}{ "name": "Test Collection - SingleOperation", "description": "A test collection", } collectionBody, _ := json.Marshal(collectionReq) collectionHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections", bytes.NewBuffer(collectionBody)) collectionHTTP.Header.Set("Content-Type", "application/json") collectionHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err := client.Do(collectionHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) var collectionResult map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&collectionResult) require.NoError(t, err) collectionID := collectionResult["id"].(string) // Create a book bookID := createTestMediaItemID(t, setup) // Add book to collection addReq := map[string]interface{}{ "operations": []map[string]interface{}{ { "collection_id": collectionID, "book_ids": []string{bookID}, }, }, } addBody, _ := json.Marshal(addReq) addHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody)) addHTTP.Header.Set("Content-Type", "application/json") addHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err = client.Do(addHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) assert.Equal(t, http.StatusOK, resp.StatusCode) var result map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Contains(t, result, "results") assert.Contains(t, result, "total") assert.Contains(t, result, "added") assert.Contains(t, result, "failed") results := result["results"].([]interface{}) firstResult := results[0].(map[string]interface{}) assert.Equal(t, "success", firstResult["status"]) }) t.Run("BulkAddBooks_MultipleBooksSingleCollection", func(t *testing.T) { // Create a collection collectionReq := map[string]interface{}{ "name": "Test Collection - MultipleBooksSingleCollection", "description": "A test collection", } collectionBody, _ := json.Marshal(collectionReq) collectionHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections", bytes.NewBuffer(collectionBody)) collectionHTTP.Header.Set("Content-Type", "application/json") collectionHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err := client.Do(collectionHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) var collectionResult map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&collectionResult) require.NoError(t, err) collectionID := collectionResult["id"].(string) // Create multiple books bookID1 := createTestMediaItemID(t, setup) bookID2 := createTestMediaItemID(t, setup) bookID3 := createTestMediaItemID(t, setup) // Add all books to collection addReq := map[string]interface{}{ "operations": []map[string]interface{}{ { "collection_id": collectionID, "book_ids": []string{bookID1, bookID2, bookID3}, }, }, } addBody, _ := json.Marshal(addReq) addHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody)) addHTTP.Header.Set("Content-Type", "application/json") addHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err = client.Do(addHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) assert.Equal(t, http.StatusOK, resp.StatusCode) var result map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, 3.0, result["total"]) assert.True(t, result["added"].(float64) > 0) }) t.Run("BulkAddBooks_MultipleCollections", func(t *testing.T) { // Create multiple collections collectionReq := map[string]interface{}{ "name": "Test Collection 1 - MultipleCollections", "description": "First test collection", } collectionBody, _ := json.Marshal(collectionReq) collectionHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections", bytes.NewBuffer(collectionBody)) collectionHTTP.Header.Set("Content-Type", "application/json") collectionHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err := client.Do(collectionHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) var collectionResult1 map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&collectionResult1) require.NoError(t, err) collectionID1 := collectionResult1["id"].(string) collectionReq2 := map[string]interface{}{ "name": "Test Collection 2 - MultipleCollections", "description": "Second test collection", } collectionBody2, _ := json.Marshal(collectionReq2) collectionHTTP2, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections", bytes.NewBuffer(collectionBody2)) collectionHTTP2.Header.Set("Content-Type", "application/json") collectionHTTP2.Header.Set("Authorization", "Bearer "+setup.Token) resp, err = client.Do(collectionHTTP2) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) var collectionResult2 map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&collectionResult2) require.NoError(t, err) collectionID2 := collectionResult2["id"].(string) // Create books bookID1 := createTestMediaItemID(t, setup) bookID2 := createTestMediaItemID(t, setup) // Add books to multiple collections addReq := map[string]interface{}{ "operations": []map[string]interface{}{ { "collection_id": collectionID1, "book_ids": []string{bookID1}, }, { "collection_id": collectionID2, "book_ids": []string{bookID1, bookID2}, }, }, } addBody, _ := json.Marshal(addReq) addHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody)) addHTTP.Header.Set("Content-Type", "application/json") addHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err = client.Do(addHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) assert.Equal(t, http.StatusOK, resp.StatusCode) var result map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Contains(t, result, "results") assert.Equal(t, 3.0, result["total"]) }) t.Run("BulkAddBooks_DuplicateBooks", func(t *testing.T) { // Create a collection collectionReq := map[string]interface{}{ "name": "Test Collection - DuplicateBooks", "description": "A test collection", } collectionBody, _ := json.Marshal(collectionReq) collectionHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections", bytes.NewBuffer(collectionBody)) collectionHTTP.Header.Set("Content-Type", "application/json") collectionHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err := client.Do(collectionHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) var collectionResult map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&collectionResult) require.NoError(t, err) collectionID := collectionResult["id"].(string) // Create a book bookID := createTestMediaItemID(t, setup) // Add book to collection addReq := map[string]interface{}{ "operations": []map[string]interface{}{ { "collection_id": collectionID, "book_ids": []string{bookID}, }, }, } addBody, _ := json.Marshal(addReq) addHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody)) addHTTP.Header.Set("Content-Type", "application/json") addHTTP.Header.Set("Authorization", "Bearer "+setup.Token) resp, err = client.Do(addHTTP) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) // Try to add same book again - create new request with fresh body addBody2, _ := json.Marshal(addReq) addHTTP2, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer(addBody2)) addHTTP2.Header.Set("Content-Type", "application/json") addHTTP2.Header.Set("Authorization", "Bearer "+setup.Token) resp2, err := client.Do(addHTTP2) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp2.Body) // Should handle duplicate gracefully (either succeed or return error) assert.Equal(t, http.StatusOK, resp2.StatusCode) }) t.Run("BulkAddBooks_InvalidRequestBody", func(t *testing.T) { // Send invalid JSON httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/collections/bulk-add-books", bytes.NewBuffer([]byte("invalid json"))) httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+setup.Token) resp, err := client.Do(httpReq) require.NoError(t, err) defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) }