Coverage for app/backend/src/tests/test_communities.py: 100%

721 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-19 15:47 +0000

1from datetime import timedelta 

2 

3import grpc 

4import pytest 

5from geoalchemy2 import WKBElement 

6from google.protobuf import empty_pb2, wrappers_pb2 

7from sqlalchemy import select 

8from sqlalchemy.orm import Session 

9 

10from couchers.db import is_user_in_node_geography, session_scope 

11from couchers.helpers.clusters import CHILD_NODE_TYPE 

12from couchers.materialized_views import refresh_materialized_views 

13from couchers.models import ( 

14 Cluster, 

15 ClusterRole, 

16 ClusterSubscription, 

17 Discussion, 

18 EventOccurrence, 

19 Node, 

20 Page, 

21 PageType, 

22 PageVersion, 

23 SignupFlow, 

24 Thread, 

25 User, 

26) 

27from couchers.proto import api_pb2, auth_pb2, communities_pb2, discussions_pb2, events_pb2, pages_pb2 

28from couchers.tasks import enforce_community_memberships 

29from couchers.utils import create_coordinate, create_polygon_lat_lng, datetime_to_iso8601_local, now, to_multi 

30from tests.fixtures.db import generate_user, get_user_id_and_token 

31from tests.fixtures.misc import Moderator 

32from tests.fixtures.sessions import ( 

33 auth_api_session, 

34 communities_session, 

35 discussions_session, 

36 events_session, 

37 pages_session, 

38) 

39from tests.test_auth import get_session_cookie_tokens 

40 

41# For testing purposes, restrict ourselves to a 1D-world, consisting of "intervals" that have width 2, and coordinates 

42# that are points at (x, 1). 

43# we'll stick to EPSG4326, even though it's not ideal, so don't use too large values, but it's around the equator, so 

44# mostly fine 

45 

46 

47def create_1d_polygon(lb: int, ub: int) -> WKBElement: 

48 # given a lower bound and upper bound on x, creates the given interval 

49 return create_polygon_lat_lng([[lb, 0], [lb, 2], [ub, 2], [ub, 0], [lb, 0]]) 

50 

51 

52def create_1d_point(x: int) -> WKBElement: 

53 return create_coordinate(x, 1) 

54 

55 

56def create_community( 

57 session: Session, 

58 interval_lb: int, 

59 interval_ub: int, 

60 name: str, 

61 admins: list[User], 

62 extra_members: list[User], 

63 parent: Node | None, 

64) -> Node: 

65 node_type = CHILD_NODE_TYPE[parent.node_type if parent else None] 

66 node = Node( 

67 geom=to_multi(create_1d_polygon(interval_lb, interval_ub)), 

68 parent_node_id=parent.id if parent else None, 

69 node_type=node_type, 

70 ) 

71 session.add(node) 

72 session.flush() 

73 cluster = Cluster( 

74 name=f"{name}", 

75 description=f"Description for {name}", 

76 parent_node_id=node.id, 

77 is_official_cluster=True, 

78 ) 

79 session.add(cluster) 

80 session.flush() 

81 thread = Thread() 

82 session.add(thread) 

83 session.flush() 

84 main_page = Page( 

85 parent_node_id=cluster.parent_node_id, 

86 creator_user_id=admins[0].id, 

87 owner_cluster_id=cluster.id, 

88 type=PageType.main_page, 

89 thread_id=thread.id, 

90 ) 

91 session.add(main_page) 

92 session.flush() 

93 page_version = PageVersion( 

94 page_id=main_page.id, 

95 editor_user_id=admins[0].id, 

96 title=f"Main page for the {name} community", 

97 content="There is nothing here yet...", 

98 ) 

99 session.add(page_version) 

100 for admin in admins: 

101 cluster.cluster_subscriptions.append( 

102 ClusterSubscription( 

103 user_id=admin.id, 

104 cluster_id=cluster.id, 

105 role=ClusterRole.admin, 

106 ) 

107 ) 

108 for member in extra_members: 

109 cluster.cluster_subscriptions.append( 

110 ClusterSubscription( 

111 user_id=member.id, 

112 cluster_id=cluster.id, 

113 role=ClusterRole.member, 

114 ) 

115 ) 

116 session.commit() 

117 # other members will be added by enforce_community_memberships() 

118 return node 

119 

120 

121def create_group( 

122 session: Session, name: str, admins: list[User], members: list[User], parent_community: Node | None 

123) -> Cluster: 

124 assert parent_community is not None 

125 cluster = Cluster( 

126 name=f"{name}", 

127 description=f"Description for {name}", 

128 parent_node_id=parent_community.id, 

129 ) 

130 session.add(cluster) 

131 session.flush() 

132 thread = Thread() 

133 session.add(thread) 

134 session.flush() 

135 main_page = Page( 

136 parent_node_id=cluster.parent_node_id, 

137 creator_user_id=admins[0].id, 

138 owner_cluster_id=cluster.id, 

139 type=PageType.main_page, 

140 thread_id=thread.id, 

141 ) 

142 session.add(main_page) 

143 session.flush() 

144 page_version = PageVersion( 

145 page_id=main_page.id, 

146 editor_user_id=admins[0].id, 

147 title=f"Main page for the {name} community", 

148 content="There is nothing here yet...", 

149 ) 

150 session.add(page_version) 

151 for admin in admins: 

152 cluster.cluster_subscriptions.append( 

153 ClusterSubscription( 

154 user_id=admin.id, 

155 cluster_id=cluster.id, 

156 role=ClusterRole.admin, 

157 ) 

158 ) 

159 for member in members: 

160 cluster.cluster_subscriptions.append( 

161 ClusterSubscription( 

162 user_id=member.id, 

163 cluster_id=cluster.id, 

164 role=ClusterRole.member, 

165 ) 

166 ) 

167 session.commit() 

168 return cluster 

169 

170 

171def create_place(token: str, title: str, content: str, address: str, x: float) -> None: 

172 with pages_session(token) as api: 

173 api.CreatePlace( 

174 pages_pb2.CreatePlaceReq( 

175 title=title, 

176 content=content, 

177 address=address, 

178 location=pages_pb2.Coordinate( 

179 lat=x, 

180 lng=1, 

181 ), 

182 ) 

183 ) 

184 

185 

186def create_discussion(token: str, community_id: int | None, group_id: int | None, title: str, content: str) -> None: 

187 # set group_id or community_id to None 

188 with discussions_session(token) as api: 

189 api.CreateDiscussion( 

190 discussions_pb2.CreateDiscussionReq( 

191 title=title, 

192 content=content, 

193 owner_community_id=community_id, 

194 owner_group_id=group_id, 

195 ) 

196 ) 

197 

198 

199def create_event( 

200 token: str, community_id: int | None, group_id: int | None, title: str, content: str, start_td: timedelta 

201) -> None: 

202 with events_session(token) as api: 

203 res = api.CreateEvent( 

204 events_pb2.CreateEventReq( 

205 title=title, 

206 content=content, 

207 location=events_pb2.EventLocation( 

208 address="Near Null Island", 

209 lat=0.1, 

210 lng=0.2, 

211 ), 

212 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + start_td), 

213 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + start_td + timedelta(hours=2)), 

214 ) 

215 ) 

