""" Tests for Collection CRUD API endpoints. Tests the following endpoints: - GET /library/api/collections - POST /library/api/collections - PUT /library/api/collections/ - DELETE /library/api/collections/ """ import uuid # Import helpers from conftest from .conftest import delete_test_collection class TestGetCollections: """Tests for GET /library/api/collections endpoint.""" def test_get_collections_unauthenticated(self, client): """Unauthenticated request is rejected.""" response = client.get("/library/api/collections") # Should redirect to login or return 401 assert response.status_code in [302, 401] def test_get_collections_authenticated_empty(self, authenticated_client): """Returns collections list (may include default Library).""" response = authenticated_client.get("/library/api/collections") assert response.status_code == 200 data = response.get_json() assert data["success"] is True assert "collections" in data assert isinstance(data["collections"], list) def test_get_collections_with_data( self, authenticated_client, create_collection_helper ): """Returns created collections.""" # Create a test collection created = create_collection_helper( name="API Test Collection", description="Testing GET endpoint", ) assert created is not None response = authenticated_client.get("/library/api/collections") assert response.status_code == 200 data = response.get_json() assert data["success"] is True # Find our created collection collection_names = [c["name"] for c in data["collections"]] assert "API Test Collection" in collection_names def test_get_collections_includes_metadata( self, authenticated_client, create_collection_helper ): """Collections include required metadata fields.""" created = create_collection_helper() assert created is not None response = authenticated_client.get("/library/api/collections") assert response.status_code == 200 data = response.get_json() assert len(data["collections"]) > 0 # Check metadata fields coll = data["collections"][0] assert "id" in coll assert "name" in coll assert "description" in coll assert "created_at" in coll assert "collection_type" in coll assert "document_count" in coll class TestCreateCollection: """Tests for POST /library/api/collections endpoint.""" def test_create_collection_unauthenticated(self, client): """Unauthenticated request is rejected.""" response = client.post( "/library/api/collections", json={"name": "Test", "description": "Test"}, content_type="application/json", ) assert response.status_code in [302, 401] def test_create_collection_success(self, authenticated_client): """Successfully creates a new collection.""" unique_name = f"Test Collection {uuid.uuid4().hex[:8]}" response = authenticated_client.post( "/library/api/collections", json={ "name": unique_name, "description": "A test collection", "type": "user_uploads", }, content_type="application/json", ) assert response.status_code == 200 data = response.get_json() assert data["success"] is True assert data["collection"]["name"] == unique_name assert data["collection"]["description"] == "A test collection" assert "id" in data["collection"] # Cleanup delete_test_collection(authenticated_client, data["collection"]["id"]) def test_create_collection_defaults_private(self, authenticated_client): """A new collection with no is_public flag defaults to private.""" name = f"Priv {uuid.uuid4().hex[:8]}" resp = authenticated_client.post( "/library/api/collections", json={"name": name}, content_type="application/json", ) assert resp.status_code == 200 coll = resp.get_json()["collection"] assert coll["is_public"] is False delete_test_collection(authenticated_client, coll["id"]) def test_create_and_toggle_collection_is_public(self, authenticated_client): """is_public can be set at create and flipped via PUT, and is reflected in the list API.""" name = f"Pub {uuid.uuid4().hex[:8]}" resp = authenticated_client.post( "/library/api/collections", json={"name": name, "is_public": True}, content_type="application/json", ) assert resp.status_code == 200 coll = resp.get_json()["collection"] cid = coll["id"] assert coll["is_public"] is True # List reflects it listing = authenticated_client.get( "/library/api/collections" ).get_json()["collections"] match = next(c for c in listing if c["id"] == cid) assert match["is_public"] is True # The collection-details load path (documents endpoint) also exposes # is_public so the details-page toggle can reflect current state. docs = authenticated_client.get( f"/library/api/collections/{cid}/documents" ) assert docs.status_code == 200 assert docs.get_json()["collection"]["is_public"] is True # Flip back to private via PUT put = authenticated_client.put( f"/library/api/collections/{cid}", json={"is_public": False}, content_type="application/json", ) assert put.status_code == 200 assert put.get_json()["collection"]["is_public"] is False delete_test_collection(authenticated_client, cid) def test_create_collection_empty_name(self, authenticated_client): """Rejects collection with empty name.""" response = authenticated_client.post( "/library/api/collections", json={"name": "", "description": "No name"}, content_type="application/json", ) assert response.status_code == 400 data = response.get_json() assert data["success"] is False assert ( "required" in data["error"].lower() or "name" in data["error"].lower() ) def test_create_collection_whitespace_name(self, authenticated_client): """Rejects collection with whitespace-only name.""" response = authenticated_client.post( "/library/api/collections", json={"name": " ", "description": "Whitespace name"}, content_type="application/json", ) assert response.status_code == 400 def test_create_collection_duplicate_name(self, authenticated_client): """Rejects collection with duplicate name.""" unique_name = f"Duplicate Test {uuid.uuid4().hex[:8]}" # Create first collection response1 = authenticated_client.post( "/library/api/collections", json={"name": unique_name, "description": "First"}, content_type="application/json", ) assert response1.status_code == 200 collection_id = response1.get_json()["collection"]["id"] try: # Try to create duplicate response2 = authenticated_client.post( "/library/api/collections", json={"name": unique_name, "description": "Duplicate"}, content_type="application/json", ) assert response2.status_code == 400 data = response2.get_json() assert data["success"] is False assert "exists" in data["error"].lower() finally: # Cleanup delete_test_collection(authenticated_client, collection_id) def test_create_collection_without_description(self, authenticated_client): """Creates collection without description.""" unique_name = f"No Desc Collection {uuid.uuid4().hex[:8]}" response = authenticated_client.post( "/library/api/collections", json={"name": unique_name}, content_type="application/json", ) assert response.status_code == 200 data = response.get_json() assert data["success"] is True assert data["collection"]["name"] == unique_name # Cleanup delete_test_collection(authenticated_client, data["collection"]["id"]) class TestUpdateCollection: """Tests for PUT /library/api/collections/ endpoint.""" def test_update_collection_unauthenticated(self, client): """Unauthenticated request is rejected.""" response = client.put( "/library/api/collections/some-id", json={"name": "Updated"}, content_type="application/json", ) assert response.status_code in [302, 401] def test_update_collection_name( self, authenticated_client, create_collection_helper ): """Successfully updates collection name.""" created = create_collection_helper(name="Original Name") assert created is not None new_name = f"Updated Name {uuid.uuid4().hex[:8]}" response = authenticated_client.put( f"/library/api/collections/{created['id']}", json={"name": new_name}, content_type="application/json", ) assert response.status_code == 200 data = response.get_json() assert data["success"] is True assert data["collection"]["name"] == new_name def test_update_collection_description( self, authenticated_client, create_collection_helper ): """Successfully updates collection description.""" created = create_collection_helper(description="Original description") assert created is not None response = authenticated_client.put( f"/library/api/collections/{created['id']}", json={"description": "Updated description"}, content_type="application/json", ) assert response.status_code == 200 data = response.get_json() assert data["success"] is True assert data["collection"]["description"] == "Updated description" def test_update_collection_not_found(self, authenticated_client): """Returns 404 for non-existent collection.""" fake_id = str(uuid.uuid4()) response = authenticated_client.put( f"/library/api/collections/{fake_id}", json={"name": "Updated"}, content_type="application/json", ) assert response.status_code == 404 data = response.get_json() assert data["success"] is False assert "not found" in data["error"].lower() def test_update_collection_duplicate_name( self, authenticated_client, create_collection_helper ): """Rejects update with duplicate name.""" # Create two collections coll1 = create_collection_helper( name=f"Collection A {uuid.uuid4().hex[:8]}" ) coll2 = create_collection_helper( name=f"Collection B {uuid.uuid4().hex[:8]}" ) assert coll1 is not None and coll2 is not None # Try to rename coll2 to coll1's name response = authenticated_client.put( f"/library/api/collections/{coll2['id']}", json={"name": coll1["name"]}, content_type="application/json", ) assert response.status_code == 400 data = response.get_json() assert data["success"] is False assert "exists" in data["error"].lower() class TestDeleteCollection: """Tests for DELETE /library/api/collections/ endpoint.""" def test_delete_collection_unauthenticated(self, client): """Unauthenticated request is rejected.""" response = client.delete("/library/api/collections/some-id") assert response.status_code in [302, 401] def test_delete_collection_success(self, authenticated_client): """Successfully deletes a collection.""" # Create a collection to delete unique_name = f"To Delete {uuid.uuid4().hex[:8]}" create_response = authenticated_client.post( "/library/api/collections", json={"name": unique_name, "description": "Will be deleted"}, content_type="application/json", ) assert create_response.status_code == 200 collection_id = create_response.get_json()["collection"]["id"] # Delete it response = authenticated_client.delete( f"/library/api/collections/{collection_id}" ) assert response.status_code == 200 data = response.get_json() assert data["success"] is True # Verify it's gone list_response = authenticated_client.get("/library/api/collections") collections = list_response.get_json()["collections"] collection_ids = [c["id"] for c in collections] assert collection_id not in collection_ids def test_delete_collection_not_found(self, authenticated_client): """Returns 404 for non-existent collection.""" fake_id = str(uuid.uuid4()) response = authenticated_client.delete( f"/library/api/collections/{fake_id}" ) assert response.status_code == 404 data = response.get_json() assert data["success"] is False assert "not found" in data["error"].lower() class TestCollectionIntegration: """Integration tests for collection workflow.""" def test_create_read_update_delete_flow(self, authenticated_client): """Full CRUD workflow.""" unique_name = f"CRUD Test {uuid.uuid4().hex[:8]}" # Create create_response = authenticated_client.post( "/library/api/collections", json={"name": unique_name, "description": "Initial"}, content_type="application/json", ) assert create_response.status_code == 200 collection_id = create_response.get_json()["collection"]["id"] try: # Read list_response = authenticated_client.get("/library/api/collections") assert list_response.status_code == 200 collections = list_response.get_json()["collections"] our_coll = next( (c for c in collections if c["id"] == collection_id), None ) assert our_coll is not None assert our_coll["name"] == unique_name # Update updated_name = f"Updated {uuid.uuid4().hex[:8]}" update_response = authenticated_client.put( f"/library/api/collections/{collection_id}", json={"name": updated_name, "description": "Updated desc"}, content_type="application/json", ) assert update_response.status_code == 200 # Verify update list_response2 = authenticated_client.get( "/library/api/collections" ) collections2 = list_response2.get_json()["collections"] our_coll2 = next( (c for c in collections2 if c["id"] == collection_id), None ) assert our_coll2["name"] == updated_name assert our_coll2["description"] == "Updated desc" finally: # Delete delete_response = authenticated_client.delete( f"/library/api/collections/{collection_id}" ) assert delete_response.status_code == 200