Skip to content

Commit 6624df7

Browse files
committed
Address CodeRabbit: tighten pagination, skip when circles disabled, actually exercise join_circle/leave_circle
1 parent 9d3a48c commit 6624df7

2 files changed

Lines changed: 81 additions & 17 deletions

File tree

PROGRESS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@
9797
| File Helpers || 26 |
9898
| File Reminders | 3 | 20 |
9999
| Forms | 25 | 34 |
100-
| Circles | 14 | 20 |
101-
| **Total** | **141** | **825** |
100+
| Circles | 14 | 23 |
101+
| **Total** | **141** | **828** |
102102

103103
Files shows 10, but one (`upload_file_from_path`) is only registered when
104104
`NEXTCLOUD_MCP_UPLOAD_ROOT` is configured. Default deployments expose 140 tools.

tests/integration/test_circles.py

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,49 @@
22

33
import contextlib
44
import json
5-
from collections.abc import AsyncGenerator
5+
from collections.abc import AsyncGenerator, AsyncIterator
66
from typing import Any
77

8+
import niquests
89
import pytest
910
from mcp.server.fastmcp.exceptions import ToolError
1011

1112
from nc_mcp_server.client import NextcloudClient, NextcloudError
1213
from nc_mcp_server.config import Config
14+
from nc_mcp_server.state import get_client, get_config, set_state
1315

1416
from .conftest import McpTestHelper
1517

1618
pytestmark = pytest.mark.integration
1719

1820
CIRCLE_TEST_USER = "mcp-circle-test-user"
21+
CIRCLE_TEST_PWD = "mcp-Circle-Test-PWD-9X!"
22+
23+
24+
@pytest.fixture(scope="session")
25+
def _circles_available(_cleanup_config: Config) -> bool:
26+
"""Probe whether the Circles app is enabled on the target Nextcloud.
27+
28+
Sync-only check so we can return a plain bool at session scope without
29+
fighting pytest-asyncio's per-test event loop.
30+
"""
31+
try:
32+
resp = niquests.get(
33+
f"{_cleanup_config.nextcloud_url}/ocs/v2.php/apps/circles/circles",
34+
auth=(_cleanup_config.user, _cleanup_config.password),
35+
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
36+
timeout=5,
37+
)
38+
except (OSError, niquests.exceptions.RequestException):
39+
return False
40+
return resp.ok
41+
42+
43+
@pytest.fixture(autouse=True)
44+
def _skip_if_no_circles(_circles_available: bool) -> None:
45+
"""Skip every Circles test when the app isn't enabled on the target instance."""
46+
if not _circles_available:
47+
pytest.skip("Circles app is not enabled on this Nextcloud instance")
1948

2049

2150
@pytest.fixture
@@ -25,14 +54,41 @@ async def circle_peer(nc_config: Config) -> AsyncGenerator[str]:
2554
with contextlib.suppress(Exception):
2655
await client.ocs_post(
2756
"cloud/users",
28-
data={"userid": CIRCLE_TEST_USER, "password": "mcp-Circle-Test-PWD-9X!"},
57+
data={"userid": CIRCLE_TEST_USER, "password": CIRCLE_TEST_PWD},
2958
)
3059
yield CIRCLE_TEST_USER
3160
with contextlib.suppress(Exception):
3261
await client.ocs_delete(f"cloud/users/{CIRCLE_TEST_USER}")
3362
await client.close()
3463

3564