216 api.TransferEvent( 

217 events_pb2.TransferEventReq( 

218 event_id=res.event_id, 

219 new_owner_community_id=community_id, 

220 new_owner_group_id=group_id, 

221 ) 

222 ) 

223 

224 

225def get_community_id(session: Session, community_name: str) -> int: 

226 return session.execute( 

227 select(Cluster.parent_node_id).where(Cluster.is_official_cluster).where(Cluster.name == community_name) 

228 ).scalar_one() 

229 

230 

231def get_group_id(session: Session, group_name: str) -> int: 

232 return session.execute( 

233 select(Cluster.id).where(~Cluster.is_official_cluster).where(Cluster.name == group_name) 

234 ).scalar_one() 

235 

236 

237@pytest.fixture(scope="class") 

238def testing_communities(db_class): 

239 user1, token1 = generate_user(username="user1", geom=create_1d_point(1), geom_radius=0.1) 

240 user2, token2 = generate_user(username="user2", geom=create_1d_point(2), geom_radius=0.1) 

241 user3, token3 = generate_user(username="user3", geom=create_1d_point(3), geom_radius=0.1) 

242 user4, token4 = generate_user(username="user4", geom=create_1d_point(8), geom_radius=0.1) 

243 user5, token5 = generate_user(username="user5", geom=create_1d_point(6), geom_radius=0.1) 

244 user6, token6 = generate_user(username="user6", geom=create_1d_point(65), geom_radius=0.1) 

245 user7, token7 = generate_user(username="user7", geom=create_1d_point(80), geom_radius=0.1) 

246 user8, token8 = generate_user(username="user8", geom=create_1d_point(51), geom_radius=0.1) 

247 

248 with session_scope() as session: 

249 w = create_community(session, 0, 100, "Global", [user1, user3, user7], [], None) 

250 c2 = create_community(session, 52, 100, "Country 2", [user6, user7], [], w) 

251 c2r1 = create_community(session, 52, 71, "Country 2, Region 1", [user6], [user8], c2) 

252 c2r1c1 = create_community(session, 53, 70, "Country 2, Region 1, City 1", [user8], [], c2r1) 

253 c1 = create_community(session, 0, 50, "Country 1", [user1, user2], [], w) 

254 c1r1 = create_community(session, 0, 10, "Country 1, Region 1", [user1, user2], [], c1) 

255 c1r1c1 = create_community(session, 0, 5, "Country 1, Region 1, City 1", [user2], [], c1r1) 

256 c1r1c2 = create_community(session, 7, 10, "Country 1, Region 1, City 2", [user4, user5], [user2], c1r1) 

257 c1r2 = create_community(session, 20, 25, "Country 1, Region 2", [user2], [], c1) 

258 c1r2c1 = create_community(session, 21, 23, "Country 1, Region 2, City 1", [user2], [], c1r2) 

259 

260 h = create_group(session, "Hitchhikers", [user1, user2], [user5, user8], w) 

261 create_group(session, "Country 1, Region 1, Foodies", [user1], [user2, user4], c1r1) 

262 create_group(session, "Country 1, Region 1, Skaters", [user2], [user1], c1r1) 

263 create_group(session, "Country 1, Region 2, Foodies", [user2], [user4, user5], c1r2) 

264 create_group(session, "Country 2, Region 1, Foodies", [user6], [user7], c2r1) 

265 

266 w_id = w.id 

267 c1r1c2_id = c1r1c2.id 

268 h_id = h.id 

269 c1_id = c1.id 

270 

271 create_discussion(token1, w_id, None, "Discussion title 1", "Discussion content 1") 

272 create_discussion(token3, w_id, None, "Discussion title 2", "Discussion content 2") 

273 create_discussion(token3, w_id, None, "Discussion title 3", "Discussion content 3") 

274 create_discussion(token3, w_id, None, "Discussion title 4", "Discussion content 4") 

275 create_discussion(token3, w_id, None, "Discussion title 5", "Discussion content 5") 

276 create_discussion(token3, w_id, None, "Discussion title 6", "Discussion content 6") 

277 create_discussion(token4, c1r1c2_id, None, "Discussion title 7", "Discussion content 7") 

278 create_discussion(token5, None, h_id, "Discussion title 8", "Discussion content 8") 

279 create_discussion(token1, None, h_id, "Discussion title 9", "Discussion content 9") 

280 create_discussion(token2, None, h_id, "Discussion title 10", "Discussion content 10") 

281 create_discussion(token3, None, h_id, "Discussion title 11", "Discussion content 11") 

282 create_discussion(token4, None, h_id, "Discussion title 12", "Discussion content 12") 

283 create_discussion(token5, None, h_id, "Discussion title 13", "Discussion content 13") 

284 create_discussion(token8, None, h_id, "Discussion title 14", "Discussion content 14") 

285 

286 create_event(token3, c1_id, None, "Event title 1", "Event content 1", timedelta(hours=1)) 

287 create_event(token1, c1_id, None, "Event title 2", "Event content 2", timedelta(hours=2)) 

288 create_event(token3, c1_id, None, "Event title 3", "Event content 3", timedelta(hours=3)) 

289 create_event(token1, c1_id, None, "Event title 4", "Event content 4", timedelta(hours=4)) 

290 create_event(token3, c1_id, None, "Event title 5", "Event content 5", timedelta(hours=5)) 

291 create_event(token1, c1_id, None, "Event title 6", "Event content 6", timedelta(hours=6)) 

292 create_event(token2, None, h_id, "Event title 7", "Event content 7", timedelta(hours=7)) 

293 create_event(token2, None, h_id, "Event title 8", "Event content 8", timedelta(hours=8)) 

294 create_event(token2, None, h_id, "Event title 9", "Event content 9", timedelta(hours=9)) 

295 create_event(token2, None, h_id, "Event title 10", "Event content 10", timedelta(hours=10)) 

296 create_event(token2, None, h_id, "Event title 11", "Event content 11", timedelta(hours=11)) 

297 create_event(token2, None, h_id, "Event title 12", "Event content 12", timedelta(hours=12)) 

298 

299 # Approve all events for visibility (UMS starts events as SHADOWED) 

300 mod_user, mod_token = generate_user(is_superuser=True) 

301 mod = Moderator(mod_user, mod_token) 

302 with session_scope() as session: 

303 occurrence_ids = session.execute(select(EventOccurrence.id)).scalars().all() 

304 discussion_ids = session.execute(select(Discussion.id)).scalars().all() 

305 for oid in occurrence_ids: 

306 mod.approve_event_occurrence(oid) 

307 for did in discussion_ids: 

308 mod.approve_discussion(did) 

309 

310 enforce_community_memberships() 

311 

312 create_place(token1, "Country 1, Region 1, Attraction", "Place content", "Somewhere in c1r1", 6) 

313 create_place(token2, "Country 1, Region 1, City 1, Attraction 1", "Place content", "Somewhere in c1r1c1", 3) 

314 create_place(token2, "Country 1, Region 1, City 1, Attraction 2", "Place content", "Somewhere in c1r1c1", 4) 

315 create_place(token8, "Global, Attraction", "Place content", "Somewhere in w", 51.5) 

316 create_place(token6, "Country 2, Region 1, Attraction", "Place content", "Somewhere in c2r1", 59) 

317 

318 refresh_materialized_views(empty_pb2.Empty()) 

