11"""Integration tests for Circles (Teams) tools against a real Nextcloud instance."""
22
3+ import contextlib
34import json
5+ from collections .abc import AsyncGenerator
46from typing import Any
57
68import pytest
79from mcp .server .fastmcp .exceptions import ToolError
810
9- from nc_mcp_server .client import NextcloudError
11+ from nc_mcp_server .client import NextcloudClient , NextcloudError
12+ from nc_mcp_server .config import Config
1013
1114from .conftest import McpTestHelper
1215
1316pytestmark = pytest .mark .integration
1417
18+ CIRCLE_TEST_USER = "mcp-circle-test-user"
19+
20+
21+ @pytest .fixture
22+ async def circle_peer (nc_config : Config ) -> AsyncGenerator [str ]:
23+ """Ensure a second user exists for membership tests. Yields the userid."""
24+ client = NextcloudClient (nc_config )
25+ with contextlib .suppress (Exception ):
26+ await client .ocs_post (
27+ "cloud/users" ,
28+ data = {"userid" : CIRCLE_TEST_USER , "password" : "mcp-Circle-Test-PWD-9X!" },
29+ )
30+ yield CIRCLE_TEST_USER
31+ with contextlib .suppress (Exception ):
32+ await client .ocs_delete (f"cloud/users/{ CIRCLE_TEST_USER } " )
33+ await client .close ()
34+
1535
1636async def _make_circle (nc_mcp : McpTestHelper , name : str ) -> dict [str , Any ]:
1737 """Create a circle and return its dict."""
@@ -105,12 +125,12 @@ async def test_owner_present_in_members(self, nc_mcp: McpTestHelper) -> None:
105125 assert admin_member ["level" ] == 9
106126
107127 @pytest .mark .asyncio
108- async def test_add_and_list_member (self , nc_mcp : McpTestHelper ) -> None :
128+ async def test_add_and_list_member (self , nc_mcp : McpTestHelper , circle_peer : str ) -> None :
109129 circle = await _make_circle (nc_mcp , "mcp-test-circle-add-member" )
110- added = json .loads (await nc_mcp .call ("add_circle_member" , circle_id = circle ["id" ], user_id = "bob" ))
111- assert added ["userId" ] == "bob"
130+ added = json .loads (await nc_mcp .call ("add_circle_member" , circle_id = circle ["id" ], user_id = circle_peer ))
131+ assert added ["userId" ] == circle_peer
112132 members : list [dict [str , Any ]] = json .loads (await nc_mcp .call ("list_circle_members" , circle_id = circle ["id" ]))
113- assert any (m .get ("userId" ) == "bob" for m in members )
133+ assert any (m .get ("userId" ) == circle_peer for m in members )
114134
115135 @pytest .mark .asyncio
116136 async def test_add_member_rejects_bad_type (self , nc_mcp : McpTestHelper ) -> None :
@@ -119,28 +139,28 @@ async def test_add_member_rejects_bad_type(self, nc_mcp: McpTestHelper) -> None:
119139 await nc_mcp .call (
120140 "add_circle_member" ,
121141 circle_id = circle ["id" ],
122- user_id = "bob " ,
142+ user_id = "anyone " ,
123143 member_type = "bogus" ,
124144 )
125145
126146 @pytest .mark .asyncio
127- async def test_update_member_level (self , nc_mcp : McpTestHelper ) -> None :
147+ async def test_update_member_level (self , nc_mcp : McpTestHelper , circle_peer : str ) -> None :
128148 circle = await _make_circle (nc_mcp , "mcp-test-circle-promote" )
129- added = json .loads (await nc_mcp .call ("add_circle_member" , circle_id = circle ["id" ], user_id = "bob" ))
149+ added = json .loads (await nc_mcp .call ("add_circle_member" , circle_id = circle ["id" ], user_id = circle_peer ))
130150 await nc_mcp .call (
131151 "update_circle_member_level" ,
132152 circle_id = circle ["id" ],
133153 member_id = added ["id" ],
134154 level = "moderator" ,
135155 )
136156 members : list [dict [str , Any ]] = json .loads (await nc_mcp .call ("list_circle_members" , circle_id = circle ["id" ]))
137- bob = next (m for m in members if m .get ("userId" ) == "bob" )
138- assert bob ["level" ] == 4
157+ peer = next (m for m in members if m .get ("userId" ) == circle_peer )
158+ assert peer ["level" ] == 4
139159
140160 @pytest .mark .asyncio
141- async def test_update_level_rejects_bad_value (self , nc_mcp : McpTestHelper ) -> None :
161+ async def test_update_level_rejects_bad_value (self , nc_mcp : McpTestHelper , circle_peer : str ) -> None :
142162 circle = await _make_circle (nc_mcp , "mcp-test-circle-bad-level" )
143- added = json .loads (await nc_mcp .call ("add_circle_member" , circle_id = circle ["id" ], user_id = "bob" ))
163+ added = json .loads (await nc_mcp .call ("add_circle_member" , circle_id = circle ["id" ], user_id = circle_peer ))
144164 with pytest .raises ((ToolError , ValueError ), match = r"[Ii]nvalid level" ):
145165 await nc_mcp .call (
146166 "update_circle_member_level" ,
@@ -150,9 +170,9 @@ async def test_update_level_rejects_bad_value(self, nc_mcp: McpTestHelper) -> No
150170 )
151171
152172 @pytest .mark .asyncio
153- async def test_remove_member (self , nc_mcp : McpTestHelper ) -> None :
173+ async def test_remove_member (self , nc_mcp : McpTestHelper , circle_peer : str ) -> None :
154174 circle = await _make_circle (nc_mcp , "mcp-test-circle-kick" )
155- added = json .loads (await nc_mcp .call ("add_circle_member" , circle_id = circle ["id" ], user_id = "bob" ))
175+ added = json .loads (await nc_mcp .call ("add_circle_member" , circle_id = circle ["id" ], user_id = circle_peer ))
156176 result = json .loads (
157177 await nc_mcp .call (
158178 "remove_circle_member" ,
@@ -162,34 +182,32 @@ async def test_remove_member(self, nc_mcp: McpTestHelper) -> None:
162182 )
163183 assert result == {"removed_member_id" : added ["id" ]}
164184 members : list [dict [str , Any ]] = json .loads (await nc_mcp .call ("list_circle_members" , circle_id = circle ["id" ]))
165- assert not any (m .get ("userId" ) == "bob" for m in members )
185+ assert not any (m .get ("userId" ) == circle_peer for m in members )
166186
167187
168188class TestJoinLeave :
169189 @pytest .mark .asyncio
170- async def test_leave_as_non_owner (self , nc_mcp : McpTestHelper ) -> None :
171- """Alice creates, admin is added, admin leaves — membership disappears."""
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."""
172192 admin = nc_mcp
173193 circle = json .loads (await admin .call ("create_circle" , name = "mcp-test-circle-leave" ))
174- # Make the circle joinable so owner can add bob then bob can leave
175- # Admin adds bob
176- bob_member = json .loads (await admin .call ("add_circle_member" , circle_id = circle ["id" ], user_id = "bob" ))
177- # Admin removes bob (equivalent to bob leaving, same effect, without needing a second client)
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)
178196 await admin .call (
179197 "remove_circle_member" ,
180198 circle_id = circle ["id" ],
181- member_id = bob_member ["id" ],
199+ member_id = peer_member ["id" ],
182200 )
183201 members : list [dict [str , Any ]] = json .loads (await admin .call ("list_circle_members" , circle_id = circle ["id" ]))
184- assert not any (m .get ("userId" ) == "bob" for m in members )
202+ assert not any (m .get ("userId" ) == circle_peer for m in members )
185203
186204
187205class TestSearch :
188206 @pytest .mark .asyncio
189- async def test_search_finds_user (self , nc_mcp : McpTestHelper ) -> None :
190- """Search by a common user id should return at least one hit ."""
207+ async def test_search_returns_list (self , nc_mcp : McpTestHelper ) -> None :
208+ """Search should return a JSON list regardless of matches ."""
191209 await _make_circle (nc_mcp , "mcp-test-circle-search" )
192- results = json .loads (await nc_mcp .call ("search_circles" , term = "bob " ))
210+ results = json .loads (await nc_mcp .call ("search_circles" , term = "admin " ))
193211 assert isinstance (results , list )
194212
195213
0 commit comments