65+
@contextlib.asynccontextmanager
66+
async def _as_peer(user_id: str, password: str) -> AsyncIterator[None]:
67+
"""Temporarily swap the global state so tool calls authenticate as the given user.
68+
69+
Restores the prior state on exit. Tools (`create_server`, `get_client`) read the
70+
client from module-global state, so swapping it lets the existing McpTestHelper
71+
exercise real tools under a different user's credentials without needing a
72+
second server instance.
73+
"""
74+
admin_client = get_client()
75+
admin_config = get_config()
76+
peer_config = Config(
77+
nextcloud_url=admin_config.nextcloud_url,
78+
user=user_id,
79+
password=password,
80+
permission_level=admin_config.permission_level,
81+
)
82+
peer_config.validate()
83+
peer_client = NextcloudClient(peer_config)
84+
set_state(peer_client, peer_config)
85+
try:
86+
yield
87+
finally:
88+
set_state(admin_client, admin_config)
89+
await peer_client.close()
90+
91+
3692
async def _make_circle(nc_mcp: McpTestHelper, name: str) -> dict[str, Any]:
3793
"""Create a circle and return its dict."""
3894
created: dict[str, Any] = json.loads(await nc_mcp.call("create_circle", name=name))
@@ -52,7 +108,7 @@ async def test_list_pagination(self, nc_mcp: McpTestHelper) -> None:
52108
for i in range(3):
53109
await _make_circle(nc_mcp, f"mcp-test-circle-page-{i}")
54110
circles: list[dict[str, Any]] = json.loads(await nc_mcp.call("list_circles", limit=2))
55-
assert len(circles) <= 2
111+
assert len(circles) == 2
56112

57113

58114
class TestCircleLifecycle:
@@ -187,18 +243,26 @@ async def test_remove_member(self, nc_mcp: McpTestHelper, circle_peer: str) -> N
187243

188244
class TestJoinLeave:
189245
@pytest.mark.asyncio
190-
async def test_leave_as_non_owner(self, nc_mcp: McpTestHelper, circle_peer: str) -> None:
191-
"""Admin adds peer then removes peer — membership disappears."""
192-
admin = nc_mcp
193-
circle = json.loads(await admin.call("create_circle", name="mcp-test-circle-leave"))
194-
peer_member = json.loads(await admin.call("add_circle_member", circle_id=circle["id"], user_id=circle_peer))
195-
# Admin removes peer (equivalent to peer leaving, same effect)
196-
await admin.call(
197-
"remove_circle_member",
198-
circle_id=circle["id"],
199-
member_id=peer_member["id"],
200-
)
201-
members: list[dict[str, Any]] = json.loads(await admin.call("list_circle_members", circle_id=circle["id"]))
246+
async def test_join_open_circle(self, nc_mcp: McpTestHelper, circle_peer: str) -> None:
247+
"""Admin creates an OPEN circle; peer calls join_circle and appears in members."""
248+
circle = await _make_circle(nc_mcp, "mcp-test-circle-open-join")
249+
# 24 = VISIBLE(8) | OPEN(16) — required for peer to join via join_circle
250+
await nc_mcp.call("update_circle_config", circle_id=circle["id"], config=24)
251+
async with _as_peer(circle_peer, CIRCLE_TEST_PWD):
252+
joined = json.loads(await nc_mcp.call("join_circle", circle_id=circle["id"]))
253+
assert joined["userId"] == circle_peer
254+
assert joined["status"] == "Member"
255+
members: list[dict[str, Any]] = json.loads(await nc_mcp.call("list_circle_members", circle_id=circle["id"]))
256+
assert any(m.get("userId") == circle_peer for m in members)
257+
258+
@pytest.mark.asyncio
259+
async def test_leave_as_member(self, nc_mcp: McpTestHelper, circle_peer: str) -> None:
260+
"""Admin adds peer; peer calls leave_circle; peer is gone from member list."""
261+
circle = await _make_circle(nc_mcp, "mcp-test-circle-leave")
262+
await nc_mcp.call("add_circle_member", circle_id=circle["id"], user_id=circle_peer)
263+
async with _as_peer(circle_peer, CIRCLE_TEST_PWD):
264+
await nc_mcp.call("leave_circle", circle_id=circle["id"])
265+
members: list[dict[str, Any]] = json.loads(await nc_mcp.call("list_circle_members", circle_id=circle["id"]))
202266
assert not any(m.get("userId") == circle_peer for m in members)
203267

204268

0 commit comments

Comments
 (0)