319 

320 yield 

321 

322 

323class TestCommunities: 

324 @staticmethod 

325 def test_GetCommunity(testing_communities): 

326 with session_scope() as session: 

327 user1_id, token1 = get_user_id_and_token(session, "user1") 

328 user2_id, token2 = get_user_id_and_token(session, "user2") 

329 user6_id, token6 = get_user_id_and_token(session, "user6") 

330 w_id = get_community_id(session, "Global") 

331 c1_id = get_community_id(session, "Country 1") 

332 c1r1_id = get_community_id(session, "Country 1, Region 1") 

333 c1r1c1_id = get_community_id(session, "Country 1, Region 1, City 1") 

334 c2_id = get_community_id(session, "Country 2") 

335 

336 with communities_session(token2) as api: 

337 res = api.GetCommunity( 

338 communities_pb2.GetCommunityReq( 

339 community_id=w_id, 

340 ) 

341 ) 

342 assert res.name == "Global" 

343 assert res.slug == "global" 

344 assert res.description == "Description for Global" 

345 assert len(res.parents) == 1 

346 assert res.parents[0].HasField("community") 

347 assert res.parents[0].community.community_id == w_id 

348 assert res.parents[0].community.name == "Global" 

349 assert res.parents[0].community.slug == "global" 

350 assert res.parents[0].community.description == "Description for Global" 

351 assert res.main_page.type == pages_pb2.PAGE_TYPE_MAIN_PAGE 

352 assert res.main_page.slug == "main-page-for-the-global-community" 

353 assert res.main_page.last_editor_user_id == user1_id 

354 assert res.main_page.creator_user_id == user1_id 

355 assert res.main_page.owner_community_id == w_id 

356 assert res.main_page.title == "Main page for the Global community" 

357 assert res.main_page.content == "There is nothing here yet..." 

358 assert not res.main_page.can_edit 

359 assert not res.main_page.can_moderate 

360 assert res.main_page.editor_user_ids == [user1_id] 

361 assert res.member 

362 assert not res.admin 

363 assert res.member_count == 8 

364 assert res.admin_count == 3 

365 

366 res = api.GetCommunity( 

367 communities_pb2.GetCommunityReq( 

368 community_id=c1r1c1_id, 

369 ) 

370 ) 

371 assert res.community_id == c1r1c1_id 

372 assert res.name == "Country 1, Region 1, City 1" 

373 assert res.slug == "country-1-region-1-city-1" 

374 assert res.description == "Description for Country 1, Region 1, City 1" 

375 assert len(res.parents) == 4 

376 assert res.parents[0].HasField("community") 

377 assert res.parents[0].community.community_id == w_id 

378 assert res.parents[0].community.name == "Global" 

379 assert res.parents[0].community.slug == "global" 

380 assert res.parents[0].community.description == "Description for Global" 

381 assert res.parents[1].HasField("community") 

382 assert res.parents[1].community.community_id == c1_id 

383 assert res.parents[1].community.name == "Country 1" 

384 assert res.parents[1].community.slug == "country-1" 

385 assert res.parents[1].community.description == "Description for Country 1" 

386 assert res.parents[2].HasField("community") 

387 assert res.parents[2].community.community_id == c1r1_id 

388 assert res.parents[2].community.name == "Country 1, Region 1" 

389 assert res.parents[2].community.slug == "country-1-region-1" 

390 assert res.parents[2].community.description == "Description for Country 1, Region 1" 

391 assert res.parents[3].HasField("community") 

392 assert res.parents[3].community.community_id == c1r1c1_id 

393 assert res.parents[3].community.name == "Country 1, Region 1, City 1" 

394 assert res.parents[3].community.slug == "country-1-region-1-city-1" 

395 assert res.parents[3].community.description == "Description for Country 1, Region 1, City 1" 

396 assert res.main_page.type == pages_pb2.PAGE_TYPE_MAIN_PAGE 

397 assert res.main_page.slug == "main-page-for-the-country-1-region-1-city-1-community" 

398 assert res.main_page.last_editor_user_id == user2_id 

399 assert res.main_page.creator_user_id == user2_id 

400 assert res.main_page.owner_community_id == c1r1c1_id 

401 assert res.main_page.title == "Main page for the Country 1, Region 1, City 1 community" 

402 assert res.main_page.content == "There is nothing here yet..." 

403 assert res.main_page.can_edit 

404 assert res.main_page.can_moderate 

405 assert res.main_page.editor_user_ids == [user2_id] 

406 assert res.member 

407 assert res.admin 

408 assert res.member_count == 3 

409 assert res.admin_count == 1 

410 

411 res = api.GetCommunity( 

412 communities_pb2.GetCommunityReq( 

413 community_id=c2_id, 

414 ) 

415 ) 

416 assert res.community_id == c2_id 

417 assert res.name == "Country 2" 

418 assert res.slug == "country-2" 

419 assert res.description == "Description for Country 2" 

420 assert len(res.parents) == 2 

421 assert res.parents[0].HasField("community") 

422 assert res.parents[0].community.community_id == w_id 

423 assert res.parents[0].community.name == "Global" 

424 assert res.parents[0].community.slug == "global" 

425 assert res.parents[0].community.description == "Description for Global" 

426 assert res.parents[1].HasField("community") 

427 assert res.parents[1].community.community_id == c2_id 

428 assert res.parents[1].community.name == "Country 2" 

429 assert res.parents[1].community.slug == "country-2" 

430 assert res.parents[1].community.description == "Description for Country 2" 

431 assert res.main_page.type == pages_pb2.PAGE_TYPE_MAIN_PAGE 

432 assert res.main_page.slug == "main-page-for-the-country-2-community" 

433 assert res.main_page.last_editor_user_id == user6_id 

434 assert res.main_page.creator_user_id == user6_id 

435 assert res.main_page.owner_community_id == c2_id 

436 assert res.main_page.title == "Main page for the Country 2 community" 

437 assert res.main_page.content == "There is nothing here yet..." 

438 assert not res.main_page.can_edit 

439 assert not res.main_page.can_moderate 

440 assert res.main_page.editor_user_ids == [user6_id] 

441 assert not res.member 

442 assert not res.admin 

443 assert res.member_count == 2 

444 assert res.admin_count == 2 

445 

446 @staticmethod 

447 def test_ListCommunities(testing_communities): 

448 with session_scope() as session: 

449 user1_id, token1 = get_user_id_and_token(session, "user1") 

450 c1_id = get_community_id(session, "Country 1") 

451 c1r1_id = get_community_id(session, "Country 1, Region 1") 

452 c1r2_id = get_community_id(session, "Country 1, Region 2") 

453 

454 with communities_session(token1) as api: 

455 res = api.ListCommunities( 

456 communities_pb2.ListCommunitiesReq( 

457 community_id=c1_id, 

458 ) 

459 ) 

460 assert [c.community_id for c in res.communities] == [c1r1_id, c1r2_id] 

461 

462 @staticmethod 

463 def test_ListCommunities_all(testing_communities): 

464 with session_scope() as session: 

465 user1_id, token1 = get_user_id_and_token(session, "user1") 

466 w_id = get_community_id(session, "Global") 

467 c1_id = get_community_id(session, "Country 1") 

468 c1r1_id = get_community_id(session, "Country 1, Region 1") 

