Coverage for app/backend/src/tests/test_discussions.py: 100%
455 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
1import grpc
2import pytest
3from google.protobuf.wrappers_pb2 import StringValue
4from sqlalchemy import select
6from couchers.db import session_scope
7from couchers.models import ModerationObjectType, ModerationState, ModerationVisibility
8from couchers.models.discussions import ContentChangeType, DiscussionVersion
9from couchers.proto import admin_pb2, communities_pb2, discussions_pb2, moderation_pb2, notifications_pb2, threads_pb2
10from couchers.utils import now, to_aware_datetime
11from tests.fixtures.db import generate_user
12from tests.fixtures.misc import Moderator, PushCollector, process_jobs
13from tests.fixtures.sessions import (
14 communities_session,
15 discussions_session,
16 notifications_session,
17 real_admin_session,
18 real_moderation_session,
19 threads_session,
20)
21from tests.test_communities import create_community, create_group
24@pytest.fixture(autouse=True)
25def _(testconfig):
26 pass
29def test_create_discussion_errors(db):
30 user, token = generate_user()
31 with discussions_session(token) as api:
32 with pytest.raises(grpc.RpcError) as e:
33 api.CreateDiscussion(
34 discussions_pb2.CreateDiscussionReq(
35 title=None,
36 content="dummy content",
37 )
38 )
39 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
40 assert e.value.details() == "Missing discussion title."
42 with discussions_session(token) as api:
43 with pytest.raises(grpc.RpcError) as e:
44 api.CreateDiscussion(
45 discussions_pb2.CreateDiscussionReq(
46 title="dummy title",
47 content=None,
48 )
49 )
50 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
51 assert e.value.details() == "Missing discussion content."
54def test_create_and_get_discussion(db, push_collector: PushCollector, moderator: Moderator):
55 generate_user()
56 user, token = generate_user()
57 user2, token2 = generate_user()
58 generate_user()
59 generate_user()
61 with notifications_session(token2) as notifications:
62 notifications.SetNotificationSettings(
63 notifications_pb2.SetNotificationSettingsReq(
64 preferences=[
65 notifications_pb2.SingleNotificationPreference(
66 topic="discussion",
67 action="create",
68 delivery_method="push",
69 enabled=True,
70 )
71 ],
72 )
73 )
75 with session_scope() as session:
76 community = create_community(session, 0, 1, "Testing Community", [user2], [], None)
77 group_id = create_group(session, "Testing Group", [user2], [], community).id
78 community_id = community.id
79 user2_id = user2.id
81 with discussions_session(token) as api:
82 time_before_create = now()
83 res = api.CreateDiscussion(
84 discussions_pb2.CreateDiscussionReq(
85 title="dummy title",
86 content="dummy content",
87 owner_community_id=community_id,
88 )
89 )
90 time_after_create = now()
92 assert res.title == "dummy title"
93 assert res.content == "dummy content"
94 assert res.slug == "dummy-title"
95 assert time_before_create <= to_aware_datetime(res.created) <= time_after_create
96 assert res.creator_user_id == user.id
97 assert res.owner_community_id == community_id
99 discussion_id = res.discussion_id
101 moderator.approve_discussion(discussion_id)
102 process_jobs()
104 push = push_collector.pop_for_user(user2_id, last=True)
105 assert push.content.title == "New discussion: dummy title"
106 assert push.content.ios_title == "New Discussion"
107 assert push.content.ios_subtitle == "dummy title"
108 assert push.content.body == f"{user.name} started the discussion in Testing Community."
110 with discussions_session(token) as api:
111 res = api.GetDiscussion(
112 discussions_pb2.GetDiscussionReq(
113 discussion_id=discussion_id,
114 )
115 )
117 assert res.title == "dummy title"
118 assert res.content == "dummy content"
119 assert res.slug == "dummy-title"
120 assert time_before_create <= to_aware_datetime(res.created) <= time_after_create
121 assert res.creator_user_id == user.id
122 assert res.owner_community_id == community_id
124 with discussions_session(token) as api:
125 time_before_create = now()
126 res = api.CreateDiscussion(
127 discussions_pb2.CreateDiscussionReq(
128 title="dummy title",
129 content="dummy content",
130 owner_group_id=group_id,
131 )
132 )
133 time_after_create = now()
135 assert res.title == "dummy title"
136 assert res.content == "dummy content"
137 assert res.slug == "dummy-title"
138 assert time_before_create <= to_aware_datetime(res.created) <= time_after_create
139 assert res.creator_user_id == user.id
140 assert res.owner_group_id == group_id
142 discussion_id = res.discussion_id
144 with discussions_session(token) as api:
145 res = api.GetDiscussion(
146 discussions_pb2.GetDiscussionReq(
147 discussion_id=discussion_id,
148 )
149 )
151 assert res.title == "dummy title"
152 assert res.content == "dummy content"
153 assert res.slug == "dummy-title"
154 assert time_before_create <= to_aware_datetime(res.created) <= time_after_create
155 assert res.creator_user_id == user.id
156 assert res.owner_group_id == group_id
159def test_update_discussion(db):
160 user, token = generate_user()
161 with session_scope() as session:
162 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
163 community_id = community.id
165 with discussions_session(token) as api:
166 res = api.CreateDiscussion(
167 discussions_pb2.CreateDiscussionReq(
168 title="Original title",
169 content="Original content",
170 owner_community_id=community_id,
171 )
172 )
173 discussion_id = res.discussion_id
174 assert res.can_edit
176 with discussions_session(token) as api:
177 res = api.UpdateDiscussion(
178 discussions_pb2.UpdateDiscussionReq(
179 discussion_id=discussion_id,
180 title=StringValue(value="Updated title"),
181 content=StringValue(value="Updated content"),
182 )
183 )
184 assert res.title == "Updated title"
185 assert res.content == "Updated content"
186 assert res.can_edit
187 assert res.last_edited is not None
189 with discussions_session(token) as api:
190 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
191 assert res.title == "Updated title"
192 assert res.content == "Updated content"
193 assert res.last_edited is not None
194 assert res.can_edit
197def test_update_discussion_permission_denied(db):
198 user, token = generate_user()
199 other_user, other_token = generate_user()
200 with session_scope() as session:
201 community = create_community(session, 0, 1, "Testing Community", [user], [other_user], None)
202 community_id = community.id
204 with discussions_session(token) as api:
205 res = api.CreateDiscussion(
206 discussions_pb2.CreateDiscussionReq(
207 title="Original title",
208 content="Original content",
209 owner_community_id=community_id,
210 )
211 )
212 discussion_id = res.discussion_id
214 with discussions_session(other_token) as api:
215 with pytest.raises(grpc.RpcError) as e:
216 api.UpdateDiscussion(
217 discussions_pb2.UpdateDiscussionReq(
218 discussion_id=discussion_id,
219 )
220 )
221 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
222 assert e.value.details() == "You are not allowed to edit this discussion."
225def test_update_deleted_discussion(db):
226 user, token = generate_user()
227 with session_scope() as session:
228 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
229 community_id = community.id
231 with discussions_session(token) as api:
232 res = api.CreateDiscussion(
233 discussions_pb2.CreateDiscussionReq(
234 title="Original title",
235 content="Original content",
236 owner_community_id=community_id,
237 )
238 )
239 discussion_id = res.discussion_id
240 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
242 with discussions_session(token) as api:
243 with pytest.raises(grpc.RpcError) as e:
244 api.UpdateDiscussion(discussions_pb2.UpdateDiscussionReq(discussion_id=discussion_id))
245 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
246 assert e.value.details() == "This discussion has been deleted."
249def test_delete_discussion_by_creator(db):
250 user, token = generate_user()
251 with session_scope() as session:
252 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
253 community_id = community.id
255 with discussions_session(token) as api:
256 res = api.CreateDiscussion(
257 discussions_pb2.CreateDiscussionReq(
258 title="To be deleted",
259 content="Some content",
260 owner_community_id=community_id,
261 )
262 )
263 discussion_id = res.discussion_id
265 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
267 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
268 assert res.deleted
269 assert res.title == ""
270 assert res.content == ""
271 assert res.creator_user_id == 0
272 assert res.thread.thread_id != 0
275def test_delete_discussion_permission_denied_for_non_creator(db):
276 creator, creator_token = generate_user()
277 community_admin, community_admin_token = generate_user()
278 random_user, random_token = generate_user()
279 with session_scope() as session:
280 community = create_community(
281 session, 0, 1, "Testing Community", [community_admin], [creator, random_user], None
282 )
283 community_id = community.id
285 with discussions_session(creator_token) as api:
286 res = api.CreateDiscussion(
287 discussions_pb2.CreateDiscussionReq(
288 title="Only creator can delete",
289 content="Some content",
290 owner_community_id=community_id,
291 )
292 )
293 discussion_id = res.discussion_id
294 assert res.can_edit
296 with discussions_session(random_token) as api:
297 with pytest.raises(grpc.RpcError) as e:
298 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
299 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
301 with discussions_session(community_admin_token) as api:
302 with pytest.raises(grpc.RpcError) as e:
303 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
304 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
307def test_deleted_discussion_not_in_list(db):
308 user, token = generate_user()
309 with session_scope() as session:
310 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
311 community_id = community.id
313 with discussions_session(token) as api:
314 res = api.CreateDiscussion(
315 discussions_pb2.CreateDiscussionReq(
316 title="Visible discussion",
317 content="Content",
318 owner_community_id=community_id,
319 )
320 )
321 visible_id = res.discussion_id
323 res = api.CreateDiscussion(
324 discussions_pb2.CreateDiscussionReq(
325 title="Deleted discussion",
326 content="Content",
327 owner_community_id=community_id,
328 )
329 )
330 deleted_id = res.discussion_id
331 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=deleted_id))
333 with communities_session(token) as api:
334 res = api.ListDiscussions(communities_pb2.ListDiscussionsReq(community_id=community_id))
335 ids = [d.discussion_id for d in res.discussions]
336 assert visible_id in ids
337 assert deleted_id not in ids
340def test_deleted_discussion_thread_still_accessible(db, moderator: Moderator):
341 user, token = generate_user()
342 commenter, commenter_token = generate_user()
343 with session_scope() as session:
344 community = create_community(session, 0, 1, "Testing Community", [user], [commenter], None)
345 community_id = community.id
347 with discussions_session(token) as api:
348 res = api.CreateDiscussion(
349 discussions_pb2.CreateDiscussionReq(
350 title="Discussion with comments",
351 content="Content",
352 owner_community_id=community_id,
353 )
354 )
355 discussion_id = res.discussion_id
356 thread_id = res.thread.thread_id
358 process_jobs()
360 with threads_session(commenter_token) as api:
361 comment_thread_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=thread_id, content="a comment")).thread_id
362 moderator.approve_thread_post(comment_thread_id)
364 with discussions_session(token) as api:
365 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
366 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
367 assert res.deleted
368 assert res.thread.num_responses == 1
370 with threads_session(token) as api:
371 res = api.GetThread(threads_pb2.GetThreadReq(thread_id=thread_id))
372 assert len(res.replies) == 1
375def test_delete_comment_shows_placeholder_with_replies(db, moderator: Moderator):
376 """Deleting a top-level comment that has nested replies should show a deleted
377 placeholder so the replies remain visible."""
378 user, token = generate_user()
379 commenter, commenter_token = generate_user()
380 replier, replier_token = generate_user()
381 with session_scope() as session:
382 community = create_community(session, 0, 1, "Testing Community", [user], [commenter, replier], None)
383 community_id = community.id
385 with discussions_session(token) as api:
386 discussion = api.CreateDiscussion(
387 discussions_pb2.CreateDiscussionReq(
388 title="A discussion",
389 content="Content",
390 owner_community_id=community_id,
391 )
392 )
393 discussion_id = discussion.discussion_id
394 thread_id = discussion.thread.thread_id
396 # commenter adds a top-level comment
397 with threads_session(commenter_token) as api:
398 comment_thread_id = api.PostReply(
399 threads_pb2.PostReplyReq(thread_id=thread_id, content="top-level comment")
400 ).thread_id
402 # replier adds two nested replies
403 with threads_session(replier_token) as api:
404 reply_thread_id_1 = api.PostReply(
405 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="nested reply 1")
406 ).thread_id
407 reply_thread_id_2 = api.PostReply(
408 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="nested reply 2")
409 ).thread_id
411 moderator.approve_thread_post(comment_thread_id)
412 moderator.approve_thread_post(reply_thread_id_1)
413 moderator.approve_thread_post(reply_thread_id_2)
415 # commenter deletes their top-level comment
416 with threads_session(commenter_token) as api:
417 api.DeleteReply(threads_pb2.DeleteReplyReq(thread_id=comment_thread_id))
419 # the top-level comment appears as a deleted placeholder (has replies), replies still visible
420 with threads_session(token) as api:
421 thread_res = api.GetThread(threads_pb2.GetThreadReq(thread_id=thread_id))
422 assert len(thread_res.replies) == 1
423 placeholder = thread_res.replies[0]
424 assert placeholder.deleted
425 assert placeholder.content == ""
426 assert placeholder.author_user_id == 0
428 nested = api.GetThread(threads_pb2.GetThreadReq(thread_id=placeholder.thread_id))
429 assert len(nested.replies) == 2
431 # num_responses counts the 2 visible nested replies (not the deleted top-level)
432 with discussions_session(token) as api:
433 disc = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
434 assert disc.thread.num_responses == 2
437def test_admin_delete_discussion(db):
438 user, token = generate_user()
439 admin, admin_token = generate_user(is_superuser=True)
440 with session_scope() as session:
441 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
442 community_id = community.id
444 with discussions_session(token) as api:
445 res = api.CreateDiscussion(
446 discussions_pb2.CreateDiscussionReq(
447 title="Admin will delete this",
448 content="Content",
449 owner_community_id=community_id,
450 )
451 )
452 discussion_id = res.discussion_id
454 with real_admin_session(admin_token) as api:
455 api.DeleteDiscussion(admin_pb2.AdminDeleteDiscussionReq(discussion_id=discussion_id))
457 with discussions_session(token) as api:
458 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
459 assert res.deleted
462def test_discussion_notifications_regression(db, push_collector: PushCollector, moderator: Moderator):
463 generate_user()
464 user, token = generate_user()
465 user2, token2 = generate_user()
466 user3, token3 = generate_user()
467 generate_user()
468 generate_user()
470 with session_scope() as session:
471 community = create_community(session, 0, 1, "Testing Community", [user2], [], None)
472 group_id = create_group(session, "Testing Group", [user2], [], community).id
473 community_id = community.id
474 user2_id = user2.id
476 with discussions_session(token) as api:
477 time_before_create = now()
478 res = api.CreateDiscussion(
479 discussions_pb2.CreateDiscussionReq(
480 title="dummy title",
481 content="dummy content",
482 owner_community_id=community_id,
483 )
484 )
485 time_after_create = now()
487 assert res.title == "dummy title"
488 assert res.content == "dummy content"
489 assert res.slug == "dummy-title"
490 assert time_before_create <= to_aware_datetime(res.created) <= time_after_create
491 assert res.creator_user_id == user.id
492 assert res.owner_community_id == community_id
494 discussion_id = res.discussion_id
495 thread_id = res.thread.thread_id
497 with threads_session(token2) as api:
498 comment_thread_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=thread_id, content="comment")).thread_id
499 moderator.approve_thread_post(comment_thread_id)
501 with threads_session(token3) as api:
502 reply_thread_id_a = api.PostReply(
503 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="reply to comment")
504 ).thread_id
505 moderator.approve_thread_post(reply_thread_id_a)
507 with threads_session(token) as api:
508 reply_thread_id_b = api.PostReply(
509 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="reply to reply to comment")
510 ).thread_id
511 moderator.approve_thread_post(reply_thread_id_b)
513 process_jobs()
515 # User2 should get 2 notifications about 2 replies to their comment, User3 should get 1 notification about 1 reply
516 push = push_collector.pop_for_user(user2_id, last=False)
517 assert push.content.title == f"{user3.name} • dummy title"
518 assert push.topic_action == "thread:reply"
520 push = push_collector.pop_for_user(user2_id, last=True)
521 assert push.content.title == f"{user.name} • dummy title"
522 assert push.topic_action == "thread:reply"
524 push = push_collector.pop_for_user(user3.id, last=True)
525 assert push.content.title == f"{user.name} • dummy title"
526 assert push.topic_action == "thread:reply"
529def test_create_discussion_creates_moderation_state(db):
530 user, token = generate_user()
532 with session_scope() as session:
533 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
534 community_id = community.id
536 with discussions_session(token) as api:
537 res = api.CreateDiscussion(
538 discussions_pb2.CreateDiscussionReq(
539 title="t",
540 content="c",
541 owner_community_id=community_id,
542 )
543 )
544 discussion_id = res.discussion_id
546 with session_scope() as session:
547 state = session.execute(
548 select(ModerationState)
549 .where(ModerationState.object_type == ModerationObjectType.discussion)
550 .where(ModerationState.object_id == discussion_id)
551 ).scalar_one()
552 assert state.visibility == ModerationVisibility.shadowed
555def test_shadowed_discussion_visible_to_author_only(db):
556 author, author_token = generate_user()
557 _, other_token = generate_user()
559 with session_scope() as session:
560 community = create_community(session, 0, 1, "Testing Community", [author], [], None)
561 community_id = community.id
563 with discussions_session(author_token) as api:
564 res = api.CreateDiscussion(
565 discussions_pb2.CreateDiscussionReq(
566 title="secret",
567 content="secret content",
568 owner_community_id=community_id,
569 )
570 )
571 discussion_id = res.discussion_id
573 # Author can read their own shadowed discussion
574 with discussions_session(author_token) as api:
575 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
576 assert res.title == "secret"
578 # Other user cannot
579 with discussions_session(other_token) as api:
580 with pytest.raises(grpc.RpcError) as e:
581 api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
582 assert e.value.code() == grpc.StatusCode.NOT_FOUND
585def test_shadowed_discussion_excluded_from_listings(db):
586 author, author_token = generate_user()
587 other, other_token = generate_user()
589 with session_scope() as session:
590 community = create_community(session, 0, 1, "Testing Community", [author, other], [], None)
591 community_id = community.id
593 with discussions_session(author_token) as api:
594 api.CreateDiscussion(
595 discussions_pb2.CreateDiscussionReq(
596 title="hidden-from-listings",
597 content="c",
598 owner_community_id=community_id,
599 )
600 )
602 # Other user does not see the shadowed discussion in listings
603 with communities_session(other_token) as api:
604 res = api.ListDiscussions(communities_pb2.ListDiscussionsReq(community_id=community_id))
605 assert [d.title for d in res.discussions] == []
608def test_approved_discussion_visible_to_others(db, moderator: Moderator):
609 author, author_token = generate_user()
610 _, other_token = generate_user()
612 with session_scope() as session:
613 community = create_community(session, 0, 1, "Testing Community", [author], [], None)
614 community_id = community.id
616 with discussions_session(author_token) as api:
617 res = api.CreateDiscussion(
618 discussions_pb2.CreateDiscussionReq(
619 title="hello",
620 content="world",
621 owner_community_id=community_id,
622 )
623 )
624 discussion_id = res.discussion_id
626 moderator.approve_discussion(discussion_id)
628 with discussions_session(other_token) as api:
629 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
630 assert res.title == "hello"
633def test_hidden_discussion_filtered_for_author_too(db, moderator: Moderator):
634 """Once admin hides a discussion, even the author cannot read it."""
635 author, author_token = generate_user()
637 with session_scope() as session:
638 community = create_community(session, 0, 1, "Testing Community", [author], [], None)
639 community_id = community.id
641 with discussions_session(author_token) as api:
642 res = api.CreateDiscussion(
643 discussions_pb2.CreateDiscussionReq(
644 title="bad",
645 content="c",
646 owner_community_id=community_id,
647 )
648 )
649 discussion_id = res.discussion_id
651 # Hide via moderator (visibility=hidden, not visible)
652 with real_moderation_session(moderator.token) as api:
653 state_res = api.GetModerationState(
654 moderation_pb2.GetModerationStateReq(
655 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_DISCUSSION,
656 object_id=discussion_id,
657 )
658 )
659 api.ModerateContent(
660 moderation_pb2.ModerateContentReq(
661 moderation_state_id=state_res.moderation_state.moderation_state_id,
662 action=moderation_pb2.MODERATION_ACTION_HIDE,
663 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
664 reason="bad content",
665 )
666 )
668 with discussions_session(author_token) as api:
669 with pytest.raises(grpc.RpcError) as e:
670 api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
671 assert e.value.code() == grpc.StatusCode.NOT_FOUND
674def test_list_my_communities_discussions_respects_moderation(db, moderator: Moderator):
675 """A shadowed discussion is hidden from other community members until approved."""
676 author, author_token = generate_user()
677 other, other_token = generate_user()
679 with session_scope() as session:
680 community = create_community(session, 0, 1, "Testing Community", [author, other], [], None)
681 community_id = community.id
683 with discussions_session(author_token) as api:
684 discussion_id = api.CreateDiscussion(
685 discussions_pb2.CreateDiscussionReq(
686 title="hello",
687 content="world",
688 owner_community_id=community_id,
689 )
690 ).discussion_id
692 # Author sees their own shadowed discussion
693 with discussions_session(author_token) as api:
694 res = api.ListMyCommunitiesDiscussions(discussions_pb2.ListMyCommunitiesDiscussionsReq())
695 assert [d.title for d in res.discussions] == ["hello"]
697 # Other members do not see the shadowed discussion
698 with discussions_session(other_token) as api:
699 res = api.ListMyCommunitiesDiscussions(discussions_pb2.ListMyCommunitiesDiscussionsReq())
700 assert [d.title for d in res.discussions] == []
702 moderator.approve_discussion(discussion_id)
704 # Once approved, it shows up for other members too
705 with discussions_session(other_token) as api:
706 res = api.ListMyCommunitiesDiscussions(discussions_pb2.ListMyCommunitiesDiscussionsReq())
707 assert [d.title for d in res.discussions] == ["hello"]
710def test_update_discussion_creates_version_record(db):
711 user, token = generate_user()
712 with session_scope() as session:
713 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
714 community_id = community.id
716 with discussions_session(token) as api:
717 res = api.CreateDiscussion(
718 discussions_pb2.CreateDiscussionReq(
719 title="Original title",
720 content="Original content",
721 owner_community_id=community_id,
722 )
723 )
724 discussion_id = res.discussion_id
726 with discussions_session(token) as api:
727 api.UpdateDiscussion(
728 discussions_pb2.UpdateDiscussionReq(
729 discussion_id=discussion_id,
730 title=StringValue(value="Updated title"),
731 content=StringValue(value="Updated content"),
732 )
733 )
735 with session_scope() as session:
736 versions = (
737 session.execute(select(DiscussionVersion).where(DiscussionVersion.discussion_id == discussion_id))
738 .scalars()
739 .all()
740 )
741 assert len(versions) == 1
742 v = versions[0]
743 assert v.change_type == ContentChangeType.edit
744 assert v.old_title == "Original title"
745 assert v.new_title == "Updated title"
746 assert v.old_content == "Original content"
747 assert v.new_content == "Updated content"
748 assert v.editor_user_id == user.id
751def test_update_discussion_multiple_edits_creates_multiple_version_records(db):
752 user, token = generate_user()
753 with session_scope() as session:
754 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
755 community_id = community.id
757 with discussions_session(token) as api:
758 discussion_id = api.CreateDiscussion(
759 discussions_pb2.CreateDiscussionReq(
760 title="v1 title",
761 content="v1 content",
762 owner_community_id=community_id,
763 )
764 ).discussion_id
766 with discussions_session(token) as api:
767 api.UpdateDiscussion(
768 discussions_pb2.UpdateDiscussionReq(
769 discussion_id=discussion_id,
770 title=StringValue(value="v2 title"),
771 content=StringValue(value="v2 content"),
772 )
773 )
775 with discussions_session(token) as api:
776 api.UpdateDiscussion(
777 discussions_pb2.UpdateDiscussionReq(
778 discussion_id=discussion_id,
779 title=StringValue(value="v3 title"),
780 content=StringValue(value="v3 content"),
781 )
782 )
784 with session_scope() as session:
785 versions = (
786 session.execute(
787 select(DiscussionVersion)
788 .where(DiscussionVersion.discussion_id == discussion_id)
789 .order_by(DiscussionVersion.id)
790 )
791 .scalars()
792 .all()
793 )
794 assert len(versions) == 2
795 assert versions[0].old_title == "v1 title"
796 assert versions[0].new_title == "v2 title"
797 assert versions[1].old_title == "v2 title"
798 assert versions[1].new_title == "v3 title"
801def test_delete_discussion_creates_version_record(db):
802 user, token = generate_user()
803 with session_scope() as session:
804 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
805 community_id = community.id
807 with discussions_session(token) as api:
808 discussion_id = api.CreateDiscussion(
809 discussions_pb2.CreateDiscussionReq(
810 title="To be deleted",
811 content="Some content",
812 owner_community_id=community_id,
813 )
814 ).discussion_id
815 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
817 with session_scope() as session:
818 versions = (
819 session.execute(select(DiscussionVersion).where(DiscussionVersion.discussion_id == discussion_id))
820 .scalars()
821 .all()
822 )
823 assert len(versions) == 1
824 v = versions[0]
825 assert v.change_type == ContentChangeType.delete
826 assert v.old_title == "To be deleted"
827 assert v.new_title is None
828 assert v.old_content == "Some content"
829 assert v.new_content is None
830 assert v.editor_user_id == user.id
833def test_admin_delete_discussion_creates_version_record(db):
834 user, token = generate_user()
835 admin, admin_token = generate_user(is_superuser=True)
836 with session_scope() as session:
837 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
838 community_id = community.id
840 with discussions_session(token) as api:
841 discussion_id = api.CreateDiscussion(
842 discussions_pb2.CreateDiscussionReq(
843 title="Admin will delete this",
844 content="Some content",
845 owner_community_id=community_id,
846 )
847 ).discussion_id
849 with real_admin_session(admin_token) as api:
850 api.DeleteDiscussion(admin_pb2.AdminDeleteDiscussionReq(discussion_id=discussion_id))
852 with session_scope() as session:
853 versions = (
854 session.execute(select(DiscussionVersion).where(DiscussionVersion.discussion_id == discussion_id))
855 .scalars()
856 .all()
857 )
858 assert len(versions) == 1
859 v = versions[0]
860 assert v.change_type == ContentChangeType.delete
861 assert v.old_title == "Admin will delete this"
862 assert v.new_title is None
863 assert v.editor_user_id == admin.id