"""Integration tests for Epic API routes""" import pytest from app import db from app.models import Epic @pytest.mark.integration class TestEpicRoutes: """Test Epic API routes""" # ========== GET /boards//epics ========== def test_get_board_epics_success( self, client, db_session, auth_headers, test_board ): """Test getting all epics for a board""" # Create epics for the board epic1 = Epic(name="Epic 1", board_id=test_board.id, color="#3b82f6", pos=1.0) epic2 = Epic(name="Epic 2", board_id=test_board.id, color="#ef4444", pos=2.0) db_session.add(epic1) db_session.add(epic2) db_session.commit() response = client.get( f"/api/boards/{test_board.id}/epics", headers=auth_headers ) assert response.status_code == 200 data = response.get_json() assert len(data) == 2 assert data[0]["name"] == "Epic 1" assert data[1]["name"] == "Epic 2" def test_get_board_epics_empty(self, client, db_session, auth_headers, test_board): """Test getting epics when none exist for board""" response = client.get( f"/api/boards/{test_board.id}/epics", headers=auth_headers ) assert response.status_code == 200 data = response.get_json() assert len(data) == 0 def test_get_board_epics_board_not_found(self, client, db_session, auth_headers): """Test getting epics for non-existent board""" response = client.get("/api/boards/99999/epics", headers=auth_headers) assert response.status_code == 404 data = response.get_json() assert "not found" in data["error"].lower() def test_get_board_epics_unauthorized(self, client, db_session, test_board): """Test getting epics without authentication""" response = client.get(f"/api/boards/{test_board.id}/epics") assert response.status_code == 401 # ========== POST /boards//epics ========== def test_create_epic_success( self, client, db_session, regular_user, auth_headers, test_board ): """Test creating a new epic successfully""" epic_data = { "name": "User Authentication", "description": "Implement login and registration", "content": [{"type": "paragraph", "children": [{"text": "Details..."}]}], "color": "#3b82f6", "pos": 1.0, "depth_limit": 5, } response = client.post( f"/api/boards/{test_board.id}/epics", headers=auth_headers, json=epic_data, ) assert response.status_code == 201 data = response.get_json() assert data["name"] == "User Authentication" assert data["description"] == "Implement login and registration" assert data["color"] == "#3b82f6" assert data["pos"] == 1.0 assert data["depth_limit"] == 5 assert data["closed"] is False assert "id" in data assert data["metrics"] == {"card_count": 0, "completed_cards_count": 0} def test_create_epic_minimal_data( self, client, db_session, auth_headers, test_board ): """Test creating epic with only required fields""" epic_data = {"name": "Minimal Epic"} response = client.post( f"/api/boards/{test_board.id}/epics", headers=auth_headers, json=epic_data, ) assert response.status_code == 201 data = response.get_json() assert data["name"] == "Minimal Epic" assert data["description"] is None assert data["color"] is None assert data["pos"] is None assert data["depth_limit"] == 5 # Model default value def test_create_epic_with_parent( self, client, db_session, auth_headers, test_board ): """Test creating epic with parent epic""" # Create parent epic parent_epic = Epic(name="Parent Epic", board_id=test_board.id) db_session.add(parent_epic) db_session.commit() epic_data = { "name": "Child Epic", "parent_epic_id": parent_epic.id, } response = client.post( f"/api/boards/{test_board.id}/epics", headers=auth_headers, json=epic_data, ) assert response.status_code == 201 data = response.get_json() assert data["parent_epic_id"] == parent_epic.id def test_create_epic_with_completed_list( self, client, db_session, auth_headers, test_board, test_list ): """Test creating epic with completed list""" epic_data = { "name": "Epic with Completed List", "completed_list_id": test_list.id, } response = client.post( f"/api/boards/{test_board.id}/epics", headers=auth_headers, json=epic_data, ) assert response.status_code == 201 data = response.get_json() assert data["completed_list_id"] == test_list.id def test_create_epic_board_not_found(self, client, db_session, auth_headers): """Test creating epic for non-existent board""" epic_data = {"name": "Test Epic"} response = client.post( "/api/boards/99999/epics", headers=auth_headers, json=epic_data, ) assert response.status_code == 404 data = response.get_json() assert "not found" in data["error"].lower() def test_create_epic_unauthorized(self, client, db_session, test_board): """Test creating epic without authentication""" epic_data = {"name": "Test Epic"} response = client.post( f"/api/boards/{test_board.id}/epics", json=epic_data, ) assert response.status_code == 401 def test_create_epic_missing_name( self, client, db_session, auth_headers, test_board ): """Test creating epic without name""" epic_data = {"description": "No name provided"} response = client.post( f"/api/boards/{test_board.id}/epics", headers=auth_headers, json=epic_data, ) assert response.status_code == 400 def test_create_epic_name_too_long( self, client, db_session, auth_headers, test_board ): """Test creating epic with name exceeding max length""" epic_data = {"name": "A" * 201} response = client.post( f"/api/boards/{test_board.id}/epics", headers=auth_headers, json=epic_data, ) assert response.status_code == 400 def test_create_epic_invalid_depth_limit( self, client, db_session, auth_headers, test_board ): """Test creating epic with invalid depth limit""" epic_data = {"name": "Test Epic", "depth_limit": 15} response = client.post( f"/api/boards/{test_board.id}/epics", headers=auth_headers, json=epic_data, ) assert response.status_code == 400 def test_create_epic_invalid_color( self, client, db_session, auth_headers, test_board ): """Test creating epic with invalid color code""" epic_data = {"name": "Test Epic", "color": "invalid-color"} response = client.post( f"/api/boards/{test_board.id}/epics", headers=auth_headers, json=epic_data, ) assert response.status_code == 400 # ========== GET /epics/ ========== def test_get_epic_success( self, client, db_session, auth_headers, test_board, test_card ): """Test getting a specific epic with details""" # Create epic and link card epic = Epic(name="Test Epic", board_id=test_board.id, color="#3b82f6") db_session.add(epic) db_session.commit() test_card.epic_id = epic.id db_session.commit() response = client.get(f"/api/epics/{epic.id}", headers=auth_headers) assert response.status_code == 200 data = response.get_json() assert data["id"] == epic.id assert data["name"] == "Test Epic" assert "cards" in data assert len(data["cards"]) == 1 assert data["cards"][0]["id"] == test_card.id def test_get_epic_with_no_cards(self, client, db_session, auth_headers, test_board): """Test getting epic with no cards""" epic = Epic(name="Empty Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() response = client.get(f"/api/epics/{epic.id}", headers=auth_headers) assert response.status_code == 200 data = response.get_json() assert data["id"] == epic.id assert "cards" in data assert len(data["cards"]) == 0 def test_get_epic_not_found(self, client, db_session, auth_headers): """Test getting non-existent epic""" response = client.get("/api/epics/99999", headers=auth_headers) assert response.status_code == 404 data = response.get_json() assert "not found" in data["error"].lower() def test_get_epic_unauthorized(self, client, db_session, test_board): """Test getting epic without authentication""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() response = client.get(f"/api/epics/{epic.id}") assert response.status_code == 401 # ========== PUT /epics/ ========== def test_update_epic_success(self, client, db_session, auth_headers, test_board): """Test updating an epic successfully""" epic = Epic( name="Original Name", board_id=test_board.id, color="#3b82f6", closed=False, ) db_session.add(epic) db_session.commit() update_data = { "name": "Updated Name", "description": "Updated description", "color": "#ef4444", "closed": True, "pos": 5.0, "depth_limit": 3, } response = client.put( f"/api/epics/{epic.id}", headers=auth_headers, json=update_data, ) assert response.status_code == 200 data = response.get_json() assert data["name"] == "Updated Name" assert data["description"] == "Updated description" assert data["color"] == "#ef4444" assert data["closed"] is True assert data["pos"] == 5.0 assert data["depth_limit"] == 3 def test_update_epic_partial(self, client, db_session, auth_headers, test_board): """Test updating epic with partial data""" epic = Epic(name="Original Name", board_id=test_board.id) db_session.add(epic) db_session.commit() update_data = {"name": "Updated Name"} response = client.put( f"/api/epics/{epic.id}", headers=auth_headers, json=update_data, ) assert response.status_code == 200 data = response.get_json() assert data["name"] == "Updated Name" # Other fields should remain unchanged assert data["description"] is None def test_update_epic_content(self, client, db_session, auth_headers, test_board): """Test updating epic content""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() new_content = [{"type": "heading", "children": [{"text": "Updated Content"}]}] response = client.put( f"/api/epics/{epic.id}", headers=auth_headers, json={"content": new_content}, ) assert response.status_code == 200 data = response.get_json() assert data["content"] == new_content def test_update_epic_not_found(self, client, db_session, auth_headers): """Test updating non-existent epic""" response = client.put( "/api/epics/99999", headers=auth_headers, json={"name": "Updated"}, ) assert response.status_code == 404 def test_update_epic_unauthorized(self, client, db_session, test_board): """Test updating epic without authentication""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() response = client.put(f"/api/epics/{epic.id}", json={"name": "Updated"}) assert response.status_code == 401 def test_update_epic_invalid_name( self, client, db_session, auth_headers, test_board ): """Test updating epic with invalid name""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() response = client.put( f"/api/epics/{epic.id}", headers=auth_headers, json={"name": ""}, ) assert response.status_code == 400 # ========== DELETE /epics/ ========== def test_delete_epic_success(self, client, db_session, auth_headers, test_board): """Test deleting an epic successfully""" epic = Epic(name="To Delete", board_id=test_board.id) db_session.add(epic) db_session.commit() epic_id = epic.id response = client.delete(f"/api/epics/{epic.id}", headers=auth_headers) assert response.status_code == 200 data = response.get_json() assert "message" in data # Verify epic is deleted deleted_epic = db.session.get(Epic, epic_id) assert deleted_epic is None def test_delete_epic_with_cards( self, client, db_session, auth_headers, test_board, test_card ): """Test deleting epic unlinks cards""" epic = Epic(name="Epic with Cards", board_id=test_board.id) db_session.add(epic) db_session.commit() # Link card to epic test_card.epic_id = epic.id db_session.commit() response = client.delete(f"/api/epics/{epic.id}", headers=auth_headers) assert response.status_code == 200 # Verify card is unlinked db.session.refresh(test_card) assert test_card.epic_id is None def test_delete_epic_not_found(self, client, db_session, auth_headers): """Test deleting non-existent epic""" response = client.delete("/api/epics/99999", headers=auth_headers) assert response.status_code == 404 def test_delete_epic_unauthorized(self, client, db_session, test_board): """Test deleting epic without authentication""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() response = client.delete(f"/api/epics/{epic.id}") assert response.status_code == 401 # ========== POST /cards//epics ========== def test_add_epic_to_card_success( self, client, db_session, auth_headers, test_board, test_card ): """Test linking an epic to a card successfully""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() response = client.post( f"/api/cards/{test_card.id}/epics", headers=auth_headers, json={"epic_id": epic.id}, ) assert response.status_code == 200 data = response.get_json() assert data["epic_id"] == epic.id # Verify card is linked db.session.refresh(test_card) assert test_card.epic_id == epic.id def test_add_epic_to_card_already_linked( self, client, db_session, auth_headers, test_board, test_card ): """Test linking epic to card that already has an epic""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() test_card.epic_id = epic.id db_session.commit() # Try to link to same epic again (should just update) response = client.post( f"/api/cards/{test_card.id}/epics", headers=auth_headers, json={"epic_id": epic.id}, ) assert response.status_code == 200 def test_add_epic_to_card_replacement( self, client, db_session, auth_headers, test_board, test_card ): """Test replacing epic on card""" epic1 = Epic(name="Epic 1", board_id=test_board.id) epic2 = Epic(name="Epic 2", board_id=test_board.id) db_session.add(epic1) db_session.add(epic2) db_session.commit() test_card.epic_id = epic1.id db_session.commit() # Replace with epic2 response = client.post( f"/api/cards/{test_card.id}/epics", headers=auth_headers, json={"epic_id": epic2.id}, ) assert response.status_code == 200 # Verify card is linked to epic2 db.session.refresh(test_card) assert test_card.epic_id == epic2.id def test_add_epic_to_card_not_found(self, client, db_session, auth_headers): """Test linking epic to non-existent card""" response = client.post( "/api/cards/99999/epics", headers=auth_headers, json={"epic_id": 1}, ) assert response.status_code == 404 def test_add_epic_to_card_epic_not_found( self, client, db_session, auth_headers, test_card ): """Test linking non-existent epic to card""" response = client.post( f"/api/cards/{test_card.id}/epics", headers=auth_headers, json={"epic_id": 99999}, ) assert response.status_code == 404 def test_add_epic_to_card_missing_epic_id( self, client, db_session, auth_headers, test_card ): """Test linking epic to card without epic_id""" response = client.post( f"/api/cards/{test_card.id}/epics", headers=auth_headers, json={}, ) assert response.status_code == 400 data = response.get_json() assert "required" in data["error"].lower() def test_add_epic_to_card_unauthorized(self, client, db_session, test_card): """Test linking epic to card without authentication""" response = client.post( f"/api/cards/{test_card.id}/epics", json={"epic_id": 1}, ) assert response.status_code == 401 # ========== DELETE /cards//epics/ ========== def test_remove_epic_from_card_success( self, client, db_session, auth_headers, test_board, test_card ): """Test unlinking an epic from a card successfully""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() test_card.epic_id = epic.id db_session.commit() response = client.delete( f"/api/cards/{test_card.id}/epics/{epic.id}", headers=auth_headers, ) assert response.status_code == 200 data = response.get_json() assert "message" in data # Verify card is unlinked db.session.refresh(test_card) assert test_card.epic_id is None def test_remove_epic_from_card_card_not_found( self, client, db_session, auth_headers ): """Test unlinking epic from non-existent card""" response = client.delete( "/api/cards/99999/epics/1", headers=auth_headers, ) assert response.status_code == 404 def test_remove_epic_from_card_wrong_epic( self, client, db_session, auth_headers, test_board, test_card ): """Test unlinking wrong epic from card""" epic1 = Epic(name="Epic 1", board_id=test_board.id) epic2 = Epic(name="Epic 2", board_id=test_board.id) db_session.add(epic1) db_session.add(epic2) db_session.commit() test_card.epic_id = epic1.id db_session.commit() # Try to unlink epic2 (but card has epic1) response = client.delete( f"/api/cards/{test_card.id}/epics/{epic2.id}", headers=auth_headers, ) assert response.status_code == 400 data = response.get_json() assert "not linked" in data["error"].lower() def test_remove_epic_from_card_no_epic( self, client, db_session, auth_headers, test_board, test_card ): """Test unlinking epic from card that has no epic""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() # Card has no epic linked assert test_card.epic_id is None response = client.delete( f"/api/cards/{test_card.id}/epics/{epic.id}", headers=auth_headers, ) assert response.status_code == 400 data = response.get_json() assert "not linked" in data["error"].lower() def test_remove_epic_from_card_unauthorized( self, client, db_session, test_board, test_card ): """Test unlinking epic from card without authentication""" epic = Epic(name="Test Epic", board_id=test_board.id) db_session.add(epic) db_session.commit() test_card.epic_id = epic.id db_session.commit() response = client.delete(f"/api/cards/{test_card.id}/epics/{epic.id}") assert response.status_code == 401 # ========== Epic Hierarchy Tests ========== def test_epic_hierarchy(self, client, db_session, auth_headers, test_board): """Test creating nested epic hierarchy""" # Create parent epic parent_epic = Epic(name="Parent Epic", board_id=test_board.id) db_session.add(parent_epic) db_session.commit() # Create child epic child_epic = Epic( name="Child Epic", board_id=test_board.id, parent_epic_id=parent_epic.id, ) db_session.add(child_epic) db_session.commit() # Verify hierarchy response = client.get( f"/api/epics/{parent_epic.id}", headers=auth_headers, ) assert response.status_code == 200 data = response.get_json() assert data["parent_epic_id"] is None response = client.get( f"/api/epics/{child_epic.id}", headers=auth_headers, ) assert response.status_code == 200 data = response.get_json() assert data["parent_epic_id"] == parent_epic.id