469 c1r1c1_id = get_community_id(session, "Country 1, Region 1, City 1") 

470 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

471 c1r2_id = get_community_id(session, "Country 1, Region 2") 

472 c1r2c1_id = get_community_id(session, "Country 1, Region 2, City 1") 

473 c2_id = get_community_id(session, "Country 2") 

474 c2r1_id = get_community_id(session, "Country 2, Region 1") 

475 c2r1c1_id = get_community_id(session, "Country 2, Region 1, City 1") 

476 

477 # Fetch all communities ordered by name 

478 with communities_session(token1) as api: 

479 res = api.ListCommunities( 

480 communities_pb2.ListCommunitiesReq( 

481 page_size=5, 

482 ) 

483 ) 

484 assert [c.community_id for c in res.communities] == [c1_id, c1r1_id, c1r1c1_id, c1r1c2_id, c1r2_id] 

485 res = api.ListCommunities( 

486 communities_pb2.ListCommunitiesReq( 

487 page_size=2, 

488 page_token=res.next_page_token, 

489 ) 

490 ) 

491 assert [c.community_id for c in res.communities] == [c1r2c1_id, c2_id] 

492 res = api.ListCommunities( 

493 communities_pb2.ListCommunitiesReq( 

494 page_size=5, 

495 page_token=res.next_page_token, 

496 ) 

497 ) 

498 assert [c.community_id for c in res.communities] == [c2r1_id, c2r1c1_id, w_id] 

499 

500 @staticmethod 

501 def test_ListUserCommunities(testing_communities): 

502 with session_scope() as session: 

503 user2_id, token2 = get_user_id_and_token(session, "user2") 

504 w_id = get_community_id(session, "Global") 

505 c1_id = get_community_id(session, "Country 1") 

506 c1r1_id = get_community_id(session, "Country 1, Region 1") 

507 c1r1c1_id = get_community_id(session, "Country 1, Region 1, City 1") 

508 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

509 c1r2_id = get_community_id(session, "Country 1, Region 2") 

510 c1r2c1_id = get_community_id(session, "Country 1, Region 2, City 1") 

511 

512 # Fetch user2's communities from user2's account 

513 with communities_session(token2) as api: 

514 res = api.ListUserCommunities(communities_pb2.ListUserCommunitiesReq()) 

515 assert [c.community_id for c in res.communities] == [ 

516 c1r1c1_id, 

517 c1r1c2_id, 

518 c1r2c1_id, 

519 c1r1_id, 

520 c1r2_id, 

521 c1_id, 

522 w_id, 

523 ] 

524 

525 # paginated, with pages crossing node type boundaries 

526 res = api.ListUserCommunities(communities_pb2.ListUserCommunitiesReq(page_size=2)) 

527 assert [c.community_id for c in res.communities] == [c1r1c1_id, c1r1c2_id] 

528 res = api.ListUserCommunities( 

529 communities_pb2.ListUserCommunitiesReq(page_size=2, page_token=res.next_page_token) 

530 ) 

531 assert [c.community_id for c in res.communities] == [c1r2c1_id, c1r1_id] 

532 res = api.ListUserCommunities( 

533 communities_pb2.ListUserCommunitiesReq(page_size=2, page_token=res.next_page_token) 

534 ) 

535 assert [c.community_id for c in res.communities] == [c1r2_id, c1_id] 

536 res = api.ListUserCommunities( 

537 communities_pb2.ListUserCommunitiesReq(page_size=2, page_token=res.next_page_token) 

538 ) 

539 assert [c.community_id for c in res.communities] == [w_id] 

540 assert not res.next_page_token 

541 

542 @staticmethod 

543 def test_ListOtherUserCommunities(testing_communities): 

544 with session_scope() as session: 

545 user1_id, token1 = get_user_id_and_token(session, "user1") 

546 user2_id, token2 = get_user_id_and_token(session, "user2") 

547 w_id = get_community_id(session, "Global") 

548 c1_id = get_community_id(session, "Country 1") 

549 c1r1_id = get_community_id(session, "Country 1, Region 1") 

550 c1r1c1_id = get_community_id(session, "Country 1, Region 1, City 1") 

551 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

552 c1r2_id = get_community_id(session, "Country 1, Region 2") 

553 c1r2c1_id = get_community_id(session, "Country 1, Region 2, City 1") 

554 

555 # Fetch user2's communities from user1's account 

556 with communities_session(token1) as api: 

557 res = api.ListUserCommunities(communities_pb2.ListUserCommunitiesReq(user_id=user2_id)) 

558 assert [c.community_id for c in res.communities] == [ 

559 c1r1c1_id, 

560 c1r1c2_id, 

561 c1r2c1_id, 

562 c1r1_id, 

563 c1r2_id, 

564 c1_id, 

565 w_id, 

566 ] 

567 

568 @staticmethod 

569 def test_ListGroups(testing_communities): 

570 with session_scope() as session: 

571 user1_id, token1 = get_user_id_and_token(session, "user1") 

572 user5_id, token5 = get_user_id_and_token(session, "user5") 

573 w_id = get_community_id(session, "Global") 

574 hitchhikers_id = get_group_id(session, "Hitchhikers") 

575 c1r1_id = get_community_id(session, "Country 1, Region 1") 

576 foodies_id = get_group_id(session, "Country 1, Region 1, Foodies") 

577 skaters_id = get_group_id(session, "Country 1, Region 1, Skaters") 

578 

579 with communities_session(token1) as api: 

580 res = api.ListGroups( 

581 communities_pb2.ListGroupsReq( 

582 community_id=c1r1_id, 

583 ) 

584 ) 

585 assert [g.group_id for g in res.groups] == [foodies_id, skaters_id] 

586 

587 with communities_session(token5) as api: 

588 res = api.ListGroups( 

589 communities_pb2.ListGroupsReq( 

590 community_id=w_id, 

591 ) 

592 ) 

593 assert len(res.groups) == 1 

594 assert res.groups[0].group_id == hitchhikers_id 

595 

596 @staticmethod 

597 def test_ListAdmins(testing_communities): 

598 with session_scope() as session: 

599 user1_id, token1 = get_user_id_and_token(session, "user1") 

600 user3_id, token3 = get_user_id_and_token(session, "user3") 

601 user4_id, token4 = get_user_id_and_token(session, "user4") 

602 user5_id, token5 = get_user_id_and_token(session, "user5") 

603 user7_id, token7 = get_user_id_and_token(session, "user7") 

604 w_id = get_community_id(session, "Global") 

605 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

606 

607 with communities_session(token1) as api: 

608 res = api.ListAdmins( 

609 communities_pb2.ListAdminsReq( 

610 community_id=w_id, 

611 ) 

612 ) 

613 assert res.admin_user_ids == [user1_id, user3_id, user7_id] 

614 

615 res = api.ListAdmins( 

616 communities_pb2.ListAdminsReq( 

617 community_id=c1r1c2_id, 

618 ) 

619 ) 

620 assert res.admin_user_ids == [user4_id, user5_id] 

621 

622 @staticmethod 

623 def test_AddAdmin(testing_communities): 

624 with session_scope() as session: 

625 user4_id, token4 = get_user_id_and_token(session, "user4") 

626 user5_id, _ = get_user_id_and_token(session, "user5") 

