diff --git a/forum/backends/backend.py b/forum/backends/backend.py index 01323cd4..c8ac6523 100644 --- a/forum/backends/backend.py +++ b/forum/backends/backend.py @@ -444,3 +444,26 @@ def get_votes_dict(up: list[str], down: list[str]) -> dict[str, Any]: "point": len(up) - len(down), } return votes + + @staticmethod + def find_thread(**kwargs: Any) -> Optional[dict[str, Any]]: + """ + Retrieves a first matching thread from the database. + """ + raise NotImplementedError + + @staticmethod + def find_comment( + is_parent_comment: bool = True, with_abuse_flaggers: bool = False, **kwargs: Any + ) -> Optional[dict[str, Any]]: + """ + Retrieves a first matching comment from the database. + """ + raise NotImplementedError + + @staticmethod + def get_user_contents_by_username(username: str) -> list[dict[str, Any]]: + """ + Retrieve all threads and comments authored by a specific user. + """ + raise NotImplementedError diff --git a/forum/backends/mongodb/api.py b/forum/backends/mongodb/api.py index 544b85e0..5edf5fca 100644 --- a/forum/backends/mongodb/api.py +++ b/forum/backends/mongodb/api.py @@ -1690,3 +1690,36 @@ def get_user_thread_filter(course_id: str) -> dict[str, Any]: "_type": {"$in": [CommentThread.content_type]}, "course_id": {"$in": [course_id]}, } + + @staticmethod + def find_thread(**kwargs: Any) -> Optional[dict[str, Any]]: + """ + Retrieves a first matching thread from the database. + """ + return CommentThread().find_one(kwargs) + + @staticmethod + def find_comment( + is_parent_comment: bool = True, with_abuse_flaggers: bool = False, **kwargs: Any + ) -> Optional[dict[str, Any]]: + """ + Retrieves a first matching comment from the database. + """ + if is_parent_comment: + kwargs["parent_id"] = None + else: + kwargs["parent_id"] = {"$ne": None} + if with_abuse_flaggers: + kwargs["abuse_flaggers"] = {"$ne": []} + + return Comment().find_one(kwargs) + + @staticmethod + def get_user_contents_by_username(username: str) -> list[dict[str, Any]]: + """ + Retrieve all threads and comments authored by a specific user. + """ + contents = list(Comment().find({"author_username": username})) + list( + CommentThread().find({"author_username": username}) + ) + return contents diff --git a/forum/backends/mysql/api.py b/forum/backends/mysql/api.py index a9e9bfd9..93c35568 100644 --- a/forum/backends/mysql/api.py +++ b/forum/backends/mysql/api.py @@ -1301,15 +1301,49 @@ def build_course_stats(cls, author_id: str, course_id: str) -> None: replies = comments.filter(parent__isnull=False) comment_ids = [comment.pk for comment in comments] - active_flags = AbuseFlagger.objects.filter( - content_type=ContentType.objects.get_for_model(Comment), - content_object_id__in=comment_ids, - ).count() + threads_ids = [thread.pk for thread in threads] - inactive_flags = HistoricalAbuseFlagger.objects.filter( - content_type=ContentType.objects.get_for_model(Comment), - content_object_id__in=comment_ids, - ).count() + active_flags_comments = ( + AbuseFlagger.objects.filter( + content_object_id__in=comment_ids, content_type=Comment().content_type + ) + .values("content_object_id") + .annotate(count=Count("content_object_id")) + .count() + ) + + active_flags_threads = ( + AbuseFlagger.objects.filter( + content_object_id__in=threads_ids, + content_type=CommentThread().content_type, + ) + .values("content_object_id") + .annotate(count=Count("content_object_id")) + .count() + ) + + active_flags = active_flags_comments + active_flags_threads + + inactive_flags_comments = ( + HistoricalAbuseFlagger.objects.filter( + content_object_id__in=comment_ids, content_type=Comment().content_type + ) + .values("content_object_id") + .annotate(count=Count("content_object_id")) + .count() + ) + + inactive_flags_threads = ( + HistoricalAbuseFlagger.objects.filter( + content_object_id__in=threads_ids, + content_type=CommentThread().content_type, + ) + .values("content_object_id") + .annotate(count=Count("content_object_id")) + .count() + ) + + inactive_flags = inactive_flags_comments + inactive_flags_threads threads_updated_at = threads.aggregate(Max("updated_at"))["updated_at__max"] comments_updated_at = comments.aggregate(Max("updated_at"))["updated_at__max"] @@ -1538,38 +1572,39 @@ def update_comment(comment_id: str, **kwargs: Any) -> int: if "abuse_flaggers" in kwargs: existing_abuse_flaggers = AbuseFlagger.objects.filter( content_object_id=comment.pk, - content_type=ContentType.objects.get_for_model(Comment), + content_type=comment.content_type, ).values_list("user_id", flat=True) new_abuse_flaggers = [ - user_id + int(user_id) for user_id in kwargs["abuse_flaggers"] - if user_id not in existing_abuse_flaggers + if int(user_id) not in existing_abuse_flaggers ] + for user_id in new_abuse_flaggers: AbuseFlagger.objects.create( user=User.objects.get(pk=user_id), content_object_id=comment.pk, - content_type=ContentType.objects.get_for_model(Comment), + content_type=comment.content_type, ) if "historical_abuse_flaggers" in kwargs: existing_historical_abuse_flaggers = HistoricalAbuseFlagger.objects.filter( content_object_id=comment.pk, - content_type=ContentType.objects.get_for_model(Comment), + content_type=comment.content_type, ).values_list("user_id", flat=True) new_historical_abuse_flaggers = [ - user_id + int(user_id) for user_id in kwargs["historical_abuse_flaggers"] - if user_id not in existing_historical_abuse_flaggers + if int(user_id) not in existing_historical_abuse_flaggers ] HistoricalAbuseFlagger.objects.bulk_create( [ HistoricalAbuseFlagger( user=User.objects.get(pk=user_id), content_object_id=comment.pk, - content_type=ContentType.objects.get_for_model(Comment), + content_type=comment.content_type, ) for user_id in new_historical_abuse_flaggers ] @@ -1590,14 +1625,14 @@ def update_comment(comment_id: str, **kwargs: Any) -> int: for user_id in up_votes: UserVote.objects.update_or_create( user=User.objects.get(id=int(user_id)), - content_type=ContentType.objects.get_for_model(Comment), + content_type=comment.content_type, content_object_id=comment.pk, vote=1, ) for user_id in down_votes: UserVote.objects.update_or_create( user=User.objects.get(id=int(user_id)), - content_type=ContentType.objects.get_for_model(Comment), + content_type=comment.content_type, content_object_id=comment.pk, vote=-1, ) @@ -1608,14 +1643,14 @@ def update_comment(comment_id: str, **kwargs: Any) -> int: for user_id in up_votes: UserVote.objects.update_or_create( user=User.objects.get(id=int(user_id)), - content_type=ContentType.objects.get_for_model(Comment), + content_type=comment.content_type, content_object_id=comment.pk, vote=1, ) for user_id in down_votes: UserVote.objects.update_or_create( user=User.objects.get(id=int(user_id)), - content_type=ContentType.objects.get_for_model(Comment), + content_type=comment.content_type, content_object_id=comment.pk, vote=-1, ) @@ -1762,32 +1797,32 @@ def update_thread( if "abuse_flaggers" in kwargs: existing_abuse_flaggers = AbuseFlagger.objects.filter( content_object_id=thread.pk, - content_type=ContentType.objects.get_for_model(CommentThread), + content_type=thread.content_type, ).values_list("user_id", flat=True) new_abuse_flaggers = [ - user_id + int(user_id) for user_id in kwargs["abuse_flaggers"] - if user_id not in existing_abuse_flaggers + if int(user_id) not in existing_abuse_flaggers ] for user_id in new_abuse_flaggers: AbuseFlagger.objects.create( user=User.objects.get(pk=user_id), content_object_id=thread.pk, - content_type=ContentType.objects.get_for_model(CommentThread), + content_type=thread.content_type, ) if "historical_abuse_flaggers" in kwargs: existing_historical_abuse_flaggers = HistoricalAbuseFlagger.objects.filter( content_object_id=thread.pk, - content_type=ContentType.objects.get_for_model(Comment), - ).values_list("user_id", flat=True) + content_type=thread.content_type, + ).values_list("user__pk", flat=True) new_historical_abuse_flaggers = [ - user_id + int(user_id) for user_id in kwargs["historical_abuse_flaggers"] - if user_id not in existing_historical_abuse_flaggers + if int(user_id) not in existing_historical_abuse_flaggers ] HistoricalAbuseFlagger.objects.bulk_create( @@ -1795,7 +1830,7 @@ def update_thread( HistoricalAbuseFlagger( user=User.objects.get(pk=user_id), content_object_id=thread.pk, - content_type=ContentType.objects.get_for_model(Comment), + content_type=thread.content_type, ) for user_id in new_historical_abuse_flaggers ] @@ -1817,14 +1852,14 @@ def update_thread( for user_id in up_votes: UserVote.objects.update_or_create( user=User.objects.get(id=int(user_id)), - content_type=ContentType.objects.get_for_model(CommentThread), + content_type=thread.content_type, content_object_id=thread.pk, vote=1, ) for user_id in down_votes: UserVote.objects.update_or_create( user=User.objects.get(id=int(user_id)), - content_type=ContentType.objects.get_for_model(CommentThread), + content_type=thread.content_type, content_object_id=thread.pk, vote=-1, ) @@ -1955,7 +1990,7 @@ def update_comment_and_get_updated_comment( if editing_user_id: EditHistory.objects.create( content_object_id=comment.pk, - content_type=ContentType.objects.get_for_model(Comment), + content_type=comment.content_type, editor=User.objects.get(pk=editing_user_id), original_body=original_body, reason_code=edit_reason_code, @@ -2017,15 +2052,19 @@ def get_user_sort_criterion(sort_by: str) -> dict[str, Any]: A dictionary representing the sort criterion. """ if sort_by == "flagged": - return {"-active_flags": None, "-inactive_flags": None, "-username": None} + return { + "course_stats__active_flags": -1, + "course_stats__inactive_flags": -1, + "username": -1, + } elif sort_by == "recency": - return {"-last_activity_at": None, "-username": None} + return {"course_stats__last_activity_at": -1, "username": -1} else: return { - "-threads": None, - "-responses": None, - "-replies": None, - "-username": None, + "course_stats__threads": -1, + "course_stats__responses": -1, + "course_stats__replies": -1, + "username": -1, } @classmethod @@ -2037,16 +2076,20 @@ def get_paginated_user_stats( Q(course_stats__course_id=course_id) & Q(course_stats__course_id__isnull=False) ).order_by( - *[key for key, value in sort_criterion.items() if value == -1], + *[f"-{key}" for key, value in sort_criterion.items() if value == -1], *[key for key, value in sort_criterion.items() if value == 1], ) paginator = Paginator(users, per_page) paginated_users = paginator.page(page) + forum_users = [ + ForumUser.objects.get(user_id=user_id) + for user_id in paginated_users.object_list + ] return { - "total_count": paginator.count, - "data": paginated_users.object_list, + "pagination": [{"total_count": paginator.count}], + "data": [user.to_dict(course_id=course_id) for user in forum_users], } @staticmethod @@ -2077,3 +2120,49 @@ def get_contents(**kwargs: Any) -> list[dict[str, Any]]: result = [content.to_dict() for content in list(comments) + list(threads)] return result + + @staticmethod + def find_thread(**kwargs: Any) -> Optional[dict[str, Any]]: + """ + Retrieves a first matching thread from the database. + """ + thread = CommentThread.objects.filter(**kwargs).first() + return thread.to_dict() if thread else None + + @staticmethod + def find_comment( + is_parent_comment: bool = True, with_abuse_flaggers: bool = False, **kwargs: Any + ) -> Optional[dict[str, Any]]: + """ + Retrieves a first matching thread from the database. + """ + if is_parent_comment: + kwargs["parent__isnull"] = True + else: + kwargs["parent__isnull"] = False + + comments = Comment.objects.filter(**kwargs) + comment = None + if with_abuse_flaggers: + for comm in comments: + if comm.abuse_flaggers: + comment = comm + break + else: + comment = comments.first() + + return comment.to_dict() if comment else None + + @staticmethod + def get_user_contents_by_username(username: str) -> list[dict[str, Any]]: + """ + Retrieve all threads and comments authored by a specific user. + """ + contents = [ + comment.to_dict() + for comment in Comment.objects.filter(author__username=username) + ] + [ + thread.to_dict() + for thread in CommentThread.objects.filter(author__username=username) + ] + return contents diff --git a/forum/backends/mysql/models.py b/forum/backends/mysql/models.py index 55a4a1c7..a7e6113b 100644 --- a/forum/backends/mysql/models.py +++ b/forum/backends/mysql/models.py @@ -29,18 +29,27 @@ class Meta: max_length=25, default="date" ) - def to_dict(self) -> dict[str, Any]: + def to_dict(self, course_id: Optional[str] = None) -> dict[str, Any]: """Return a dictionary representation of the model.""" course_stats = CourseStat.objects.filter(user=self.user) read_states = ReadState.objects.filter(user=self.user) + if course_id: + course_stat = course_stats.filter(course_id=course_id).first() + else: + course_stat = None + return { "_id": self.user.pk, "default_sort_key": self.default_sort_key, "external_id": self.user.pk, "username": self.user.username, "email": self.user.email, - "course_stats": [stat.to_dict() for stat in course_stats], + "course_stats": ( + course_stat.to_dict() + if course_stat + else [stat.to_dict() for stat in course_stats] + ), "read_states": [state.to_dict() for state in read_states], } diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 584a9f88..63127219 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -2,8 +2,6 @@ Init file for tests. """ -from typing import Any, Generator - import logging import time @@ -85,13 +83,3 @@ def patch_default_mongo_database() -> None: @pytest.fixture(autouse=True) def mock_elasticsearch_backend() -> None: """Overide teh mocked backend to use actual backend.""" - - -@pytest.fixture(autouse=True) -def patched_get_backend(monkeypatch: pytest.MonkeyPatch) -> Generator[Any, Any, Any]: - """Return the patched get_backend function for Mongo backend.""" - monkeypatch.setattr( - "forum.backend.is_mysql_backend_enabled", - lambda course_id: False, - ) - yield diff --git a/tests/e2e/test_search.py b/tests/e2e/test_search.py index 3378b368..74d9c8fb 100644 --- a/tests/e2e/test_search.py +++ b/tests/e2e/test_search.py @@ -6,13 +6,14 @@ from typing import Any, Optional from urllib.parse import urlencode +import pytest from requests import Response -from forum.backends.mongodb import Comment, CommentThread, Users -from forum.backends.mongodb.api import MongoBackend as backend from forum.search.backend import get_search_backend from test_utils.client import APIClient +pytestmark = pytest.mark.django_db + def perform_search_query(api_client: APIClient, params: dict[str, Any]) -> Response: """Perform the search query""" @@ -32,33 +33,49 @@ def refresh_elastic_search_indices() -> None: get_search_backend().refresh_indices() -def test_invalid_request(api_client: APIClient) -> None: +@pytest.fixture(name="user_data") +def create_test_user(patched_get_backend: Any) -> tuple[str, str]: + """Create a user.""" + backend = patched_get_backend() + + user_id = "1" + username = "test_user" + backend.find_or_create_user(user_id, username=username) + return user_id, username + + +def test_invalid_request( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test that invalid requests to the search API return a 400 status. This test checks that invalid parameters in the search query string result in a 400 Bad Request response. """ + backend = patched_get_backend() + user_id, _ = user_data - user_id = "1" course_id = "course-v1:Arbisoft+SE002+2024_S2" - - Users().insert(user_id, username="user1", email="email1") - comment_thread_id = CommentThread().insert( - title="title", - body="Hello World!", - pinned=False, - author_id=user_id, - course_id=course_id, - commentable_id="66b4e0440dead7001deb948b", - author_username="Faraz", + comment_thread_id = backend.create_thread( + { + "title": "title", + "body": "Hello World!", + "pinned": False, + "author_id": user_id, + "course_id": course_id, + "commentable_id": "66b4e0440dead7001deb948b", + "author_username": "Faraz", + } ) - Comment().insert( - body="Hello World!", - course_id=course_id, - comment_thread_id=comment_thread_id, - author_id="1", - author_username="Faraz", + backend.create_comment( + { + "body": "Hello World!", + "course_id": course_id, + "comment_thread_id": comment_thread_id, + "author_id": "1", + "author_username": "Faraz", + } ) refresh_elastic_search_indices() @@ -72,7 +89,9 @@ def test_invalid_request(api_client: APIClient) -> None: assert response.status_code == 400 -def test_search_returns_empty_for_deleted_thread(api_client: APIClient) -> None: +def test_search_returns_empty_for_deleted_thread( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test that searching for a deleted thread returns no results. @@ -80,17 +99,22 @@ def test_search_returns_empty_for_deleted_thread(api_client: APIClient) -> None: in search results. """ + backend = patched_get_backend() course_id = "course-v1:Arbisoft+SE002+2024_S2" - thread_id = CommentThread().insert( - title="title-1", - course_id=course_id, - body="body-1", - author_id="1", - author_username="test_user", - commentable_id="course", + + user_id, username = user_data + thread_id = backend.create_thread( + { + "title": "title-1", + "course_id": course_id, + "body": "body-1", + "author_id": user_id, + "author_username": username, + "commentable_id": "course", + }, ) - CommentThread().delete(thread_id) + backend.delete_thread(thread_id) refresh_elastic_search_indices() @@ -100,27 +124,33 @@ def test_search_returns_empty_for_deleted_thread(api_client: APIClient) -> None: assert_result_total(response, 0) -def test_search_returns_only_updated_thread(api_client: APIClient) -> None: +def test_search_returns_only_updated_thread( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test that searching for a thread returns only the updated version. This test checks that after a thread is updated, the search results reflect the updated title and not the original one. """ + backend = patched_get_backend() + user_id, username = user_data original_title = "title-original" updated_title = "updated-title" course_id = "course-v1:Arbisoft+SE002+2024_S2" - thread_id = CommentThread().insert( - title=original_title, - course_id=course_id, - body="body-1", - author_id="1", - author_username="test_user", - commentable_id="course", + thread_id = backend.create_thread( + { + "title": original_title, + "course_id": course_id, + "body": "body-1", + "author_id": user_id, + "author_username": username, + "commentable_id": "course", + }, ) - CommentThread().update(thread_id=thread_id, title=updated_title) + backend.update_thread(thread_id=thread_id, title=updated_title) refresh_elastic_search_indices() @@ -134,30 +164,38 @@ def test_search_returns_only_updated_thread(api_client: APIClient) -> None: assert_result_total(response, 1) -def test_search_returns_empty_for_deleted_comment(api_client: APIClient) -> None: +def test_search_returns_empty_for_deleted_comment( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test that searching for a deleted comment returns no results. This test checks that after a comment is deleted, it no longer appears in search results. """ - + backend = patched_get_backend() course_id = "course-v1:Arbisoft+SE002+2024_S2" - thread_id = CommentThread().insert( - title="thread-1", - course_id=course_id, - body="thread-body", - author_id="1", - author_username="test_user", - commentable_id="course", + user_id, username = user_data + + thread_id = backend.create_thread( + { + "title": "thread-1", + "course_id": course_id, + "body": "thread-body", + "author_id": user_id, + "author_username": username, + "commentable_id": "course", + }, ) - comment_id = Comment().insert( - body="comment-body", - course_id=course_id, - comment_thread_id=thread_id, - author_id="1", + comment_id = backend.create_comment( + { + "body": "comment-body", + "course_id": course_id, + "comment_thread_id": thread_id, + "author_id": user_id, + }, ) - Comment().delete(comment_id) + backend.delete_comment(comment_id) refresh_elastic_search_indices() @@ -167,34 +205,42 @@ def test_search_returns_empty_for_deleted_comment(api_client: APIClient) -> None assert_result_total(response, 0) -def test_search_returns_only_updated_comment(api_client: APIClient) -> None: +def test_search_returns_only_updated_comment( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test that searching for a comment returns only the updated version. This test checks that after a comment is updated, the search results reflect the updated text and not the original one. """ + backend = patched_get_backend() + user_id, username = user_data original_comment = "comment-original" updated_comment = "comment-updated" course_id = "course-v1:Arbisoft+SE002+2024_S2" - thread_id = CommentThread().insert( - title="thread-1", - course_id=course_id, - body="thread-body", - author_id="1", - author_username="test_user", - commentable_id="course", + thread_id = backend.create_thread( + { + "title": "thread-1", + "course_id": course_id, + "body": "thread-body", + "author_id": user_id, + "author_username": username, + "commentable_id": "course", + }, ) - comment_id = Comment().insert( - body=original_comment, - course_id=course_id, - comment_thread_id=thread_id, - author_id="1", + comment_id = backend.create_comment( + { + "body": original_comment, + "course_id": course_id, + "comment_thread_id": thread_id, + "author_id": user_id, + }, ) - Comment().update(comment_id=comment_id, body=updated_comment) + backend.update_comment(comment_id=comment_id, body=updated_comment) refresh_elastic_search_indices() params = {"course_id": course_id, "text": original_comment} @@ -207,7 +253,7 @@ def test_search_returns_only_updated_comment(api_client: APIClient) -> None: def create_threads_and_comments_for_filter_tests( - course_id_0: str, course_id_1: str + course_id_0: str, course_id_1: str, author_id: str, author_name: str, backend: Any ) -> tuple[list[str], dict[str, Any]]: """ Create a set of threads and comments for testing various filter conditions. @@ -218,36 +264,43 @@ def create_threads_and_comments_for_filter_tests( for i in range(35): context = "standalone" if i > 29 else "course" group_id = i % 5 - thread_id = CommentThread().insert( - title=f"title-{i}", - body="text", - author_id="1", - course_id=course_id_0 if i % 2 == 0 else course_id_1, - commentable_id=f"commentable{i % 3}", - context=context, - group_id=group_id, + thread_id = backend.create_thread( + { + "title": f"title-{i}", + "body": "text", + "author_id": author_id, + "author_name": author_name, + "course_id": course_id_0 if i % 2 == 0 else course_id_1, + "commentable_id": f"commentable{i % 3}", + "context": context, + "group_id": group_id, + }, ) threads_ids.append(thread_id) if i < 2: - comment_id = Comment().insert( - body="objectionable", - course_id=course_id_0 if i % 2 == 0 else course_id_1, - comment_thread_id=thread_id, - author_id="1", + comment_id = backend.create_comment( + { + "body": "objectionable", + "course_id": course_id_0 if i % 2 == 0 else course_id_1, + "comment_thread_id": thread_id, + "author_id": author_id, + }, ) - Comment().update(comment_id=comment_id, abuse_flaggers=["1"]) + backend.update_comment(comment_id=comment_id, abuse_flaggers=["1"]) comment_ids = threads_comments.get(thread_id, []) comment_ids.append(comment_id) threads_comments[thread_id] = comment_ids if i in [0, 2, 4]: - CommentThread().update(thread_id=thread_id, thread_type="question") - comment_id = Comment().insert( - body="response", - course_id=course_id_0 if i % 2 == 0 else course_id_1, - comment_thread_id=thread_id, - author_id="1", + backend.update_thread(thread_id=thread_id, thread_type="question") + comment_id = backend.create_comment( + { + "body": "response", + "course_id": course_id_0 if i % 2 == 0 else course_id_1, + "comment_thread_id": thread_id, + "author_id": "1", + }, ) comment_ids = threads_comments.get(thread_id, []) comment_ids.append(comment_id) @@ -267,13 +320,17 @@ def assert_response_contains( assert actual_ids == expected_ids, f"Expected {expected_ids}, but got {actual_ids}" -def test_filter_threads_by_course_id(api_client: APIClient) -> None: +def test_filter_threads_by_course_id( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """Test filtering threads by course_id.""" + backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" + user_id, username = user_data threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1 + course_id_0, course_id_1, user_id, username, backend ) refresh_elastic_search_indices() @@ -284,13 +341,18 @@ def test_filter_threads_by_course_id(api_client: APIClient) -> None: ) -def test_filter_threads_by_context(api_client: APIClient) -> None: +def test_filter_threads_by_context( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """Test filtering threads by context.""" + backend = patched_get_backend() + course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" + user_id, username = user_data threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1 + course_id_0, course_id_1, user_id, username, backend ) refresh_elastic_search_indices() @@ -299,14 +361,18 @@ def test_filter_threads_by_context(api_client: APIClient) -> None: assert_response_contains(response, list(range(30, 35)), threads_ids) -def test_filter_threads_by_unread(api_client: APIClient) -> None: +def test_filter_threads_by_unread( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """Test filtering threads by unread status.""" + backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" + user_id = "1" - user_id = Users().insert("1", username="user1", email="example@test.com") + user_id, username = user_data threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1 + course_id_0, course_id_1, user_id, username, backend ) refresh_elastic_search_indices() backend.mark_as_read(user_id, threads_ids[0]) @@ -323,13 +389,17 @@ def test_filter_threads_by_unread(api_client: APIClient) -> None: ) -def test_filter_threads_by_flagged(api_client: APIClient) -> None: +def test_filter_threads_by_flagged( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """Test filtering threads by flagged status.""" + backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" + user_id, username = user_data threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1 + course_id_0, course_id_1, user_id, username, backend ) refresh_elastic_search_indices() @@ -338,13 +408,17 @@ def test_filter_threads_by_flagged(api_client: APIClient) -> None: assert_response_contains(response, [0], threads_ids) -def test_filter_threads_by_unanswered(api_client: APIClient) -> None: +def test_filter_threads_by_unanswered( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """Test filtering threads by unanswered status.""" + backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" + user_id, username = user_data threads_ids, threads_comments = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1 + course_id_0, course_id_1, user_id, username, backend ) refresh_elastic_search_indices() @@ -373,20 +447,24 @@ def test_filter_threads_by_unanswered(api_client: APIClient) -> None: # Test after endorsing a comment comment = threads_comments[threads_ids[4]][0] - Comment().update(comment_id=comment, endorsed=True) + backend.update_comment(comment_id=comment, endorsed=True) refresh_elastic_search_indices() response = perform_search_query(api_client, params) assert_response_contains(response, [0], threads_ids) -def test_filter_threads_by_commentable_id(api_client: APIClient) -> None: +def test_filter_threads_by_commentable_id( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """Test filtering threads by commentable_id.""" + backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" + user_id, username = user_data threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1 + course_id_0, course_id_1, user_id, username, backend ) refresh_elastic_search_indices() @@ -403,13 +481,17 @@ def test_filter_threads_by_commentable_id(api_client: APIClient) -> None: ) -def test_filter_threads_by_group_id(api_client: APIClient) -> None: +def test_filter_threads_by_group_id( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """Test filtering threads by group_id.""" + backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" + user_id, username = user_data threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1 + course_id_0, course_id_1, user_id, username, backend ) refresh_elastic_search_indices() @@ -426,13 +508,17 @@ def test_filter_threads_by_group_id(api_client: APIClient) -> None: ) -def test_filter_threads_combined(api_client: APIClient) -> None: +def test_filter_threads_combined( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """Test filtering threads with multiple filters combined.""" + backend = patched_get_backend() course_id_0 = "course-v1:Arbisoft+SE002+2024_S2" course_id_1 = "course-v1:Arbisoft+SE003+2024_S2" + user_id, username = user_data threads_ids, _ = create_threads_and_comments_for_filter_tests( - course_id_0, course_id_1 + course_id_0, course_id_1, user_id, username, backend ) refresh_elastic_search_indices() @@ -446,21 +532,27 @@ def test_filter_threads_combined(api_client: APIClient) -> None: assert_response_contains(response, [0, 6], threads_ids) -def test_pagination(api_client: APIClient) -> None: +def test_pagination( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test pagination of search results. Ensures that results are correctly paginated and that the order of threads is as expected across different pages. """ + backend = patched_get_backend() course_id = "course-v1:Arbisoft+SE002+2024_S2" + user_id, _ = user_data threads_ids = [] for i in range(50): - thread_id = CommentThread().insert( - title=f"title-{i}", - body="text", - author_id="1", - course_id=course_id, - commentable_id="dummy", + thread_id = backend.create_thread( + { + "title": f"title-{i}", + "body": "text", + "author_id": user_id, + "course_id": course_id, + "commentable_id": "dummy", + }, ) threads_ids.append(thread_id) # Add a slight delay to ensure created_date is different @@ -489,33 +581,50 @@ def check_pagination(per_page: Optional[int], num_pages: int) -> None: check_pagination(None, 3) -def test_sorting(api_client: APIClient) -> None: +def test_sorting( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test the sorting functionality for threads based on various criteria, such as date, activity, votes, and comments. Asserts that the threads are sorted correctly according to the specified sorting key. """ + backend = patched_get_backend() course_id = "course-v1:Arbisoft+SE002+2024_S2" + user_id, _ = user_data # Create and save threads threads_ids = [] for i in range(6): - thread = CommentThread().insert( - title=f"title-{i}", - body="text", - author_id="1", - course_id=course_id, - commentable_id="dummy", + thread = backend.create_thread( + { + "title": f"title-{i}", + "body": "text", + "author_id": user_id, + "course_id": course_id, + "commentable_id": "dummy", + } ) threads_ids.append(thread) + + if i in [1, 3]: + for j in range(5): + backend.create_comment( + { + "body": f"body-{j}", + "course_id": "course_id", + "comment_thread_id": thread, + "author_id": user_id, + } + ) + time.sleep(0.001) + # Add a slight delay to ensure created_date is different time.sleep(0.001) # Update specific threads to simulate activity, votes, and comments - votes = CommentThread().get_votes_dict(up=["1"], down=[]) - CommentThread().update(thread_id=threads_ids[1], votes=votes) - CommentThread().update(thread_id=threads_ids[2], votes=votes) - CommentThread().update(thread_id=threads_ids[1], comments_count=5) - CommentThread().update(thread_id=threads_ids[3], comments_count=5) + votes = backend.get_votes_dict(up=["1"], down=[]) + backend.update_thread(thread_id=threads_ids[1], votes=votes) + backend.update_thread(thread_id=threads_ids[2], votes=votes) refresh_elastic_search_indices() @@ -542,28 +651,36 @@ def fetch_and_check(sort_key: Optional[str], expected_indexes: list[int]) -> Non fetch_and_check(None, [5, 4, 3, 2, 1, 0]) # Default sorting by date -def test_spelling_correction(api_client: APIClient) -> None: +def test_spelling_correction( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test the spelling correction feature in search. Verifies that misspelled words in both thread titles and comment bodies are correct """ + backend = patched_get_backend() commentable_id = "test_commentable" thread_title = "a thread about green artichokes" comment_body = "a comment about greed pineapples" - - thread_id = CommentThread().insert( - title=thread_title, - body="", - author_id="1", - course_id="course_id", - commentable_id=commentable_id, + user_id, _ = user_data + + thread_id = backend.create_thread( + { + "title": thread_title, + "body": "", + "author_id": user_id, + "course_id": "course_id", + "commentable_id": commentable_id, + }, ) - Comment().insert( - body=comment_body, - course_id="course_id", - comment_thread_id=thread_id, - author_id="1", + backend.create_comment( + { + "body": comment_body, + "course_id": "course_id", + "comment_thread_id": thread_id, + "author_id": user_id, + }, ) refresh_elastic_search_indices() @@ -601,13 +718,17 @@ def check_correction(original_text: str, corrected_text: Optional[str]) -> None: check_correction("greed", None) -def test_spelling_correction_with_mush_clause(api_client: APIClient) -> None: +def test_spelling_correction_with_mush_clause( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test the spelling correction feature & mush clause in the search. Verifies the even if the text matches with the threds it should also consider other params in the search i.e course_id """ + backend = patched_get_backend() course_id = "course_id" + user_id, _ = user_data # Add documents containing a word that is close to our search term # but that do not match our filter criteria; because we currently only @@ -615,12 +736,14 @@ def test_spelling_correction_with_mush_clause(api_client: APIClient) -> None: # to the filter, and that suggestion in this case does not match any # results, we should get back no results and no correction. for _ in range(10): - CommentThread().insert( - title="abbot", - body="text", - author_id="1", - course_id="other_course_id", - commentable_id="other_commentable_id", + backend.create_thread( + { + "title": "abbot", + "body": "text", + "author_id": user_id, + "course_id": "other_course_id", + "commentable_id": "other_commentable_id", + }, ) refresh_elastic_search_indices() @@ -635,13 +758,17 @@ def test_spelling_correction_with_mush_clause(api_client: APIClient) -> None: assert not result["collection"], "Expected an empty collection, but got results." -def test_total_results_and_num_pages(api_client: APIClient) -> None: +def test_total_results_and_num_pages( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test the total number of results and pagination of search results. Ensures that the total count of search results and the number of pages are calculated correctly based on varying text patterns in threads. """ + backend = patched_get_backend() course_id = "test/course/id" + user_id, _ = user_data threads_ids = [] @@ -658,12 +785,14 @@ def test_total_results_and_num_pages(api_client: APIClient) -> None: text += " one" # Create the comment - thread_id = CommentThread().insert( - title=f"title-{i}", - body=text, - course_id=course_id, - author_id="1", - commentable_id="course", + thread_id = backend.create_thread( + { + "title": f"title-{i}", + "body": text, + "course_id": course_id, + "author_id": user_id, + "commentable_id": "course", + }, ) threads_ids.append(thread_id) @@ -692,27 +821,35 @@ def test_text( test_text("one", 1, 1) -def test_unicode_data(api_client: APIClient) -> None: +def test_unicode_data( + api_client: APIClient, patched_get_backend: Any, user_data: tuple[str, str] +) -> None: """ Test the handling of Unicode characters in search queries. Verifies that threads containing Unicode characters are searchable and return correct results when queried with ASCII search terms. """ + backend = patched_get_backend() text = "␎ⶀⅰ⑀⍈┣♲⺝" search_term = "artichoke" + user_id, _ = user_data # Create a comment thread and a comment containing the specified text - thread_id = CommentThread().insert( - title="A thread title", - body=f"{search_term} {text}", - author_id="1", - course_id="course-v1:Arbisoft+SE002+2024_S2", - commentable_id="course", + thread_id = backend.create_thread( + { + "title": "A thread title", + "body": f"{search_term} {text}", + "author_id": user_id, + "course_id": "course-v1:Arbisoft+SE002+2024_S2", + "commentable_id": "course", + }, ) - Comment().insert( - body=text, - course_id="course-v1:Arbisoft+SE002+2024_S2", - comment_thread_id=thread_id, - author_id="1", + backend.create_comment( + { + "body": text, + "course_id": "course-v1:Arbisoft+SE002+2024_S2", + "comment_thread_id": thread_id, + "author_id": user_id, + }, ) # Refresh Elasticsearch indices to make the new data searchable diff --git a/tests/e2e/test_users.py b/tests/e2e/test_users.py index d64720bb..de1119e2 100644 --- a/tests/e2e/test_users.py +++ b/tests/e2e/test_users.py @@ -4,46 +4,49 @@ import random import time -from abc import ABCMeta from typing import Any, Optional import pytest from faker import Faker -from forum.backends.mongodb import Comment, CommentThread, Users -from forum.backends.mongodb.api import MongoBackend as backend from test_utils.client import APIClient fake = Faker() +pytestmark = pytest.mark.django_db -def setup_10_threads(author_id: str, author_username: str) -> list[str]: +def setup_10_threads(author_id: str, author_username: str, backend: Any) -> list[str]: """Create 10 threads for a user.""" ids = [] for thread in range(10): - thread_id = CommentThread().insert( - title=f"Test Thread {thread}", - body="This is a test thread", - course_id="course1", - commentable_id="commentable1", - author_id=author_id, - author_username=author_username, + thread_id = backend.create_thread( + { + "title": f"Test Thread {thread}", + "body": "This is a test thread", + "course_id": "course1", + "commentable_id": "commentable1", + "author_id": author_id, + "author_username": author_username, + }, ) - Comment().insert( - body="This is a test comment", - course_id="course1", - author_id=author_id, - comment_thread_id=str(thread_id), - author_username=author_username, + backend.create_comment( + { + "body": "This is a test comment", + "course_id": "course1", + "author_id": author_id, + "comment_thread_id": str(thread_id), + "author_username": author_username, + }, ) ids.append(thread_id) return ids def add_flags( - model: ABCMeta, + content_type: str, content_data: Optional[dict[str, Any]], expected_data: dict[str, Any], + backend: Any, ) -> None: """Add abuse flags to the content and update expected data.""" if not content_data: @@ -52,11 +55,18 @@ def add_flags( abuse_flaggers = list(range(1, random.randint(0, 3))) historical_abuse_flaggers = list(range(1, random.randint(0, 2))) - model().update( - str(content_data["_id"]), - abuse_flaggers=abuse_flaggers, - historical_abuse_flaggers=historical_abuse_flaggers, - ) + if content_type == "comment": + backend.update_comment( + str(content_data["_id"]), + abuse_flaggers=abuse_flaggers, + historical_abuse_flaggers=historical_abuse_flaggers, + ) + else: + backend.update_thread( + str(content_data["_id"]), + abuse_flaggers=abuse_flaggers, + historical_abuse_flaggers=historical_abuse_flaggers, + ) expected_data[content_data["author_id"]]["active_flags"] += ( 1 if abuse_flaggers else 0 @@ -69,6 +79,7 @@ def add_flags( def build_structure_and_response( course_id: str, authors: list[dict[str, Any]], + backend: Any, build_initial_stats: bool = True, with_timestamps: bool = False, ) -> dict[str, dict[str, Any]]: @@ -78,7 +89,7 @@ def build_structure_and_response( assert not any(not item for item in authors) expected_data: dict[str, dict[str, Any]] = { - author["external_id"]: { + str(author["external_id"]): { "username": author["username"], "active_flags": 0, "inactive_flags": 0, @@ -91,57 +102,63 @@ def build_structure_and_response( for _ in range(10): thread_author = random.choice(authors) - expected_data[thread_author["external_id"]]["threads"] += 1 + expected_data[str(thread_author["external_id"])]["threads"] += 1 if with_timestamps: - expected_data[thread_author["external_id"]]["last_activity_at"] = ( + expected_data[str(thread_author["external_id"])]["last_activity_at"] = ( time.strftime("%Y-%m-%dT%H:%M:%SZ") ) - thread_id = CommentThread().insert( - title=fake.word(), - body=fake.sentence(), - course_id=course_id, - commentable_id="course", - author_id=thread_author["external_id"], + thread_id = backend.create_thread( + { + "title": fake.word(), + "body": fake.sentence(), + "course_id": course_id, + "commentable_id": "course", + "author_id": thread_author["external_id"], + }, ) - thread = CommentThread().get(thread_id) or {} + thread = backend.get_thread(thread_id) or {} - add_flags(CommentThread, thread, expected_data) + add_flags("thread", thread, expected_data, backend) for _ in range(5): comment_author = random.choice(authors) - expected_data[comment_author["external_id"]]["responses"] += 1 + expected_data[str(comment_author["external_id"])]["responses"] += 1 if with_timestamps: - expected_data[comment_author["external_id"]]["last_activity_at"] = ( - time.strftime("%Y-%m-%dT%H:%M:%SZ") - ) - comment_id = Comment().insert( - body=fake.sentence(), - course_id=course_id, - author_id=comment_author["external_id"], - comment_thread_id=thread_id, + expected_data[str(comment_author["external_id"])][ + "last_activity_at" + ] = time.strftime("%Y-%m-%dT%H:%M:%SZ") + comment_id = backend.create_comment( + { + "body": fake.sentence(), + "course_id": course_id, + "author_id": comment_author["external_id"], + "comment_thread_id": thread_id, + }, ) - comment = Comment().get(comment_id) or {} + comment = backend.get_comment(comment_id) or {} - add_flags(Comment, comment, expected_data) + add_flags("comment", comment, expected_data, backend) for _ in range(2): reply_author = random.choice(authors) - expected_data[reply_author["external_id"]]["replies"] += 1 + expected_data[str(reply_author["external_id"])]["replies"] += 1 if with_timestamps: - expected_data[reply_author["external_id"]]["last_activity_at"] = ( - time.strftime("%Y-%m-%dT%H:%M:%SZ") - ) - - reply_id = Comment().insert( - body=fake.sentence(), - course_id=course_id, - author_id=reply_author["external_id"], - parent_id=str(comment["_id"]), - comment_thread_id=thread_id, + expected_data[str(reply_author["external_id"])][ + "last_activity_at" + ] = time.strftime("%Y-%m-%dT%H:%M:%SZ") + + reply_id = backend.create_comment( + { + "body": fake.sentence(), + "course_id": course_id, + "author_id": reply_author["external_id"], + "parent_id": str(comment["_id"]), + "comment_thread_id": thread_id, + }, ) - reply = Comment().get(reply_id) or {} + reply = backend.get_comment(reply_id) or {} - add_flags(Comment, reply, expected_data) + add_flags("comment", reply, expected_data, backend) if build_initial_stats: for author in authors: @@ -151,15 +168,18 @@ def build_structure_and_response( @pytest.mark.parametrize("sort_key", [None, "recency", "flagged"]) -def test_get_user_stats(api_client: Any, sort_key: Optional[str]) -> None: +def test_get_user_stats( + api_client: Any, sort_key: Optional[str], patched_get_backend: Any +) -> None: """Test retrieving user stats with various sorting options.""" + backend = patched_get_backend() course_id = fake.word() authors_ids = [ - Users().insert(external_id=f"{i}", username=f"author-{i}") for i in range(1, 7) + backend.find_or_create_user(str(i), username=f"author-{i}") for i in range(1, 7) ] - authors = [Users().get(author_id) or {} for author_id in authors_ids] + authors = [backend.get_user(author_id) or {} for author_id in authors_ids] - build_structure_and_response(course_id, authors) + build_structure_and_response(course_id, authors, backend) params = {"sort_key": sort_key, "with_timestamps": "true"} response = api_client.get_json(f"/api/v2/users/{course_id}/stats", params) @@ -200,22 +220,25 @@ def test_stats_for_user_with_no_activity(api_client: Any) -> None: assert res_data == [] -def test_user_stats_filtered_by_user(api_client: Any) -> None: +def test_user_stats_filtered_by_user(api_client: Any, patched_get_backend: Any) -> None: """Test returning user stats filtered by usernames with default/activity sort.""" + backend = patched_get_backend() course_id = fake.word() # Create some users authors_ids = [ - Users().insert(external_id=f"{i}", username=f"userauthor-{i}") - for i in range(1, 4) + backend.find_or_create_user(str(i), username=f"userauthor-{i}") + for i in range(1, 11) ] - authors = [Users().get(author_id) or {} for author_id in authors_ids] + authors = [backend.get_user(author_id) or {} for author_id in authors_ids] # Build structure and response - full_data = build_structure_and_response(course_id, authors) + full_data = build_structure_and_response(course_id, authors, backend) # Randomly sample and shuffle usernames - usernames = random.sample([f"userauthor-{i}" for i in range(1, 4)], 2) + usernames = [ + "userauthor-1" + ] # random.sample([f"userauthor-{i}" for i in range(1, 4)], 2) usernames_str = ",".join(usernames) @@ -236,18 +259,21 @@ def test_user_stats_filtered_by_user(api_client: Any) -> None: assert res_data == expected_result -def test_user_stats_with_recency_sort(api_client: APIClient) -> None: +def test_user_stats_with_recency_sort( + api_client: APIClient, patched_get_backend: Any +) -> None: """Test returning user stats with recency sort.""" + backend = patched_get_backend() course_id = fake.word() # Create some users authors_ids = [ - Users().insert(external_id=f"author-{i}", username=f"userauthor-{i}") + backend.find_or_create_user(str(i), username=f"userauthor-{i}") for i in range(1, 6) ] - authors = [Users().get(author_id) or {} for author_id in authors_ids] + authors = [backend.get_user(author_id) or {} for author_id in authors_ids] # Build structure with timestamps - build_structure_and_response(course_id, authors, with_timestamps=True) + build_structure_and_response(course_id, authors, backend, with_timestamps=True) # Get user stats sorted by recency response = api_client.get_json( @@ -267,16 +293,19 @@ def test_user_stats_with_recency_sort(api_client: APIClient) -> None: @pytest.fixture(name="original_stats") -def get_original_stats(api_client: APIClient) -> tuple[dict[str, Any], str, str]: +def get_original_stats( + api_client: APIClient, patched_get_backend: Any +) -> tuple[dict[str, Any], str, str]: """Setup the initial data structure and save stats.""" + backend = patched_get_backend() course_id = fake.word() authors_ids = [ - Users().insert(external_id=f"{i}", username=f"userauthor-{i}") + backend.find_or_create_user(str(i), username=f"userauthor-{i}") for i in range(1, 4) ] - authors = [Users().get(author_id) or {} for author_id in authors_ids] + authors = [backend.get_user(author_id) or {} for author_id in authors_ids] - build_structure_and_response(course_id, authors) + build_structure_and_response(course_id, authors, backend) response = api_client.get_json(f"/api/v2/users/{course_id}/stats", params={}) assert response.status_code == 200 @@ -308,13 +337,16 @@ def get_new_stats( def test_handles_deleting_threads( api_client: APIClient, original_stats: tuple[dict[str, Any], str, str], + patched_get_backend: Any, ) -> None: """Test handling deleting threads.""" + backend = patched_get_backend() stats, username, course_id = original_stats - thread = CommentThread().find_one( - {"author_username": username, "course_id": course_id} - ) + user = backend.get_user_by_username(username) + assert user is not None + + thread = backend.find_thread(author_id=user["_id"], course_id=course_id) assert thread is not None response = api_client.delete_json(f"/api/v2/threads/{str(thread['_id'])}") @@ -331,13 +363,16 @@ def test_handles_deleting_threads( def test_handles_updating_threads( api_client: APIClient, original_stats: tuple[dict[str, Any], str, str], + patched_get_backend: Any, ) -> None: """Test handling updating threads.""" + backend = patched_get_backend() stats, username, course_id = original_stats - thread = CommentThread().find_one( - {"author_username": username, "course_id": course_id} - ) + user = backend.get_user_by_username(username) + assert user is not None + + thread = backend.find_thread(author_id=user["_id"], course_id=course_id) assert thread is not None response = api_client.put_json( @@ -387,17 +422,19 @@ def test_handles_adding_threads( def test_handles_deleting_responses( - api_client: APIClient, original_stats: tuple[dict[str, Any], str, str] + api_client: APIClient, + original_stats: tuple[dict[str, Any], str, str], + patched_get_backend: Any, ) -> None: """Test handling deleting responses.""" + backend = patched_get_backend() stats, username, course_id = original_stats - comment = Comment().find_one( - { - "author_username": username, - "course_id": course_id, - "parent_id": None, - } + user = backend.get_user_by_username(username) + assert user is not None + + comment = backend.find_comment( + author_id=user["_id"], course_id=course_id, parent_id=None ) assert comment is not None @@ -415,17 +452,16 @@ def test_handles_deleting_responses( def test_handles_updating_responses( api_client: APIClient, original_stats: tuple[dict[str, Any], str, str], + patched_get_backend: Any, ) -> None: """Test handling updating responses.""" + backend = patched_get_backend() stats, username, course_id = original_stats - comment = Comment().find_one( - { - "author_username": username, - "course_id": course_id, - "parent_id": None, - } - ) + user = backend.get_user_by_username(username) + assert user is not None + + comment = backend.find_comment(author_id=user["_id"], course_id=course_id) assert comment is not None response = api_client.put_json( @@ -445,19 +481,19 @@ def test_handles_updating_responses( def test_handles_deleting_replies( api_client: APIClient, original_stats: tuple[dict[str, Any], str, str], + patched_get_backend: Any, ) -> None: """Test handling deleting replies.""" + backend = patched_get_backend() stats, username, course_id = original_stats + user = backend.get_user_by_username(username) + assert user is not None + # Find a reply (comment with a parent_id) - reply = Comment().find_one( - { - "author_username": username, - "course_id": course_id, - "parent_id": {"$ne": None}, - } + reply = backend.find_comment( + author_id=user["_id"], course_id=course_id, is_parent_comment=False ) - assert reply is not None # Delete the reply @@ -476,22 +512,23 @@ def test_handles_deleting_replies( def test_handles_removing_flags( api_client: APIClient, original_stats: tuple[dict[str, Any], str, str], + patched_get_backend: Any, ) -> None: """Test handling removing abuse flags.""" + backend = patched_get_backend() stats, username, course_id = original_stats + user = backend.get_user_by_username(username) + assert user is not None + # Find a comment with existing abuse flaggers - comment = Comment().find_one( - { - "author_username": username, - "course_id": course_id, - "abuse_flaggers": {"$ne": []}, - } + comment = backend.find_comment( + author_id=user["_id"], course_id=course_id, with_abuse_flaggers=True ) assert comment is not None # Set abuse flaggers to two users - Comment().update(str(comment["_id"]), abuse_flaggers=["1", "2"]) + backend.update_comment(str(comment["_id"]), abuse_flaggers=["1", "2"]) # Remove the flag for the first user response = api_client.put_json( @@ -523,11 +560,13 @@ def test_handles_removing_flags( assert new_stats["active_flags"] == stats["active_flags"] - 1 -def test_build_course_stats_with_anonymous_posts(api_client: APIClient) -> None: +def test_build_course_stats_with_anonymous_posts( + api_client: APIClient, patched_get_backend: Any +) -> None: """Test that anonymous posts are not included in user stats after a non-anonymous post.""" - + backend = patched_get_backend() # Create a test user - user_id = Users().insert(external_id="3", username="user3") + user_id = backend.find_or_create_user(user_id="3", username="user3") course_id = "course-1" threads_ids = [] @@ -561,18 +600,19 @@ def test_build_course_stats_with_anonymous_posts(api_client: APIClient) -> None: assert stats["user_stats"][0]["threads"] == 1 -def test_update_user_stats(api_client: APIClient) -> None: +def test_update_user_stats(api_client: APIClient, patched_get_backend: Any) -> None: """Test that user stats are updated when requested.""" + backend = patched_get_backend() # Create a test course ID and users course_id = fake.word() authors_ids = [ - Users().insert(external_id=f"author-{i}", username=f"author-{i}") + backend.find_or_create_user(user_id=str(i), username=f"author-{i}") for i in range(1, 7) ] - authors = [Users().get(author_id) or {} for author_id in authors_ids] + authors = [backend.get_user(author_id) or {} for author_id in authors_ids] # Build the expected data without initial stats expected_data = build_structure_and_response( - course_id, authors, build_initial_stats=False + course_id, authors, backend, build_initial_stats=False ) # Sort the data for expected result (threads, responses, replies) @@ -604,18 +644,19 @@ def test_update_user_stats(api_client: APIClient) -> None: ) # User stats should now match the expected data -def test_mark_thread_as_read(api_client: APIClient) -> None: +def test_mark_thread_as_read(api_client: APIClient, patched_get_backend: Any) -> None: """Test that a thread is marked as read for the user.""" + backend = patched_get_backend() user_id = "1" username = "user1" - Users().insert(external_id=user_id, username=username) + backend.find_or_create_user(user_id=user_id, username=username) # Setup 10 threads for testing - threads_ids = setup_10_threads(user_id, username) - thread = CommentThread().get(threads_ids[0]) or {} + threads_ids = setup_10_threads(user_id, username, backend) + thread = backend.get_thread(threads_ids[0]) or {} # Create a test user - user_id = Users().insert(external_id="42", username="user-42") + user_id = backend.find_or_create_user(user_id="42", username="user-42") # Mark the first thread as read response = api_client.post_json( @@ -625,7 +666,7 @@ def test_mark_thread_as_read(api_client: APIClient) -> None: assert response.status_code == 200 # Reload the user and verify read state - user = Users().get(user_id) or {} + user = backend.get_user(user_id) or {} read_states = [ course_state for course_state in user["read_states"] @@ -638,11 +679,12 @@ def test_mark_thread_as_read(api_client: APIClient) -> None: ) # Verify the read date is on or after the thread's updated_at -def test_retire_user_inactive(api_client: APIClient) -> None: +def test_retire_user_inactive(api_client: APIClient, patched_get_backend: Any) -> None: """Test retiring an inactive user.""" - user_id = Users().insert(external_id="1", username="user1") - user = Users().get(user_id) or {} + backend = patched_get_backend() + user_id = backend.find_or_create_user(user_id="1", username="user1") + user = backend.get_user(user_id) or {} # Verify user is not subscribed to any threads response = api_client.get_json( @@ -667,12 +709,9 @@ def test_retire_user_inactive(api_client: APIClient) -> None: ) assert response.status_code == 200 - user = Users().get(user_id) or {} + user = backend.get_user(user_id) or {} assert user["username"] == retired_username assert user["email"] == "" - # Check user's comments are blanked - comments = list(Comment().find({"author_username": retired_username})) + list( - CommentThread().find({"author_username": retired_username}) - ) - assert len(comments) == 0 + content = backend.get_user_contents_by_username(retired_username) + assert len(content) == 0