Coverage for app/backend/src/tests/test_discussions.py: 100%
459 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +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
24def test_create_discussion_errors(db):
25 user, token = generate_user()
26 with discussions_session(token) as api:
27 with pytest.raises(grpc.RpcError) as e:
28 api.CreateDiscussion(
29 discussions_pb2.CreateDiscussionReq(
30 title=None,
31 content="dummy content",
32 )
33 )
34 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
35 assert e.value.details() == "Missing discussion title."
37 with discussions_session(token) as api:
38 with pytest.raises(grpc.RpcError) as e:
39 api.CreateDiscussion(
40 discussions_pb2.CreateDiscussionReq(
41 title="dummy title",
42 content=None,
43 )
44 )
45 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
46 assert e.value.details() == "Missing discussion content."
49def test_create_and_get_discussion(db, frozen_timewarp, push_collector: PushCollector, moderator: Moderator):
50 generate_user()
51 user, token = generate_user()
52 user2, token2 = generate_user()
53 generate_user()
54 generate_user()
56 with notifications_session(token2) as notifications:
57 notifications.SetNotificationSettings(
58 notifications_pb2.SetNotificationSettingsReq(
59 preferences=[
60 notifications_pb2.SingleNotificationPreference(
61 topic="discussion",
62 action="create",
63 delivery_method="push",
64 enabled=True,
65 )
66 ],
67 )
68 )
70 with session_scope() as session:
71 community = create_community(session, 0, 1, "Testing Community", [user2], [], None)
72 group_id = create_group(session, "Testing Group", [user2], [], community).id
73 community_id = community.id
74 user2_id = user2.id
76 with discussions_session(token) as api:
77 time_before_create = now()
78 res = api.CreateDiscussion(
79 discussions_pb2.CreateDiscussionReq(
80 title="dummy title",
81 content="dummy content",
82 owner_community_id=community_id,
83 )
84 )
85 time_after_create = now()
87 assert res.title == "dummy title"
88 assert res.content == "dummy content"
89 assert res.slug == "dummy-title"
90 assert time_before_create <= to_aware_datetime(res.created) <= time_after_create
91 assert res.creator_user_id == user.id
92 assert res.owner_community_id == community_id
94 discussion_id = res.discussion_id
96 moderator.approve_discussion(discussion_id)
97 process_jobs()
99 push = push_collector.pop_for_user(user2_id, last=True)
100 assert push.content.title == "New discussion: dummy title"
101 assert push.content.ios_title == "New Discussion"
102 assert push.content.ios_subtitle == "dummy title"
103 assert push.content.body == f"{user.name} started the discussion in Testing Community."
105 with discussions_session(token) as api:
106 res = api.GetDiscussion(
107 discussions_pb2.GetDiscussionReq(
108 discussion_id=discussion_id,
109 )
110 )
112 assert res.title == "dummy title"
113 assert res.content == "dummy content"
114 assert res.slug == "dummy-title"
115 assert time_before_create <= to_aware_datetime(res.created) <= time_after_create
116 assert res.creator_user_id == user.id
117 assert res.owner_community_id == community_id
119 with discussions_session(token) as api:
120 time_before_create = now()
121 res = api.CreateDiscussion(
122 discussions_pb2.CreateDiscussionReq(
123 title="dummy title",
124 content="dummy content",
125 owner_group_id=group_id,
126 )
127 )
128 time_after_create = now()
130 assert res.title == "dummy title"
131 assert res.content == "dummy content"
132 assert res.slug == "dummy-title"
133 assert time_before_create <= to_aware_datetime(res.created) <= time_after_create
134 assert res.creator_user_id == user.id
135 assert res.owner_group_id == group_id
137 discussion_id = res.discussion_id
139 with discussions_session(token) as api:
140 res = api.GetDiscussion(
141 discussions_pb2.GetDiscussionReq(
142 discussion_id=discussion_id,
143 )
144 )
146 assert res.title == "dummy title"
147 assert res.content == "dummy content"
148 assert res.slug == "dummy-title"
149 assert time_before_create <= to_aware_datetime(res.created) <= time_after_create
150 assert res.creator_user_id == user.id
151 assert res.owner_group_id == group_id
154def test_update_discussion(db):
155 user, token = generate_user()
156 with session_scope() as session:
157 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
158 community_id = community.id
160 with discussions_session(token) as api:
161 res = api.CreateDiscussion(
162 discussions_pb2.CreateDiscussionReq(
163 title="Original title",
164 content="Original content",
165 owner_community_id=community_id,
166 )
167 )
168 discussion_id = res.discussion_id
169 assert res.can_edit
171 with discussions_session(token) as api:
172 res = api.UpdateDiscussion(
173 discussions_pb2.UpdateDiscussionReq(
174 discussion_id=discussion_id,
175 title=StringValue(value="Updated title"),
176 content=StringValue(value="Updated content"),
177 )
178 )
179 assert res.title == "Updated title"
180 assert res.content == "Updated content"
181 assert res.can_edit
182 assert res.last_edited is not None
184 with discussions_session(token) as api:
185 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
186 assert res.title == "Updated title"
187 assert res.content == "Updated content"
188 assert res.last_edited is not None
189 assert res.can_edit
192def test_update_discussion_permission_denied(db):
193 user, token = generate_user()
194 other_user, other_token = generate_user()
195 with session_scope() as session:
196 community = create_community(session, 0, 1, "Testing Community", [user], [other_user], None)
197 community_id = community.id
199 with discussions_session(token) as api:
200 res = api.CreateDiscussion(
201 discussions_pb2.CreateDiscussionReq(
202 title="Original title",
203 content="Original content",
204 owner_community_id=community_id,
205 )
206 )
207 discussion_id = res.discussion_id
209 with discussions_session(other_token) as api:
210 with pytest.raises(grpc.RpcError) as e:
211 api.UpdateDiscussion(
212 discussions_pb2.UpdateDiscussionReq(
213 discussion_id=discussion_id,
214 )
215 )
216 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
217 assert e.value.details() == "You are not allowed to edit this discussion."
220def test_update_deleted_discussion(db):
221 user, token = generate_user()
222 with session_scope() as session:
223 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
224 community_id = community.id
226 with discussions_session(token) as api:
227 res = api.CreateDiscussion(
228 discussions_pb2.CreateDiscussionReq(
229 title="Original title",
230 content="Original content",
231 owner_community_id=community_id,
232 )
233 )
234 discussion_id = res.discussion_id
235 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
237 with discussions_session(token) as api:
238 with pytest.raises(grpc.RpcError) as e:
239 api.UpdateDiscussion(discussions_pb2.UpdateDiscussionReq(discussion_id=discussion_id))
240 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
241 assert e.value.details() == "This discussion has been deleted."
244def test_delete_discussion_by_creator(db):
245 user, token = generate_user()
246 with session_scope() as session:
247 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
248 community_id = community.id
250 with discussions_session(token) as api:
251 res = api.CreateDiscussion(
252 discussions_pb2.CreateDiscussionReq(
253 title="To be deleted",
254 content="Some content",
255 owner_community_id=community_id,
256 )
257 )
258 discussion_id = res.discussion_id
260 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
262 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
263 assert res.deleted
264 assert res.title == ""
265 assert res.content == ""
266 assert res.creator_user_id == 0
267 assert res.thread.thread_id != 0
270def test_delete_discussion_permission_denied_for_non_creator(db):
271 creator, creator_token = generate_user()
272 community_admin, community_admin_token = generate_user()
273 random_user, random_token = generate_user()
274 with session_scope() as session:
275 community = create_community(
276 session, 0, 1, "Testing Community", [community_admin], [creator, random_user], None
277 )
278 community_id = community.id
280 with discussions_session(creator_token) as api:
281 res = api.CreateDiscussion(
282 discussions_pb2.CreateDiscussionReq(
283 title="Only creator can delete",
284 content="Some content",
285 owner_community_id=community_id,
286 )
287 )
288 discussion_id = res.discussion_id
289 assert res.can_edit
291 with discussions_session(random_token) as api:
292 with pytest.raises(grpc.RpcError) as e:
293 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
294 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
296 with discussions_session(community_admin_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
302def test_deleted_discussion_not_in_list(db):
303 user, token = generate_user()
304 with session_scope() as session:
305 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
306 community_id = community.id
308 with discussions_session(token) as api:
309 res = api.CreateDiscussion(
310 discussions_pb2.CreateDiscussionReq(
311 title="Visible discussion",
312 content="Content",
313 owner_community_id=community_id,
314 )
315 )
316 visible_id = res.discussion_id
318 res = api.CreateDiscussion(
319 discussions_pb2.CreateDiscussionReq(
320 title="Deleted discussion",
321 content="Content",
322 owner_community_id=community_id,
323 )
324 )
325 deleted_id = res.discussion_id
326 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=deleted_id))
328 with communities_session(token) as api:
329 res = api.ListDiscussions(communities_pb2.ListDiscussionsReq(community_id=community_id))
330 ids = [d.discussion_id for d in res.discussions]
331 assert visible_id in ids
332 assert deleted_id not in ids
335def test_deleted_discussion_thread_still_accessible(db, moderator: Moderator):
336 user, token = generate_user()
337 commenter, commenter_token = generate_user()
338 with session_scope() as session:
339 community = create_community(session, 0, 1, "Testing Community", [user], [commenter], None)
340 community_id = community.id
342 with discussions_session(token) as api:
343 res = api.CreateDiscussion(
344 discussions_pb2.CreateDiscussionReq(
345 title="Discussion with comments",
346 content="Content",
347 owner_community_id=community_id,
348 )
349 )
350 discussion_id = res.discussion_id
351 thread_id = res.thread.thread_id
353 process_jobs()
355 with threads_session(commenter_token) as api:
356 comment_thread_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=thread_id, content="a comment")).thread_id
357 moderator.approve_thread_post(comment_thread_id)
359 with discussions_session(token) as api:
360 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
361 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
362 assert res.deleted
363 assert res.thread.num_responses == 1
365 with threads_session(token) as api:
366 res = api.GetThread(threads_pb2.GetThreadReq(thread_id=thread_id))
367 assert len(res.replies) == 1
370def test_delete_comment_shows_placeholder_with_replies(db, moderator: Moderator):
371 """Deleting a top-level comment that has nested replies should show a deleted
372 placeholder so the replies remain visible."""
373 user, token = generate_user()
374 commenter, commenter_token = generate_user()
375 replier, replier_token = generate_user()
376 with session_scope() as session:
377 community = create_community(session, 0, 1, "Testing Community", [user], [commenter, replier], None)
378 community_id = community.id
380 with discussions_session(token) as api:
381 discussion = api.CreateDiscussion(
382 discussions_pb2.CreateDiscussionReq(
383 title="A discussion",
384 content="Content",
385 owner_community_id=community_id,
386 )
387 )
388 discussion_id = discussion.discussion_id
389 thread_id = discussion.thread.thread_id
391 # commenter adds a top-level comment
392 with threads_session(commenter_token) as api:
393 comment_thread_id = api.PostReply(
394 threads_pb2.PostReplyReq(thread_id=thread_id, content="top-level comment")
395 ).thread_id
397 # replier adds two nested replies
398 with threads_session(replier_token) as api:
399 reply_thread_id_1 = api.PostReply(
400 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="nested reply 1")
401 ).thread_id
402 reply_thread_id_2 = api.PostReply(
403 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="nested reply 2")
404 ).thread_id
406 moderator.approve_thread_post(comment_thread_id)
407 moderator.approve_thread_post(reply_thread_id_1)
408 moderator.approve_thread_post(reply_thread_id_2)
410 # commenter deletes their top-level comment
411 with threads_session(commenter_token) as api:
412 api.DeleteReply(threads_pb2.DeleteReplyReq(thread_id=comment_thread_id))
414 # the top-level comment appears as a deleted placeholder (has replies), replies still visible
415 with threads_session(token) as api:
416 thread_res = api.GetThread(threads_pb2.GetThreadReq(thread_id=thread_id))
417 assert len(thread_res.replies) == 1
418 placeholder = thread_res.replies[0]
419 assert placeholder.deleted
420 assert placeholder.content == ""
421 assert placeholder.author_user_id == 0
423 nested = api.GetThread(threads_pb2.GetThreadReq(thread_id=placeholder.thread_id))
424 assert len(nested.replies) == 2
426 # num_responses counts the 2 visible nested replies (not the deleted top-level)
427 with discussions_session(token) as api:
428 disc = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
429 assert disc.thread.num_responses == 2
432def test_admin_delete_discussion(db):
433 user, token = generate_user()
434 admin, admin_token = generate_user(is_superuser=True)
435 with session_scope() as session:
436 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
437 community_id = community.id
439 with discussions_session(token) as api:
440 res = api.CreateDiscussion(
441 discussions_pb2.CreateDiscussionReq(
442 title="Admin will delete this",
443 content="Content",
444 owner_community_id=community_id,
445 )
446 )
447 discussion_id = res.discussion_id
449 with real_admin_session(admin_token) as api:
450 api.DeleteDiscussion(admin_pb2.AdminDeleteDiscussionReq(discussion_id=discussion_id))
452 with discussions_session(token) as api:
453 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
454 assert res.deleted
457def test_discussion_notifications_regression(db, push_collector: PushCollector, moderator: Moderator):
458 generate_user()
459 user, token = generate_user()
460 user2, token2 = generate_user()
461 user3, token3 = generate_user()
462 generate_user()
463 generate_user()
465 with session_scope() as session:
466 community = create_community(session, 0, 1, "Testing Community", [user2], [], None)
467 group_id = create_group(session, "Testing Group", [user2], [], community).id
468 community_id = community.id
469 user2_id = user2.id
471 with discussions_session(token) as api:
472 res = api.CreateDiscussion(
473 discussions_pb2.CreateDiscussionReq(
474 title="dummy title",
475 content="dummy content",
476 owner_community_id=community_id,
477 )
478 )
480 assert res.title == "dummy title"
481 assert res.content == "dummy content"
482 assert res.slug == "dummy-title"
483 assert res.creator_user_id == user.id
484 assert res.owner_community_id == community_id
486 discussion_id = res.discussion_id
487 thread_id = res.thread.thread_id
489 with threads_session(token2) as api:
490 comment_thread_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=thread_id, content="comment")).thread_id
491 moderator.approve_thread_post(comment_thread_id)
493 with threads_session(token3) as api:
494 reply_thread_id_a = api.PostReply(
495 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="reply to comment")
496 ).thread_id
497 moderator.approve_thread_post(reply_thread_id_a)
499 with threads_session(token) as api:
500 reply_thread_id_b = api.PostReply(
501 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="reply to reply to comment")
502 ).thread_id
503 moderator.approve_thread_post(reply_thread_id_b)
505 process_jobs()
507 # User2 should get 2 notifications about 2 replies to their comment, User3 should get 1 notification about 1 reply
508 push = push_collector.pop_for_user(user2_id, last=False)
509 assert push.content.title == f"{user3.name} • dummy title"
510 assert push.topic_action == "thread:reply"
512 push = push_collector.pop_for_user(user2_id, last=True)
513 assert push.content.title == f"{user.name} • dummy title"
514 assert push.topic_action == "thread:reply"
516 push = push_collector.pop_for_user(user3.id, last=True)
517 assert push.content.title == f"{user.name} • dummy title"
518 assert push.topic_action == "thread:reply"
521def test_create_discussion_creates_moderation_state(db):
522 user, token = generate_user()
524 with session_scope() as session:
525 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
526 community_id = community.id
528 with discussions_session(token) as api:
529 res = api.CreateDiscussion(
530 discussions_pb2.CreateDiscussionReq(
531 title="t",
532 content="c",
533 owner_community_id=community_id,
534 )
535 )
536 discussion_id = res.discussion_id
538 with session_scope() as session:
539 state = session.execute(
540 select(ModerationState)
541 .where(ModerationState.object_type == ModerationObjectType.discussion)
542 .where(ModerationState.object_id == discussion_id)
543 ).scalar_one()
544 assert state.visibility == ModerationVisibility.shadowed
547def test_shadowed_discussion_visible_to_author_only(db):
548 author, author_token = generate_user()
549 _, other_token = generate_user()
551 with session_scope() as session:
552 community = create_community(session, 0, 1, "Testing Community", [author], [], None)
553 community_id = community.id
555 with discussions_session(author_token) as api:
556 res = api.CreateDiscussion(
557 discussions_pb2.CreateDiscussionReq(
558 title="secret",
559 content="secret content",
560 owner_community_id=community_id,
561 )
562 )
563 discussion_id = res.discussion_id
565 # Author can read their own shadowed discussion
566 with discussions_session(author_token) as api:
567 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
568 assert res.title == "secret"
570 # Other user cannot
571 with discussions_session(other_token) as api:
572 with pytest.raises(grpc.RpcError) as e:
573 api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
574 assert e.value.code() == grpc.StatusCode.NOT_FOUND
577def test_shadowed_discussion_excluded_from_listings(db):
578 author, author_token = generate_user()
579 other, other_token = generate_user()
581 with session_scope() as session:
582 community = create_community(session, 0, 1, "Testing Community", [author, other], [], None)
583 community_id = community.id
585 with discussions_session(author_token) as api:
586 api.CreateDiscussion(
587 discussions_pb2.CreateDiscussionReq(
588 title="hidden-from-listings",
589 content="c",
590 owner_community_id=community_id,
591 )
592 )
594 # Other user does not see the shadowed discussion in listings
595 with communities_session(other_token) as api:
596 res = api.ListDiscussions(communities_pb2.ListDiscussionsReq(community_id=community_id))
597 assert [d.title for d in res.discussions] == []
600def test_approved_discussion_visible_to_others(db, moderator: Moderator):
601 author, author_token = generate_user()
602 _, other_token = generate_user()
604 with session_scope() as session:
605 community = create_community(session, 0, 1, "Testing Community", [author], [], None)
606 community_id = community.id
608 with discussions_session(author_token) as api:
609 res = api.CreateDiscussion(
610 discussions_pb2.CreateDiscussionReq(
611 title="hello",
612 content="world",
613 owner_community_id=community_id,
614 )
615 )
616 discussion_id = res.discussion_id
618 moderator.approve_discussion(discussion_id)
620 with discussions_session(other_token) as api:
621 res = api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
622 assert res.title == "hello"
625def test_hidden_discussion_filtered_for_author_too(db, moderator: Moderator):
626 """Once admin hides a discussion, even the author cannot read it."""
627 author, author_token = generate_user()
629 with session_scope() as session:
630 community = create_community(session, 0, 1, "Testing Community", [author], [], None)
631 community_id = community.id
633 with discussions_session(author_token) as api:
634 res = api.CreateDiscussion(
635 discussions_pb2.CreateDiscussionReq(
636 title="bad",
637 content="c",
638 owner_community_id=community_id,
639 )
640 )
641 discussion_id = res.discussion_id
643 # Hide via moderator (visibility=hidden, not visible)
644 with real_moderation_session(moderator.token) as api:
645 state_res = api.GetModerationState(
646 moderation_pb2.GetModerationStateReq(
647 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_DISCUSSION,
648 object_id=discussion_id,
649 )
650 )
651 api.ModerateContent(
652 moderation_pb2.ModerateContentReq(
653 moderation_state_id=state_res.moderation_state.moderation_state_id,
654 action=moderation_pb2.MODERATION_ACTION_HIDE,
655 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN,
656 reason="bad content",
657 )
658 )
660 with discussions_session(author_token) as api:
661 with pytest.raises(grpc.RpcError) as e:
662 api.GetDiscussion(discussions_pb2.GetDiscussionReq(discussion_id=discussion_id))
663 assert e.value.code() == grpc.StatusCode.NOT_FOUND
666def test_list_my_communities_discussions_respects_moderation(db, moderator: Moderator):
667 """A shadowed discussion is hidden from other community members until approved."""
668 author, author_token = generate_user()
669 other, other_token = generate_user()
671 with session_scope() as session:
672 community = create_community(session, 0, 1, "Testing Community", [author, other], [], None)
673 community_id = community.id
675 with discussions_session(author_token) as api:
676 discussion_id = api.CreateDiscussion(
677 discussions_pb2.CreateDiscussionReq(
678 title="hello",
679 content="world",
680 owner_community_id=community_id,
681 )
682 ).discussion_id
684 # Author sees their own shadowed discussion
685 with discussions_session(author_token) as api:
686 res = api.ListMyCommunitiesDiscussions(discussions_pb2.ListMyCommunitiesDiscussionsReq())
687 assert [d.title for d in res.discussions] == ["hello"]
689 # Other members do not see the shadowed discussion
690 with discussions_session(other_token) as api:
691 res = api.ListMyCommunitiesDiscussions(discussions_pb2.ListMyCommunitiesDiscussionsReq())
692 assert [d.title for d in res.discussions] == []
694 moderator.approve_discussion(discussion_id)
696 # Once approved, it shows up for other members too
697 with discussions_session(other_token) as api:
698 res = api.ListMyCommunitiesDiscussions(discussions_pb2.ListMyCommunitiesDiscussionsReq())
699 assert [d.title for d in res.discussions] == ["hello"]
702def test_update_discussion_creates_version_record(db):
703 user, token = generate_user()
704 with session_scope() as session:
705 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
706 community_id = community.id
708 with discussions_session(token) as api:
709 res = api.CreateDiscussion(
710 discussions_pb2.CreateDiscussionReq(
711 title="Original title",
712 content="Original content",
713 owner_community_id=community_id,
714 )
715 )
716 discussion_id = res.discussion_id
718 with discussions_session(token) as api:
719 api.UpdateDiscussion(
720 discussions_pb2.UpdateDiscussionReq(
721 discussion_id=discussion_id,
722 title=StringValue(value="Updated title"),
723 content=StringValue(value="Updated content"),
724 )
725 )
727 with session_scope() as session:
728 versions = (
729 session.execute(select(DiscussionVersion).where(DiscussionVersion.discussion_id == discussion_id))
730 .scalars()
731 .all()
732 )
733 assert len(versions) == 1
734 v = versions[0]
735 assert v.change_type == ContentChangeType.edit
736 assert v.old_title == "Original title"
737 assert v.new_title == "Updated title"
738 assert v.old_content == "Original content"
739 assert v.new_content == "Updated content"
740 assert v.editor_user_id == user.id
743def test_update_discussion_multiple_edits_creates_multiple_version_records(db):
744 user, token = generate_user()
745 with session_scope() as session:
746 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
747 community_id = community.id
749 with discussions_session(token) as api:
750 discussion_id = api.CreateDiscussion(
751 discussions_pb2.CreateDiscussionReq(
752 title="v1 title",
753 content="v1 content",
754 owner_community_id=community_id,
755 )
756 ).discussion_id
758 with discussions_session(token) as api:
759 api.UpdateDiscussion(
760 discussions_pb2.UpdateDiscussionReq(
761 discussion_id=discussion_id,
762 title=StringValue(value="v2 title"),
763 content=StringValue(value="v2 content"),
764 )
765 )
767 with discussions_session(token) as api:
768 api.UpdateDiscussion(
769 discussions_pb2.UpdateDiscussionReq(
770 discussion_id=discussion_id,
771 title=StringValue(value="v3 title"),
772 content=StringValue(value="v3 content"),
773 )
774 )
776 with session_scope() as session:
777 versions = (
778 session.execute(
779 select(DiscussionVersion)
780 .where(DiscussionVersion.discussion_id == discussion_id)
781 .order_by(DiscussionVersion.id)
782 )
783 .scalars()
784 .all()
785 )
786 assert len(versions) == 2
787 assert versions[0].old_title == "v1 title"
788 assert versions[0].new_title == "v2 title"
789 assert versions[1].old_title == "v2 title"
790 assert versions[1].new_title == "v3 title"
793def test_delete_discussion_creates_version_record(db):
794 user, token = generate_user()
795 with session_scope() as session:
796 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
797 community_id = community.id
799 with discussions_session(token) as api:
800 discussion_id = api.CreateDiscussion(
801 discussions_pb2.CreateDiscussionReq(
802 title="To be deleted",
803 content="Some content",
804 owner_community_id=community_id,
805 )
806 ).discussion_id
807 api.DeleteDiscussion(discussions_pb2.DeleteDiscussionReq(discussion_id=discussion_id))
809 with session_scope() as session:
810 versions = (
811 session.execute(select(DiscussionVersion).where(DiscussionVersion.discussion_id == discussion_id))
812 .scalars()
813 .all()
814 )
815 assert len(versions) == 1
816 v = versions[0]
817 assert v.change_type == ContentChangeType.delete
818 assert v.old_title == "To be deleted"
819 assert v.new_title is None
820 assert v.old_content == "Some content"
821 assert v.new_content is None
822 assert v.editor_user_id == user.id
825def test_admin_delete_discussion_creates_version_record(db):
826 user, token = generate_user()
827 admin, admin_token = generate_user(is_superuser=True)
828 with session_scope() as session:
829 community = create_community(session, 0, 1, "Testing Community", [user], [], None)
830 community_id = community.id
832 with discussions_session(token) as api:
833 discussion_id = api.CreateDiscussion(
834 discussions_pb2.CreateDiscussionReq(
835 title="Admin will delete this",
836 content="Some content",
837 owner_community_id=community_id,
838 )
839 ).discussion_id
841 with real_admin_session(admin_token) as api:
842 api.DeleteDiscussion(admin_pb2.AdminDeleteDiscussionReq(discussion_id=discussion_id))
844 with session_scope() as session:
845 versions = (
846 session.execute(select(DiscussionVersion).where(DiscussionVersion.discussion_id == discussion_id))
847 .scalars()
848 .all()
849 )
850 assert len(versions) == 1
851 v = versions[0]
852 assert v.change_type == ContentChangeType.delete
853 assert v.old_title == "Admin will delete this"
854 assert v.new_title is None
855 assert v.editor_user_id == admin.id
858def test_create_discussion_incomplete_profile(db):
859 user, token = generate_user(complete_profile=False)
860 user2, token2 = generate_user()
862 with session_scope() as session:
863 community_id = create_community(session, 0, 1, "Testing Community", [user2], [], None).id
865 with discussions_session(token) as api:
866 with pytest.raises(grpc.RpcError) as e:
867 api.CreateDiscussion(
868 discussions_pb2.CreateDiscussionReq(
869 title="dummy title",
870 content="dummy content",
871 owner_community_id=community_id,
872 )
873 )
874 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
875 assert e.value.details() == "You have to complete your profile before you can start a discussion."