Merge pull request #453 from cbcoutinho/fix/452
fix: DeckClient.update_card partial update bugs
This commit is contained in:
@@ -285,28 +285,23 @@ class DeckClient(BaseNextcloudClient):
|
|||||||
archived: Optional[bool] = None,
|
archived: Optional[bool] = None,
|
||||||
done: Optional[str] = None,
|
done: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# First, get the current card to use existing values for required fields
|
# Deck PUT API is a full replacement - all required fields must be sent.
|
||||||
|
# Fetch current card to preserve values for fields not being updated.
|
||||||
current_card = await self.get_card(board_id, stack_id, card_id)
|
current_card = await self.get_card(board_id, stack_id, card_id)
|
||||||
|
|
||||||
json_data = {}
|
# Build payload with required fields always included
|
||||||
if title is not None:
|
json_data = {
|
||||||
json_data["title"] = title
|
# Title is required by the API
|
||||||
if description is not None:
|
"title": title if title is not None else current_card.title,
|
||||||
json_data["description"] = description
|
# Type is required by the API
|
||||||
# Type is required by the API, use provided or keep current
|
"type": type if type is not None else current_card.type,
|
||||||
json_data["type"] = type if type is not None else current_card.type
|
# Owner is required by the API (model validator ensures it's a string)
|
||||||
# Owner is required by the API, use provided or keep current
|
"owner": owner if owner is not None else current_card.owner,
|
||||||
json_data["owner"] = (
|
# Description must be sent to preserve it (PUT clears omitted fields)
|
||||||
owner
|
"description": description
|
||||||
if owner is not None
|
if description is not None
|
||||||
else (
|
else (current_card.description or ""),
|
||||||
current_card.owner
|
}
|
||||||
if isinstance(current_card.owner, str)
|
|
||||||
else current_card.owner.uid
|
|
||||||
if hasattr(current_card.owner, "uid")
|
|
||||||
else current_card.owner.primaryKey
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if order is not None:
|
if order is not None:
|
||||||
json_data["order"] = order
|
json_data["order"] = order
|
||||||
if duedate is not None:
|
if duedate is not None:
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
"""
|
||||||
|
Integration tests for DeckClient.update_card API behavior.
|
||||||
|
|
||||||
|
These tests define the EXPECTED behavior for partial card updates:
|
||||||
|
- Only fields explicitly passed should be modified
|
||||||
|
- All other fields should be preserved unchanged
|
||||||
|
|
||||||
|
Related issues:
|
||||||
|
- nextcloud-mcp-server #452: DeckClient.update_card partial update bugs
|
||||||
|
- deck #3127: REST API Docs: missing parameter in "update cards"
|
||||||
|
- deck #4106: Provide a working example of API usage to update a cards details
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.integration]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def deck_test_card(nc_client):
|
||||||
|
"""Create a board, stack, and card for testing, cleanup after."""
|
||||||
|
board = await nc_client.deck.create_board("Test Update Card API", "FF0000")
|
||||||
|
stack = await nc_client.deck.create_stack(board.id, "Test Stack", 1)
|
||||||
|
card = await nc_client.deck.create_card(
|
||||||
|
board.id,
|
||||||
|
stack.id,
|
||||||
|
"Original Title",
|
||||||
|
type="plain",
|
||||||
|
description="Original description",
|
||||||
|
)
|
||||||
|
|
||||||
|
yield {
|
||||||
|
"board_id": board.id,
|
||||||
|
"stack_id": stack.id,
|
||||||
|
"card_id": card.id,
|
||||||
|
"card": card,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await nc_client.deck.delete_board(board.id)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeckClientUpdateCard:
|
||||||
|
"""
|
||||||
|
Test DeckClient.update_card() partial update behavior.
|
||||||
|
|
||||||
|
Expected: Only explicitly provided fields are updated, all others preserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def test_update_title_only_preserves_description(
|
||||||
|
self, nc_client, deck_test_card
|
||||||
|
):
|
||||||
|
"""Updating only the title should preserve the description."""
|
||||||
|
await nc_client.deck.update_card(
|
||||||
|
board_id=deck_test_card["board_id"],
|
||||||
|
stack_id=deck_test_card["stack_id"],
|
||||||
|
card_id=deck_test_card["card_id"],
|
||||||
|
title="New Title",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await nc_client.deck.get_card(
|
||||||
|
deck_test_card["board_id"],
|
||||||
|
deck_test_card["stack_id"],
|
||||||
|
deck_test_card["card_id"],
|
||||||
|
)
|
||||||
|
assert updated.title == "New Title"
|
||||||
|
assert updated.description == "Original description"
|
||||||
|
|
||||||
|
async def test_update_description_only(self, nc_client, deck_test_card):
|
||||||
|
"""Updating only the description should work and preserve other fields."""
|
||||||
|
await nc_client.deck.update_card(
|
||||||
|
board_id=deck_test_card["board_id"],
|
||||||
|
stack_id=deck_test_card["stack_id"],
|
||||||
|
card_id=deck_test_card["card_id"],
|
||||||
|
description="New description only",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await nc_client.deck.get_card(
|
||||||
|
deck_test_card["board_id"],
|
||||||
|
deck_test_card["stack_id"],
|
||||||
|
deck_test_card["card_id"],
|
||||||
|
)
|
||||||
|
assert updated.title == "Original Title"
|
||||||
|
assert updated.description == "New description only"
|
||||||
|
|
||||||
|
async def test_update_title_and_description(self, nc_client, deck_test_card):
|
||||||
|
"""Updating title and description together should work."""
|
||||||
|
await nc_client.deck.update_card(
|
||||||
|
board_id=deck_test_card["board_id"],
|
||||||
|
stack_id=deck_test_card["stack_id"],
|
||||||
|
card_id=deck_test_card["card_id"],
|
||||||
|
title="New Title",
|
||||||
|
description="New description",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await nc_client.deck.get_card(
|
||||||
|
deck_test_card["board_id"],
|
||||||
|
deck_test_card["stack_id"],
|
||||||
|
deck_test_card["card_id"],
|
||||||
|
)
|
||||||
|
assert updated.title == "New Title"
|
||||||
|
assert updated.description == "New description"
|
||||||
|
|
||||||
|
async def test_update_duedate_only(self, nc_client, deck_test_card):
|
||||||
|
"""Updating only the duedate should work and preserve other fields."""
|
||||||
|
await nc_client.deck.update_card(
|
||||||
|
board_id=deck_test_card["board_id"],
|
||||||
|
stack_id=deck_test_card["stack_id"],
|
||||||
|
card_id=deck_test_card["card_id"],
|
||||||
|
duedate="2025-12-31T23:59:59+00:00",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await nc_client.deck.get_card(
|
||||||
|
deck_test_card["board_id"],
|
||||||
|
deck_test_card["stack_id"],
|
||||||
|
deck_test_card["card_id"],
|
||||||
|
)
|
||||||
|
assert updated.title == "Original Title"
|
||||||
|
assert updated.description == "Original description"
|
||||||
|
assert updated.duedate is not None
|
||||||
|
|
||||||
|
async def test_update_archived_only(self, nc_client, deck_test_card):
|
||||||
|
"""Updating only the archived status should work and preserve other fields."""
|
||||||
|
await nc_client.deck.update_card(
|
||||||
|
board_id=deck_test_card["board_id"],
|
||||||
|
stack_id=deck_test_card["stack_id"],
|
||||||
|
card_id=deck_test_card["card_id"],
|
||||||
|
archived=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await nc_client.deck.get_card(
|
||||||
|
deck_test_card["board_id"],
|
||||||
|
deck_test_card["stack_id"],
|
||||||
|
deck_test_card["card_id"],
|
||||||
|
)
|
||||||
|
assert updated.title == "Original Title"
|
||||||
|
assert updated.description == "Original description"
|
||||||
|
assert updated.archived is True
|
||||||
|
|
||||||
|
async def test_update_order_only(self, nc_client, deck_test_card):
|
||||||
|
"""Updating only the order should work and preserve other fields."""
|
||||||
|
await nc_client.deck.update_card(
|
||||||
|
board_id=deck_test_card["board_id"],
|
||||||
|
stack_id=deck_test_card["stack_id"],
|
||||||
|
card_id=deck_test_card["card_id"],
|
||||||
|
order=99,
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await nc_client.deck.get_card(
|
||||||
|
deck_test_card["board_id"],
|
||||||
|
deck_test_card["stack_id"],
|
||||||
|
deck_test_card["card_id"],
|
||||||
|
)
|
||||||
|
assert updated.title == "Original Title"
|
||||||
|
assert updated.description == "Original description"
|
||||||
|
assert updated.order == 99
|
||||||
|
|
||||||
|
async def test_update_preserves_type(self, nc_client, deck_test_card):
|
||||||
|
"""Type should be preserved when not explicitly changed."""
|
||||||
|
original = deck_test_card["card"]
|
||||||
|
|
||||||
|
await nc_client.deck.update_card(
|
||||||
|
board_id=deck_test_card["board_id"],
|
||||||
|
stack_id=deck_test_card["stack_id"],
|
||||||
|
card_id=deck_test_card["card_id"],
|
||||||
|
title="Changed Title",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await nc_client.deck.get_card(
|
||||||
|
deck_test_card["board_id"],
|
||||||
|
deck_test_card["stack_id"],
|
||||||
|
deck_test_card["card_id"],
|
||||||
|
)
|
||||||
|
assert updated.type == original.type
|
||||||
|
assert updated.description == "Original description"
|
||||||
|
|
||||||
|
async def test_update_preserves_owner(self, nc_client, deck_test_card):
|
||||||
|
"""Owner should be preserved when not explicitly changed."""
|
||||||
|
original = deck_test_card["card"]
|
||||||
|
|
||||||
|
await nc_client.deck.update_card(
|
||||||
|
board_id=deck_test_card["board_id"],
|
||||||
|
stack_id=deck_test_card["stack_id"],
|
||||||
|
card_id=deck_test_card["card_id"],
|
||||||
|
title="Changed Title",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = await nc_client.deck.get_card(
|
||||||
|
deck_test_card["board_id"],
|
||||||
|
deck_test_card["stack_id"],
|
||||||
|
deck_test_card["card_id"],
|
||||||
|
)
|
||||||
|
assert updated.owner == original.owner
|
||||||
|
assert updated.description == "Original description"
|
||||||
Reference in New Issue
Block a user