627 user2_id, _ = get_user_id_and_token(session, "user2") 

628 user8_id, token8 = get_user_id_and_token(session, "user8") 

629 node_id = get_community_id(session, "Country 1, Region 1, City 2") 

630 

631 with communities_session(token8) as api: 

632 with pytest.raises(grpc.RpcError) as err: 

633 api.AddAdmin(communities_pb2.AddAdminReq(community_id=node_id, user_id=user2_id)) 

634 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

635 assert err.value.details() == "You're not allowed to moderate that community" 

636 

637 with communities_session(token4) as api: 

638 res = api.ListAdmins(communities_pb2.ListAdminsReq(community_id=node_id)) 

639 assert res.admin_user_ids == [user4_id, user5_id] 

640 

641 with pytest.raises(grpc.RpcError) as err: 

642 api.AddAdmin(communities_pb2.AddAdminReq(community_id=node_id, user_id=user8_id)) 

643 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

644 assert err.value.details() == "That user is not in the community." 

645 

646 with pytest.raises(grpc.RpcError) as err: 

647 api.AddAdmin(communities_pb2.AddAdminReq(community_id=node_id, user_id=user5_id)) 

648 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

649 assert err.value.details() == "That user is already an admin." 

650 

651 api.AddAdmin(communities_pb2.AddAdminReq(community_id=node_id, user_id=user2_id)) 

652 res = api.ListAdmins(communities_pb2.ListAdminsReq(community_id=node_id)) 

653 assert res.admin_user_ids == [user2_id, user4_id, user5_id] 

654 # Cleanup because database changes do not roll back 

655 api.RemoveAdmin(communities_pb2.RemoveAdminReq(community_id=node_id, user_id=user2_id)) 

656 

657 @staticmethod 

658 def test_RemoveAdmin(testing_communities): 

659 with session_scope() as session: 

660 user4_id, token4 = get_user_id_and_token(session, "user4") 

661 user5_id, _ = get_user_id_and_token(session, "user5") 

662 user2_id, _ = get_user_id_and_token(session, "user2") 

663 user8_id, token8 = get_user_id_and_token(session, "user8") 

664 node_id = get_community_id(session, "Country 1, Region 1, City 2") 

665 

666 with communities_session(token8) as api: 

667 with pytest.raises(grpc.RpcError) as err: 

668 api.AddAdmin(communities_pb2.AddAdminReq(community_id=node_id, user_id=user2_id)) 

669 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

670 assert err.value.details() == "You're not allowed to moderate that community" 

671 

672 with communities_session(token4) as api: 

673 res = api.ListAdmins(communities_pb2.ListAdminsReq(community_id=node_id)) 

674 assert res.admin_user_ids == [user4_id, user5_id] 

675 

676 with pytest.raises(grpc.RpcError) as err: 

677 api.RemoveAdmin(communities_pb2.RemoveAdminReq(community_id=node_id, user_id=user8_id)) 

678 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

679 assert err.value.details() == "That user is not in the community." 

680 

681 with pytest.raises(grpc.RpcError) as err: 

682 api.RemoveAdmin(communities_pb2.RemoveAdminReq(community_id=node_id, user_id=user2_id)) 

683 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

684 assert err.value.details() == "That user is not an admin." 

685 

686 api.RemoveAdmin(communities_pb2.RemoveAdminReq(community_id=node_id, user_id=user5_id)) 

687 res = api.ListAdmins(communities_pb2.ListAdminsReq(community_id=node_id)) 

688 assert res.admin_user_ids == [user4_id] 

689 # Cleanup because database changes do not roll back 

690 api.AddAdmin(communities_pb2.AddAdminReq(community_id=node_id, user_id=user5_id)) 

691 

692 @staticmethod 

693 def test_ListMembers(testing_communities): 

694 with session_scope() as session: 

695 user1_id, token1 = get_user_id_and_token(session, "user1") 

696 user2_id, token2 = get_user_id_and_token(session, "user2") 

697 user3_id, token3 = get_user_id_and_token(session, "user3") 

698 user4_id, token4 = get_user_id_and_token(session, "user4") 

699 user5_id, token5 = get_user_id_and_token(session, "user5") 

700 user6_id, token6 = get_user_id_and_token(session, "user6") 

701 user7_id, token7 = get_user_id_and_token(session, "user7") 

702 user8_id, token8 = get_user_id_and_token(session, "user8") 

703 w_id = get_community_id(session, "Global") 

704 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

705 

706 with communities_session(token1) as api: 

707 res = api.ListMembers( 

708 communities_pb2.ListMembersReq( 

709 community_id=w_id, 

710 ) 

711 ) 

712 assert res.member_user_ids == [ 

713 user8_id, 

714 user7_id, 

715 user6_id, 

716 user5_id, 

717 user4_id, 

718 user3_id, 

719 user2_id, 

720 user1_id, 

721 ] 

722 

723 res = api.ListMembers( 

724 communities_pb2.ListMembersReq( 

725 community_id=c1r1c2_id, 

726 ) 

727 ) 

728 assert res.member_user_ids == [user5_id, user4_id, user2_id] 

729 

730 @staticmethod 

731 def test_ListNearbyUsers(testing_communities): 

732 with session_scope() as session: 

733 user1_id, token1 = get_user_id_and_token(session, "user1") 

734 user2_id, token2 = get_user_id_and_token(session, "user2") 

735 user3_id, token3 = get_user_id_and_token(session, "user3") 

736 user4_id, token4 = get_user_id_and_token(session, "user4") 

737 user5_id, token5 = get_user_id_and_token(session, "user5") 

738 user6_id, token6 = get_user_id_and_token(session, "user6") 

739 user7_id, token7 = get_user_id_and_token(session, "user7") 

740 user8_id, token8 = get_user_id_and_token(session, "user8") 

741 w_id = get_community_id(session, "Global") 

742 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

743 

744 with communities_session(token1) as api: 

745 res = api.ListNearbyUsers( 

746 communities_pb2.ListNearbyUsersReq( 

747 community_id=w_id, 

748 ) 

749 ) 

750 assert res.nearby_user_ids == [ 

751 user1_id, 

752 user2_id, 

753 user3_id, 

754 user4_id, 

755 user5_id, 

756 user6_id, 

757 user7_id, 

758 user8_id, 

759 ] 

760 

761 res = api.ListNearbyUsers( 

762 communities_pb2.ListNearbyUsersReq( 

763 community_id=c1r1c2_id, 

764 ) 

765 ) 

766 assert res.nearby_user_ids == [user4_id] 

767 

768 @staticmethod 

769 def test_ListDiscussions(testing_communities): 

770 with session_scope() as session: 

771 user1_id, token1 = get_user_id_and_token(session, "user1") 

772 w_id = get_community_id(session, "Global") 

773 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

774 

775 with communities_session(token1) as api: 

776 res = api.ListDiscussions( 

777 communities_pb2.ListDiscussionsReq( 

778 community_id=w_id, 

779 page_size=3, 

780 ) 

781 ) 

782 assert [d.title for d in res.discussions] == [ 

783 "Discussion title 6", 

784 "Discussion title 5", 

785 "Discussion title 4", 

786 ] 

787 for d in res.discussions: 

788 assert d.thread.thread_id > 0 

789 assert d.thread.num_responses == 0 

