From 21e4d3effde07bd6e6cffab64d26247303d701f1 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Mon, 12 Jan 2026 13:29:03 +0100 Subject: [PATCH] fix(deck): use correct endpoint for reorder_card to fix cross-stack moves The reorder_card method was using the API route /api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/reorder which has a parameter conflict: the URL's {stackId} (current stack) overrides the body's stackId (target stack) in Nextcloud's routing. This caused cards to stay in their original stack even when the API reported success. Switched to the non-API route /cards/{cardId}/reorder which correctly reads stackId from the request body, matching the behavior of the working curl command reported in the issue. Also added the required OCS-APIRequest headers that were missing. Fixes #469 Co-Authored-By: Claude Opus 4.5 --- nextcloud_mcp_server/client/deck.py | 8 +- tests/integration/test_deck_reorder_card.py | 177 ++++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 tests/integration/test_deck_reorder_card.py diff --git a/nextcloud_mcp_server/client/deck.py b/nextcloud_mcp_server/client/deck.py index 62b6f84..2b9d05f 100644 --- a/nextcloud_mcp_server/client/deck.py +++ b/nextcloud_mcp_server/client/deck.py @@ -386,11 +386,17 @@ class DeckClient(BaseNextcloudClient): order: int, target_stack_id: int, ) -> None: + # Use the non-API route /cards/{cardId}/reorder which correctly reads + # stackId from the body. The API route /api/.../stacks/{stackId}/cards/... + # has a parameter conflict where URL stackId overrides body stackId. + # See: https://github.com/cbcoutinho/nextcloud-mcp-server/issues/469 json_data = {"order": order, "stackId": target_stack_id} + headers = self._get_deck_headers() await self._make_request( "PUT", - f"/apps/deck/api/v1.0/boards/{board_id}/stacks/{stack_id}/cards/{card_id}/reorder", + f"/apps/deck/cards/{card_id}/reorder", json=json_data, + headers=headers, ) # Labels diff --git a/tests/integration/test_deck_reorder_card.py b/tests/integration/test_deck_reorder_card.py new file mode 100644 index 0000000..3a08315 --- /dev/null +++ b/tests/integration/test_deck_reorder_card.py @@ -0,0 +1,177 @@ +"""Integration tests for Deck card reorder functionality. + +Tests issue #469: Moving Deck card from one column (stack) to another not working. +https://github.com/cbcoutinho/nextcloud-mcp-server/issues/469 +""" + +import logging +import uuid + +import pytest + +from nextcloud_mcp_server.client import NextcloudClient + +logger = logging.getLogger(__name__) +pytestmark = pytest.mark.integration + + +@pytest.fixture +async def board_with_two_stacks(nc_client: NextcloudClient): + """Create a temporary board with two stacks for testing card movement. + + Yields: + tuple: (board_data, source_stack_data, target_stack_data) + """ + unique_suffix = uuid.uuid4().hex[:8] + board_title = f"Reorder Test Board {unique_suffix}" + board = None + + logger.info(f"Creating board with two stacks: {board_title}") + try: + board = await nc_client.deck.create_board(board_title, "0000FF") + board_id = board.id + + # Create source stack (stack 1) + source_stack = await nc_client.deck.create_stack( + board_id, f"Source Stack {unique_suffix}", order=1 + ) + source_stack_data = { + "id": source_stack.id, + "title": source_stack.title, + "order": source_stack.order, + } + logger.info(f"Created source stack with ID: {source_stack.id}") + + # Create target stack (stack 2) + target_stack = await nc_client.deck.create_stack( + board_id, f"Target Stack {unique_suffix}", order=2 + ) + target_stack_data = { + "id": target_stack.id, + "title": target_stack.title, + "order": target_stack.order, + } + logger.info(f"Created target stack with ID: {target_stack.id}") + + board_data = { + "id": board_id, + "title": board.title, + "color": board.color, + } + + yield (board_data, source_stack_data, target_stack_data) + + finally: + if board: + logger.info(f"Cleaning up board ID: {board.id}") + try: + await nc_client.deck.delete_board(board.id) + except Exception as e: + logger.warning(f"Error cleaning up board: {e}") + + +async def test_reorder_card_move_to_different_stack( + nc_client: NextcloudClient, board_with_two_stacks: tuple +): + """Test moving a card from one stack to another (issue #469). + + This test reproduces the bug where the reorder_card API reports success + but the card doesn't actually move to the target stack. + """ + board_data, source_stack_data, target_stack_data = board_with_two_stacks + board_id = board_data["id"] + source_stack_id = source_stack_data["id"] + target_stack_id = target_stack_data["id"] + + # Create a card in the source stack + unique_suffix = uuid.uuid4().hex[:8] + card_title = f"Test Card {unique_suffix}" + card = await nc_client.deck.create_card( + board_id, source_stack_id, card_title, description="Card to be moved" + ) + card_id = card.id + logger.info(f"Created card ID: {card_id} in source stack ID: {source_stack_id}") + + try: + # Verify card is in source stack + card_before = await nc_client.deck.get_card(board_id, source_stack_id, card_id) + assert card_before.stackId == source_stack_id, ( + f"Card should start in source stack {source_stack_id}, " + f"but is in {card_before.stackId}" + ) + logger.info(f"Verified card is in source stack: {source_stack_id}") + + # Move card to target stack + logger.info( + f"Moving card {card_id} from stack {source_stack_id} " + f"to stack {target_stack_id}" + ) + await nc_client.deck.reorder_card( + board_id=board_id, + stack_id=source_stack_id, + card_id=card_id, + order=0, + target_stack_id=target_stack_id, + ) + logger.info("reorder_card API call completed") + + # Verify card moved to target stack + # Note: After moving, the card should be accessible from the target stack + card_after = await nc_client.deck.get_card(board_id, target_stack_id, card_id) + assert card_after.stackId == target_stack_id, ( + f"Card should have moved to target stack {target_stack_id}, " + f"but is in {card_after.stackId}" + ) + logger.info(f"SUCCESS: Card moved to target stack {target_stack_id}") + + finally: + # Clean up - try to delete from target stack first, then source + try: + await nc_client.deck.delete_card(board_id, target_stack_id, card_id) + except Exception: + try: + await nc_client.deck.delete_card(board_id, source_stack_id, card_id) + except Exception as e: + logger.warning(f"Error cleaning up card: {e}") + + +async def test_reorder_card_within_same_stack( + nc_client: NextcloudClient, board_with_two_stacks: tuple +): + """Test reordering a card within the same stack (should work).""" + board_data, source_stack_data, _ = board_with_two_stacks + board_id = board_data["id"] + source_stack_id = source_stack_data["id"] + + # Create two cards in the source stack + unique_suffix = uuid.uuid4().hex[:8] + card1 = await nc_client.deck.create_card( + board_id, source_stack_id, f"Card 1 {unique_suffix}", order=0 + ) + card2 = await nc_client.deck.create_card( + board_id, source_stack_id, f"Card 2 {unique_suffix}", order=1 + ) + logger.info(f"Created cards {card1.id} (order 0) and {card2.id} (order 1)") + + try: + # Reorder card1 to position after card2 + await nc_client.deck.reorder_card( + board_id=board_id, + stack_id=source_stack_id, + card_id=card1.id, + order=2, # Move to position 2 + target_stack_id=source_stack_id, # Same stack + ) + logger.info(f"Reordered card {card1.id} to order 2") + + # Verify card is still in the same stack + card_after = await nc_client.deck.get_card(board_id, source_stack_id, card1.id) + assert card_after.stackId == source_stack_id + logger.info("Card reorder within same stack succeeded") + + finally: + try: + await nc_client.deck.delete_card(board_id, source_stack_id, card1.id) + await nc_client.deck.delete_card(board_id, source_stack_id, card2.id) + except Exception as e: + logger.warning(f"Error cleaning up cards: {e}")