Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a26a470af6 | |||
| 71ace47197 | |||
| 30d3d9f0cf | |||
| 48a4182ef9 | |||
| 13dd709fc2 |
@@ -1,6 +1,6 @@
|
|||||||
[tool.commitizen]
|
[tool.commitizen]
|
||||||
name = "cz_conventional_commits"
|
name = "cz_conventional_commits"
|
||||||
version = "0.56.1"
|
version = "0.56.2"
|
||||||
tag_format = "nextcloud-mcp-server-$version"
|
tag_format = "nextcloud-mcp-server-$version"
|
||||||
version_scheme = "semver"
|
version_scheme = "semver"
|
||||||
update_changelog_on_bump = true
|
update_changelog_on_bump = true
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Configurable resource limits
|
- Configurable resource limits
|
||||||
- Grafana dashboard annotations
|
- Grafana dashboard annotations
|
||||||
|
|
||||||
|
## nextcloud-mcp-server-0.56.2 (2025-12-29)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- **oauth**: Enable browser OAuth routes for Management API in hybrid mode
|
||||||
|
|
||||||
## nextcloud-mcp-server-0.56.1 (2025-12-26)
|
## nextcloud-mcp-server-0.56.1 (2025-12-26)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ apiVersion: v2
|
|||||||
name: nextcloud-mcp-server
|
name: nextcloud-mcp-server
|
||||||
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
||||||
type: application
|
type: application
|
||||||
version: 0.56.1
|
version: 0.56.2
|
||||||
appVersion: "0.60.2"
|
appVersion: "0.60.2"
|
||||||
keywords:
|
keywords:
|
||||||
- nextcloud
|
- nextcloud
|
||||||
|
|||||||
+3
-1
@@ -8,6 +8,8 @@ services:
|
|||||||
command: --transaction-isolation=READ-COMMITTED
|
command: --transaction-isolation=READ-COMMITTED
|
||||||
volumes:
|
volumes:
|
||||||
- db:/var/lib/mysql
|
- db:/var/lib/mysql
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:3306:3306
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_ROOT_PASSWORD=password
|
- MYSQL_ROOT_PASSWORD=password
|
||||||
- MYSQL_PASSWORD=password
|
- MYSQL_PASSWORD=password
|
||||||
@@ -24,7 +26,7 @@ services:
|
|||||||
image: docker.io/library/nextcloud:32.0.3@sha256:53231a9fb9233af2c15bfe70fc03ebe639fd53243fa42a9369884b1e0008deae
|
image: docker.io/library/nextcloud:32.0.3@sha256:53231a9fb9233af2c15bfe70fc03ebe639fd53243fa42a9369884b1e0008deae
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 0.0.0.0:8080:80
|
- 127.0.0.1:8080:80
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- db
|
- db
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -559,3 +559,259 @@ async def test_multi_user_astrolabe_background_sync_enablement(
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"\n✓ All {len(test_users)} users successfully enabled background sync via app passwords!"
|
f"\n✓ All {len(test_users)} users successfully enabled background sync via app passwords!"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def revoke_background_sync_access(page: Page, username: str) -> bool:
|
||||||
|
"""Revoke background sync access by clicking the Revoke Access button.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Playwright page instance (must be authenticated)
|
||||||
|
username: Username (for logging)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if revocation was successful
|
||||||
|
"""
|
||||||
|
logger.info(f"Revoking background sync access for {username}...")
|
||||||
|
|
||||||
|
nextcloud_url = "http://localhost:8080"
|
||||||
|
|
||||||
|
# Set up network request and console listeners
|
||||||
|
network_requests = []
|
||||||
|
network_responses = []
|
||||||
|
console_messages = []
|
||||||
|
|
||||||
|
def log_request(req):
|
||||||
|
network_requests.append(f"{req.method} {req.url}")
|
||||||
|
|
||||||
|
def log_response(resp):
|
||||||
|
response_info = f"{resp.status} {resp.url}"
|
||||||
|
network_responses.append(response_info)
|
||||||
|
logger.info(f"Response: {response_info}")
|
||||||
|
|
||||||
|
def log_console(msg):
|
||||||
|
console_messages.append(f"[{msg.type}] {msg.text}")
|
||||||
|
|
||||||
|
page.on("request", log_request)
|
||||||
|
page.on("response", log_response)
|
||||||
|
page.on("console", log_console)
|
||||||
|
|
||||||
|
# Navigate to Astrolabe settings
|
||||||
|
await page.goto(
|
||||||
|
f"{nextcloud_url}/settings/user/astrolabe", wait_until="networkidle"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for page to load
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
# Check if "Active" badge is visible (indicating background sync is enabled)
|
||||||
|
try:
|
||||||
|
active_text = page.get_by_text("Active", exact=True)
|
||||||
|
if not await active_text.is_visible(timeout=2000):
|
||||||
|
logger.warning(
|
||||||
|
f"Background sync not active for {username}, nothing to revoke"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
logger.warning(f"Could not find Active badge for {username}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Find the "Revoke Access" button
|
||||||
|
revoke_button = page.get_by_role("button", name="Revoke Access")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await revoke_button.wait_for(timeout=5000, state="visible")
|
||||||
|
logger.info("Found Revoke Access button")
|
||||||
|
except Exception:
|
||||||
|
screenshot_path = f"/tmp/astrolabe_no_revoke_button_{username}.png"
|
||||||
|
await page.screenshot(path=screenshot_path)
|
||||||
|
raise ValueError(
|
||||||
|
f"Could not find Revoke Access button for {username}. Screenshot: {screenshot_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up dialog handler for confirmation dialog
|
||||||
|
page.once("dialog", lambda dialog: dialog.accept())
|
||||||
|
|
||||||
|
# Click the Revoke Access button
|
||||||
|
await revoke_button.click()
|
||||||
|
logger.info("Clicked Revoke Access button")
|
||||||
|
|
||||||
|
# Wait for the request to complete and page to reload
|
||||||
|
await page.wait_for_load_state("networkidle", timeout=15000)
|
||||||
|
await anyio.sleep(2)
|
||||||
|
|
||||||
|
# Log network requests after clicking
|
||||||
|
logger.info(f"Network requests after Revoke for {username}:")
|
||||||
|
for req in network_requests[-10:]:
|
||||||
|
logger.info(f" {req}")
|
||||||
|
|
||||||
|
# Log network responses
|
||||||
|
logger.info(f"Network responses after Revoke for {username}:")
|
||||||
|
for resp in network_responses[-10:]:
|
||||||
|
logger.info(f" {resp}")
|
||||||
|
|
||||||
|
# Check specifically for the revoke POST response
|
||||||
|
revoke_responses = [r for r in network_responses if "credentials/revoke" in r]
|
||||||
|
if revoke_responses:
|
||||||
|
logger.info(f"Revoke endpoint response: {revoke_responses[-1]}")
|
||||||
|
if "200" not in revoke_responses[-1]:
|
||||||
|
logger.error(f"Revoke POST did not return 200 OK: {revoke_responses[-1]}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.warning("No response found for credentials/revoke endpoint!")
|
||||||
|
# Take screenshot for debugging
|
||||||
|
screenshot_path = f"/tmp/astrolabe_revoke_no_response_{username}.png"
|
||||||
|
await page.screenshot(path=screenshot_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Log any console messages
|
||||||
|
if console_messages:
|
||||||
|
logger.info(f"Console messages for {username}:")
|
||||||
|
for msg in console_messages:
|
||||||
|
logger.info(f" {msg}")
|
||||||
|
|
||||||
|
# Check for error notifications (toast messages)
|
||||||
|
try:
|
||||||
|
error_toast = page.locator(".toastify.toast-error, .toast-error")
|
||||||
|
if await error_toast.count() > 0:
|
||||||
|
error_text = await error_toast.first.text_content()
|
||||||
|
logger.error(f"Error notification for {username}: {error_text}")
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Verify "Active" badge is no longer visible
|
||||||
|
try:
|
||||||
|
active_text = page.get_by_text("Active", exact=True)
|
||||||
|
if await active_text.is_visible(timeout=2000):
|
||||||
|
logger.error(f"Active badge still visible for {username} after revoke!")
|
||||||
|
screenshot_path = f"/tmp/astrolabe_revoke_still_active_{username}.png"
|
||||||
|
await page.screenshot(path=screenshot_path)
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info(f"✓ Background sync access revoked for {username}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_app_password_deleted(username: str) -> bool:
|
||||||
|
"""Verify that background sync app password was deleted for the user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: Nextcloud username
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if background sync credentials no longer exist
|
||||||
|
"""
|
||||||
|
logger.info(f"Verifying background sync credentials deleted for {username}...")
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT userid, configkey, configvalue
|
||||||
|
FROM oc_preferences
|
||||||
|
WHERE userid = '{username}'
|
||||||
|
AND appid = 'astrolabe'
|
||||||
|
AND configkey IN ('background_sync_password', 'background_sync_type', 'background_sync_provisioned_at')
|
||||||
|
ORDER BY configkey;
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"exec",
|
||||||
|
"-T",
|
||||||
|
"db",
|
||||||
|
"mariadb",
|
||||||
|
"-u",
|
||||||
|
"root",
|
||||||
|
"-ppassword",
|
||||||
|
"nextcloud",
|
||||||
|
"-e",
|
||||||
|
query,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
output = result.stdout
|
||||||
|
logger.debug(f"Background sync credentials query result:\n{output}")
|
||||||
|
|
||||||
|
# After deletion, we should NOT see background_sync_password
|
||||||
|
if "background_sync_password" not in output:
|
||||||
|
logger.info(f"✓ Background sync credentials deleted for {username}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"Background sync credentials still exist for {username}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking background sync credentials for {username}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.oauth
|
||||||
|
async def test_revoke_background_sync_access(
|
||||||
|
browser,
|
||||||
|
nc_client,
|
||||||
|
test_users_setup,
|
||||||
|
configure_astrolabe_for_mcp_server,
|
||||||
|
):
|
||||||
|
"""Test that users can revoke background sync access via the Revoke Access button.
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
1. User enables background sync via app password
|
||||||
|
2. User clicks "Revoke Access" button
|
||||||
|
3. Confirmation dialog is handled
|
||||||
|
4. POST request is sent to /api/v1/background-sync/credentials/revoke
|
||||||
|
5. "Active" badge disappears from settings page
|
||||||
|
6. Background sync credentials are deleted from database
|
||||||
|
|
||||||
|
This tests the fix for the issue where POST requests to the revoke endpoint
|
||||||
|
were returning errors due to HTTP method mismatch (was DELETE, now POST).
|
||||||
|
"""
|
||||||
|
# Configure Astrolabe to point to the mcp-multi-user-basic server
|
||||||
|
logger.info("Configuring Astrolabe for mcp-multi-user-basic server...")
|
||||||
|
await configure_astrolabe_for_mcp_server(
|
||||||
|
mcp_server_internal_url="http://mcp-multi-user-basic:8000",
|
||||||
|
mcp_server_public_url="http://localhost:8003",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with a single user for this specific test
|
||||||
|
username = "alice"
|
||||||
|
user_config = test_users_setup[username]
|
||||||
|
password = user_config["password"]
|
||||||
|
|
||||||
|
# Create new browser context
|
||||||
|
context = await browser.new_context(ignore_https_errors=True)
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: Login to Nextcloud
|
||||||
|
await login_to_nextcloud(page, username, password)
|
||||||
|
|
||||||
|
# Step 2: Generate app password and enable background sync
|
||||||
|
app_password = await generate_app_password(page, username)
|
||||||
|
await enable_background_sync_via_app_password(page, username, app_password)
|
||||||
|
|
||||||
|
# Step 3: Verify background sync is enabled
|
||||||
|
assert await verify_app_password_created(username), (
|
||||||
|
f"Background sync not enabled for {username}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 4: Revoke background sync access
|
||||||
|
revoke_success = await revoke_background_sync_access(page, username)
|
||||||
|
assert revoke_success, f"Failed to revoke background sync access for {username}"
|
||||||
|
|
||||||
|
# Step 5: Verify credentials are deleted from database
|
||||||
|
credentials_deleted = await verify_app_password_deleted(username)
|
||||||
|
assert credentials_deleted, (
|
||||||
|
f"Background sync credentials not deleted for {username}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"\n✓ Successfully revoked background sync access for {username}!")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await context.close()
|
||||||
|
|||||||
+2
-2
@@ -47,8 +47,8 @@ return [
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'credentials#deleteCredentials',
|
'name' => 'credentials#deleteCredentials',
|
||||||
'url' => '/api/v1/background-sync/credentials',
|
'url' => '/api/v1/background-sync/credentials/revoke',
|
||||||
'verb' => 'DELETE',
|
'verb' => 'POST',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'credentials#getStatus',
|
'name' => 'credentials#getStatus',
|
||||||
|
|||||||
Reference in New Issue
Block a user