790 

791 res = api.ListDiscussions( 

792 communities_pb2.ListDiscussionsReq( 

793 community_id=w_id, 

794 page_token=res.next_page_token, 

795 page_size=2, 

796 ) 

797 ) 

798 assert [d.title for d in res.discussions] == [ 

799 "Discussion title 3", 

800 "Discussion title 2", 

801 ] 

802 for d in res.discussions: 

803 assert d.thread.thread_id > 0 

804 assert d.thread.num_responses == 0 

805 

806 res = api.ListDiscussions( 

807 communities_pb2.ListDiscussionsReq( 

808 community_id=w_id, 

809 page_token=res.next_page_token, 

810 page_size=2, 

811 ) 

812 ) 

813 assert [d.title for d in res.discussions] == [ 

814 "Discussion title 1", 

815 ] 

816 for d in res.discussions: 

817 assert d.thread.thread_id > 0 

818 assert d.thread.num_responses == 0 

819 

820 res = api.ListDiscussions( 

821 communities_pb2.ListDiscussionsReq( 

822 community_id=c1r1c2_id, 

823 ) 

824 ) 

825 assert [d.title for d in res.discussions] == [ 

826 "Discussion title 7", 

827 ] 

828 for d in res.discussions: 

829 assert d.thread.thread_id > 0 

830 assert d.thread.num_responses == 0 

831 

832 @staticmethod 

833 def test_is_user_in_node_geography(testing_communities): 

834 with session_scope() as session: 

835 c1_id = get_community_id(session, "Country 1") 

836 

837 user1_id, _ = get_user_id_and_token(session, "user1") 

838 user2_id, _ = get_user_id_and_token(session, "user2") 

839 user3_id, _ = get_user_id_and_token(session, "user3") 

840 user4_id, _ = get_user_id_and_token(session, "user4") 

841 user5_id, _ = get_user_id_and_token(session, "user5") 

842 

843 # All these users should be in Country 1's geography 

844 assert is_user_in_node_geography(session, user1_id, c1_id) 

845 assert is_user_in_node_geography(session, user2_id, c1_id) 

846 assert is_user_in_node_geography(session, user3_id, c1_id) 

847 assert is_user_in_node_geography(session, user4_id, c1_id) 

848 assert is_user_in_node_geography(session, user5_id, c1_id) 

849 

850 @staticmethod 

851 def test_ListEvents(testing_communities): 

852 with session_scope() as session: 

853 user1_id, token1 = get_user_id_and_token(session, "user1") 

854 c1_id = get_community_id(session, "Country 1") 

855 

856 with communities_session(token1) as api: 

857 res = api.ListEvents( 

858 communities_pb2.ListEventsReq( 

859 community_id=c1_id, 

860 page_size=3, 

861 ) 

862 ) 

863 assert [d.title for d in res.events] == [ 

864 "Event title 1", 

865 "Event title 2", 

866 "Event title 3", 

867 ] 

868 

869 res = api.ListEvents( 

870 communities_pb2.ListEventsReq( 

871 community_id=c1_id, 

872 page_token=res.next_page_token, 

873 page_size=2, 

874 ) 

875 ) 

876 assert [d.title for d in res.events] == [ 

877 "Event title 4", 

878 "Event title 5", 

879 ] 

880 

881 res = api.ListEvents( 

882 communities_pb2.ListEventsReq( 

883 community_id=c1_id, 

884 page_token=res.next_page_token, 

885 page_size=2, 

886 ) 

887 ) 

888 assert [d.title for d in res.events] == [ 

889 "Event title 6", 

890 ] 

891 assert not res.next_page_token 

892 

893 @staticmethod 

894 def test_empty_query_aborts(testing_communities): 

895 with session_scope() as session: 

896 _, token = get_user_id_and_token(session, "user1") 

897 

898 with communities_session(token) as api: 

899 with pytest.raises(grpc.RpcError) as err: 

900 api.SearchCommunities(communities_pb2.SearchCommunitiesReq(query=" ")) 

901 assert err.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

902 assert err.value.details() == "Query must be at least 3 characters long." 

903 

904 @staticmethod 

905 def test_min_length_lt_3_aborts(testing_communities): 

906 """ 

907 len(query) < 3 → return INVALID_ARGUMENT: query_too_short 

908 """ 

909 with session_scope() as session: 

910 _, token = get_user_id_and_token(session, "user1") 

911 

912 with communities_session(token) as api: 

913 with pytest.raises(grpc.RpcError) as err: 

914 api.SearchCommunities(communities_pb2.SearchCommunitiesReq(query="zz", page_size=5)) 

915 assert err.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

916 assert err.value.details() == "Query must be at least 3 characters long." 

917 

918 @staticmethod 

919 def test_typo_matches_existing_name(testing_communities): 

920 """ 

921 Word_similarity should match a simple typo in community name. 

922 """ 

923 with session_scope() as session: 

924 _, token = get_user_id_and_token(session, "user1") 

925 c1_id = get_community_id(session, "Country 1") 

926 

927 with communities_session(token) as api: 

928 res = api.SearchCommunities(communities_pb2.SearchCommunitiesReq(query="Coutri 1", page_size=5)) 

929 ids = [c.community_id for c in res.communities] 

930 assert c1_id in ids 

931 

932 @staticmethod 

933 def test_word_similarity_matches_partial_word(testing_communities): 

934 """ 

935 Query 'city' should match 'Country 1, Region 1, City 1'. 

936 """ 

937 with session_scope() as session: 

938 _, token = get_user_id_and_token(session, "user1") 

939 city1_id = get_community_id(session, "Country 1, Region 1, City 1") # переименовал для ясности 

940 

941 with communities_session(token) as api: 

942 res = api.SearchCommunities(communities_pb2.SearchCommunitiesReq(query="city", page_size=5)) 

943 ids = [c.community_id for c in res.communities] 

944 assert city1_id in ids 

945 

946 @staticmethod 

947 def test_results_sorted_by_similarity(testing_communities): 

948 """ 

949 Results should be ordered by similarity score (best match first). 

950 For query 'Country 1, Region', the full region name should rank higher 

951 than deeper descendants like 'City 1'. 

952 """ 

953 with session_scope() as session: 

954 _, token = get_user_id_and_token(session, "user1") 

955 region_id = get_community_id(session, "Country 1, Region 1") 

956 city_id = get_community_id(session, "Country 1, Region 1, City 1") 

957 

958 with communities_session(token) as api: 

959 res = api.SearchCommunities(communities_pb2.SearchCommunitiesReq(query="Country 1, Region", page_size=5)) 

960 ids = [c.community_id for c in res.communities] 

961 

962 assert region_id in ids 

963 assert city_id in ids 

964 assert ids.index(region_id) < ids.index(city_id) 

965 

966 @staticmethod 

967 def test_no_results_returns_empty(testing_communities): 

968 """ 

969 For a nonsense query that shouldn't meet the similarity threshold, return empty list. 

970 """ 

971 with session_scope() as session: 

972 _, token = get_user_id_and_token(session, "user1") 

973 

974 with communities_session(token) as api: 

975 res = api.SearchCommunities(communities_pb2.SearchCommunitiesReq(query="qwertyuiopasdf", page_size=5)) 

976 assert res.communities == [] 

977 

978 @staticmethod 

979 def test_ListAllCommunities(testing_communities): 

980 """ 

981 Test that ListAllCommunities returns all communities with proper hierarchy information. 

982 """ 

983 with session_scope() as session: 

984 user1_id, token1 = get_user_id_and_token(session, "user1") 

985 user2_id, token2 = get_user_id_and_token(session, "user2") 

986 user6_id, token6 = get_user_id_and_token(session, "user6") 

987 w_id = get_community_id(session, "Global") 

988 c1_id = get_community_id(session, "Country 1") 

989 c1r1_id = get_community_id(session, "Country 1, Region 1") 

990 c1r1c1_id = get_community_id(session, "Country 1, Region 1, City 1") 

991 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

992 c1r2_id = get_community_id(session, "Country 1, Region 2") 

993 c1r2c1_id = get_community_id(session, "Country 1, Region 2, City 1") 

994 c2_id = get_community_id(session, "Country 2") 

995 c2r1_id = get_community_id(session, "Country 2, Region 1") 

996 c2r1c1_id = get_community_id(session, "Country 2, Region 1, City 1") 

997 

998 # Test with user1 who is a member of multiple communities 

999 with communities_session(token1) as api: 

1000 res = api.ListAllCommunities(communities_pb2.ListAllCommunitiesReq()) 

1001 

1002 # Should return all 10 communities 

1003 assert len(res.communities) == 10 

1004 

1005 # Get all community IDs 

1006 community_ids = [c.community_id for c in res.communities] 

1007 assert set(community_ids) == { 

1008 w_id, 

1009 c1_id, 

1010 c1r1_id, 

1011 c1r1c1_id, 

1012 c1r1c2_id, 

1013 c1r2_id, 

1014 c1r2c1_id, 

1015 c2_id, 

1016 c2r1_id, 

1017 c2r1c1_id, 

1018 } 

1019 

1020 # Check that each community has the required fields 

1021 for community in res.communities: 

1022 assert community.community_id > 0 

1023 assert len(community.name) > 0 

1024 assert len(community.slug) > 0 

1025 assert community.member_count > 0 

1026 # member field should be a boolean 

1027 assert isinstance(community.member, bool) 

1028 # parents should be present for hierarchical ordering 

1029 assert len(community.parents) >= 1 

1030 # created timestamp should be present 

1031 assert community.HasField("created") 

1032 assert community.created.seconds > 0 

1033 

1034 # Find specific communities and verify their data 

1035 global_community = next(c for c in res.communities if c.community_id == w_id) 

1036 assert global_community.name == "Global" 

1037 assert global_community.slug == "global" 

1038 assert global_community.member # user1 is a member 

1039 assert global_community.member_count == 8 

1040 assert len(global_community.parents) == 1 # Only itself 

1041 

1042 c1r1c1_community = next(c for c in res.communities if c.community_id == c1r1c1_id) 

1043 assert c1r1c1_community.name == "Country 1, Region 1, City 1" 

1044 assert c1r1c1_community.slug == "country-1-region-1-city-1" 

1045 assert c1r1c1_community.member # user1 is a member 

1046 assert c1r1c1_community.member_count == 3 

1047 assert len(c1r1c1_community.parents) == 4 # Global, Country 1, Region 1, City 1 

1048 # Verify parent hierarchy 

1049 assert c1r1c1_community.parents[0].community.community_id == w_id 

1050 assert c1r1c1_community.parents[1].community.community_id == c1_id 

1051 assert c1r1c1_community.parents[2].community.community_id == c1r1_id 

1052 assert c1r1c1_community.parents[3].community.community_id == c1r1c1_id 

1053 

1054 # Test with user6 who has different community memberships 

1055 with communities_session(token6) as api: 

1056 res = api.ListAllCommunities(communities_pb2.ListAllCommunitiesReq()) 

1057 

1058 # Should still return all 10 communities 

1059 assert len(res.communities) == 10 

1060 

1061 # Find Country 2 community - user6 should be a member 

1062 c2_community = next(c for c in res.communities if c.community_id == c2_id) 

1063 assert c2_community.member # user6 is a member 

1064 assert c2_community.member_count == 2 

1065 

1066 # Find Country 1 - user6 should NOT be a member 

1067 c1_community = next(c for c in res.communities if c.community_id == c1_id) 

1068 assert not c1_community.member # user6 is not a member 

1069 

1070 # Global - user6 should be a member 

1071 global_community = next(c for c in res.communities if c.community_id == w_id) 

1072 assert global_community.member # user6 is a member 

1073 

1074 @staticmethod 

1075 def test_ListRecentCommunities(testing_communities, monkeypatch): 

1076 """ 

1077 ListRecentCommunities returns newest-first communities across the whole tree, 

1078 honouring page_size. 

1079 """ 

1080 with session_scope() as session: 

1081 _, token = get_user_id_and_token(session, "user1") 

1082 # communities are created in this order in the fixture, so creation 

1083 # time ascends down the list 

1084 w_id = get_community_id(session, "Global") 

1085 c2_id = get_community_id(session, "Country 2") 

1086 c2r1_id = get_community_id(session, "Country 2, Region 1") 

1087 c2r1c1_id = get_community_id(session, "Country 2, Region 1, City 1") 

1088 c1_id = get_community_id(session, "Country 1") 

1089 c1r1_id = get_community_id(session, "Country 1, Region 1") 

1090 c1r1c1_id = get_community_id(session, "Country 1, Region 1, City 1") 

1091 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

1092 c1r2_id = get_community_id(session, "Country 1, Region 2") 

1093 c1r2c1_id = get_community_id(session, "Country 1, Region 2, City 1") 

1094 

1095 newest_first = [ 

1096 c1r2c1_id, 

1097 c1r2_id, 

1098 c1r1c2_id, 

1099 c1r1c1_id, 

1100 c1r1_id, 

1101 c1_id, 

1102 c2r1c1_id, 

1103 c2r1_id, 

1104 c2_id, 

1105 w_id, 

1106 ] 

1107 

1108 with communities_session(token) as api: 

1109 res = api.ListRecentCommunities(communities_pb2.ListRecentCommunitiesReq(page_size=3)) 

1110 assert [c.community_id for c in res.communities] == newest_first[:3] 

1111 for community in res.communities: 

1112 assert community.HasField("created") 

1113 assert community.created.seconds > 0 

1114 

1115 # default page size returns all communities for this fixture 

1116 res = api.ListRecentCommunities(communities_pb2.ListRecentCommunitiesReq()) 

1117 assert [c.community_id for c in res.communities] == newest_first 

1118 

1119 # page_size is clamped to MAX_PAGINATION_LENGTH on the server 

1120 monkeypatch.setattr("couchers.servicers.communities.MAX_PAGINATION_LENGTH", 4) 

1121 res = api.ListRecentCommunities(communities_pb2.ListRecentCommunitiesReq(page_size=1000)) 

1122 assert [c.community_id for c in res.communities] == newest_first[:4] 

1123 # and also applies to the default when the request omits page_size 

1124 res = api.ListRecentCommunities(communities_pb2.ListRecentCommunitiesReq()) 

1125 assert [c.community_id for c in res.communities] == newest_first[:4] 

1126 

1127 

1128def test_JoinCommunity_and_LeaveCommunity(testing_communities): 

1129 # these are separate as they mutate the database 

1130 with session_scope() as session: 

1131 # at x=1, inside c1 (country 1) 

1132 user1_id, token1 = get_user_id_and_token(session, "user1") 

1133 # at x=51, not inside c1 

1134 user8_id, token8 = get_user_id_and_token(session, "user8") 

1135 c1_id = get_community_id(session, "Country 1") 

1136 

1137 with communities_session(token1) as api: 

1138 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1139 

1140 # user1 is already part of c1, cannot join 

1141 with pytest.raises(grpc.RpcError) as e: 

1142 res = api.JoinCommunity( 

1143 communities_pb2.JoinCommunityReq( 

1144 community_id=c1_id, 

1145 ) 

1146 ) 

1147 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1148 assert e.value.details() == "You're already in that community." 

1149 

1150 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1151 

1152 # user1 is inside c1, cannot leave 

1153 with pytest.raises(grpc.RpcError) as e: 

1154 res = api.LeaveCommunity( 

1155 communities_pb2.LeaveCommunityReq( 

1156 community_id=c1_id, 

1157 ) 

1158 ) 

1159 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1160 assert ( 

1161 e.value.details() 

1162 == "Your location on your profile is within this community, so you cannot leave it. However, you can adjust your notifications in your account settings." 

1163 ) 

1164 

1165 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1166 

1167 with communities_session(token8) as api: 

1168 assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1169 

1170 # user8 is not in c1 yet, cannot leave 

1171 with pytest.raises(grpc.RpcError) as e: 

1172 res = api.LeaveCommunity( 

1173 communities_pb2.LeaveCommunityReq( 

1174 community_id=c1_id, 

1175 ) 

1176 ) 

1177 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1178 assert e.value.details() == "You're not in that community." 

1179 

1180 assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1181 

1182 # user8 is not in c1 and not part, can join 

1183 res = api.JoinCommunity( 

1184 communities_pb2.JoinCommunityReq( 

1185 community_id=c1_id, 

1186 ) 

1187 ) 

1188 

1189 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1190 

1191 # user8 is not in c1 and but now part, can't join again 

1192 with pytest.raises(grpc.RpcError) as e: 

1193 res = api.JoinCommunity( 

1194 communities_pb2.JoinCommunityReq( 

1195 community_id=c1_id, 

1196 ) 

1197 ) 

1198 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1199 assert e.value.details() == "You're already in that community." 

1200 

1201 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1202 

1203 # user8 is not in c1 yet, but part of it, can leave 

1204 res = api.LeaveCommunity( 

1205 communities_pb2.LeaveCommunityReq( 

1206 community_id=c1_id, 

1207 ) 

1208 ) 

1209 assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1210 

1211 

1212def test_LeaveCommunity_regression(db): 

1213 # See github issue #1444, repro: 

1214 # 1. Join more than one community 

1215 # 2. Leave one of them 

1216 # 3. You are no longer in any community 

1217 # admin 

1218 user1, token1 = generate_user(username="user1", geom=create_1d_point(200), geom_radius=0.1) 

1219 # joiner/leaver 

1220 user2, token2 = generate_user(username="user2", geom=create_1d_point(201), geom_radius=0.1) 

1221 

1222 with session_scope() as session: 

1223 c0 = create_community(session, 0, 100, "Community 0", [user1], [], None) 

1224 c1 = create_community(session, 0, 50, "Community 1", [user1], [], c0) 

1225 c2 = create_community(session, 0, 10, "Community 2", [user1], [], c0) 

1226 c0_id = c0.id 

1227 c1_id = c1.id 

1228 c2_id = c2.id 

1229 

1230 enforce_community_memberships() 

1231 

1232 with communities_session(token1) as api: 

1233 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c0_id)).member 

1234 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1235 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c2_id)).member 

1236 

1237 with communities_session(token2) as api: 

1238 # first check we're not in any communities 

1239 assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c0_id)).member 

1240 assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1241 assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c2_id)).member 

1242 

1243 # join some communities 

1244 api.JoinCommunity(communities_pb2.JoinCommunityReq(community_id=c1_id)) 

1245 api.JoinCommunity(communities_pb2.JoinCommunityReq(community_id=c2_id)) 

1246 

1247 # check memberships 

1248 assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c0_id)).member 

1249 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1250 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c2_id)).member 

1251 

1252 # leave just c2 

1253 api.LeaveCommunity(communities_pb2.LeaveCommunityReq(community_id=c2_id)) 

1254 

1255 # check memberships 

1256 assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c0_id)).member 

1257 assert api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c1_id)).member 

1258 assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c2_id)).member 

1259 

1260 

1261def test_enforce_community_memberships_for_user(testing_communities, fast_passwords): 

1262 """ 

1263 Make sure the user is added to the right communities on signup 

1264 """ 

1265 with auth_api_session() as (auth_api, metadata_interceptor): 

1266 res = auth_api.SignupFlow( 

1267 auth_pb2.SignupFlowReq( 

1268 basic=auth_pb2.SignupBasic(name="testing", email="email@couchers.org.invalid"), 

1269 account=auth_pb2.SignupAccount( 

1270 username="frodo", 

1271 password="a very insecure password", 

1272 birthdate="1970-01-01", 

1273 gender="Bot", 

1274 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

1275 city="Country 1, Region 1, City 2", 

1276 # lat=8, lng=1 is equivalent to creating this coordinate with create_coordinate(8) 

1277 lat=8, 

1278 lng=1, 

1279 radius=500, 

1280 accept_tos=True, 

1281 ), 

1282 feedback=auth_pb2.ContributorForm(), 

1283 accept_community_guidelines=wrappers_pb2.BoolValue(value=True), 

1284 motivations=auth_pb2.SignupMotivations(motivations=["surfing"]), 

1285 ) 

1286 ) 

1287 with session_scope() as session: 

1288 email_token = ( 

1289 session.execute(select(SignupFlow).where(SignupFlow.flow_token == res.flow_token)).scalar_one().email_token 

1290 ) 

1291 with auth_api_session() as (auth_api, metadata_interceptor): 

1292 res = auth_api.SignupFlow(auth_pb2.SignupFlowReq(email_token=email_token)) 

1293 user_id = res.auth_res.user_id 

1294 

1295 # now check the user is in the right communities 

1296 with session_scope() as session: 

1297 w_id = get_community_id(session, "Global") 

1298 c1_id = get_community_id(session, "Country 1") 

1299 c1r1_id = get_community_id(session, "Country 1, Region 1") 

1300 c1r1c2_id = get_community_id(session, "Country 1, Region 1, City 2") 

1301 

1302 token, _ = get_session_cookie_tokens(metadata_interceptor) 

1303 

1304 with communities_session(token) as api: 

1305 res = api.ListUserCommunities(communities_pb2.ListUserCommunitiesReq()) 

1306 assert [c.community_id for c in res.communities] == [c1r1c2_id, c1r1_id, c1_id, w_id] 

1307 

1308 

1309# TODO: requires transferring of content 

1310 

1311# def test_ListPlaces(db, testing_communities): 

1312# pass 

1313 

1314# def test_ListGuides(db, testing_communities): 

1315# pass 

1316 

1317# def test_ListEvents(db, testing_communities): 

1318# pass