diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index e5fc62d0..8911bfb9 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1425,10 +1425,7 @@ class DocumentViewSet( except requests.exceptions.RequestException as e: logger.error("Error while searching documents with indexer: %s", e) # fallback on title search if the indexer is not reached - return self._title_search( - request, params.validated_data, *args, **kwargs - ) - + return self._title_search(request, params.validated_data, *args, **kwargs) @staticmethod def _search_with_indexer(indexer, request, params): diff --git a/src/backend/core/tests/documents/test_api_documents_descendants.py b/src/backend/core/tests/documents/test_api_documents_descendants.py index ed42ae23..f320b070 100644 --- a/src/backend/core/tests/documents/test_api_documents_descendants.py +++ b/src/backend/core/tests/documents/test_api_documents_descendants.py @@ -1,6 +1,5 @@ """ -Tests for search API endpoint in impress's core app when indexer is not -available and a path param is given. +Tests for Documents API endpoint in impress's core app: descendants """ import random @@ -15,55 +14,22 @@ from core import factories pytestmark = pytest.mark.django_db -@pytest.fixture(autouse=True) -def disable_indexer(indexer_settings): - """Disable search indexer for all tests in this file.""" - indexer_settings.SEARCH_INDEXER_CLASS = None - - -def test_api_documents_descendants_list_anonymous_public_standalone(indexer_settings): +def test_api_documents_descendants_list_anonymous_public_standalone(): """Anonymous users should be allowed to retrieve the descendants of a public document.""" - document = factories.DocumentFactory(link_reach="public", title="doc parent") - child1, child2 = factories.DocumentFactory.create_batch(2, parent=document, title="doc child") - grand_child = factories.DocumentFactory(parent=child1, title="doc grand child") + document = factories.DocumentFactory(link_reach="public") + child1, child2 = factories.DocumentFactory.create_batch(2, parent=document) + grand_child = factories.DocumentFactory(parent=child1) factories.UserDocumentAccessFactory(document=child1) - response = APIClient().get( - "/api/v1.0/documents/search/", - data={"q": "doc", "path": document.path} - ) + response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/descendants/") assert response.status_code == 200 assert response.json() == { - "count": 4, + "count": 3, "next": None, "previous": None, "results": [ - { - # the search should include the parent document itself - "abilities": document.get_abilities(AnonymousUser()), - "ancestors_link_reach": None, - "ancestors_link_role": None, - "computed_link_reach": "public", - "computed_link_role": document.computed_link_role, - "created_at": document.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(document.creator.id), - "deleted_at": None, - "depth": 1, - "excerpt": document.excerpt, - "id": str(document.id), - "is_favorite": False, - "link_reach": document.link_reach, - "link_role": document.link_role, - "numchild": 2, - "nb_accesses_ancestors": 0, - "nb_accesses_direct": 0, - "path": document.path, - "title": document.title, - "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), - "user_role": None, - }, { "abilities": child1.get_abilities(AnonymousUser()), "ancestors_link_reach": "public", @@ -139,58 +105,31 @@ def test_api_documents_descendants_list_anonymous_public_standalone(indexer_sett } -def test_api_documents_descendants_list_anonymous_public_parent(indexer_settings): +def test_api_documents_descendants_list_anonymous_public_parent(): """ Anonymous users should be allowed to retrieve the descendants of a document who has a public ancestor. """ - grand_parent = factories.DocumentFactory(link_reach="public", title="grand parent doc") + grand_parent = factories.DocumentFactory(link_reach="public") parent = factories.DocumentFactory( - parent=grand_parent, link_reach=random.choice(["authenticated", "restricted"]), title="parent doc" + parent=grand_parent, link_reach=random.choice(["authenticated", "restricted"]) ) document = factories.DocumentFactory( - link_reach=random.choice(["authenticated", "restricted"]), parent=parent, title="document" + link_reach=random.choice(["authenticated", "restricted"]), parent=parent ) - child1, child2 = factories.DocumentFactory.create_batch(2, parent=document, title="child doc") - grand_child = factories.DocumentFactory(parent=child1, title="grand child doc") + child1, child2 = factories.DocumentFactory.create_batch(2, parent=document) + grand_child = factories.DocumentFactory(parent=child1) factories.UserDocumentAccessFactory(document=child1) - response = APIClient().get( - "/api/v1.0/documents/search/", - data={"q": "doc", "path": document.path} - ) + response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/descendants/") assert response.status_code == 200 assert response.json() == { - "count": 4, + "count": 3, "next": None, "previous": None, "results": [ - { - # the search should include the parent document itself - "abilities": document.get_abilities(AnonymousUser()), - "ancestors_link_reach": "public", - "ancestors_link_role": grand_parent.link_role, - "computed_link_reach": document.computed_link_reach, - "computed_link_role": document.computed_link_role, - "created_at": document.created_at.isoformat().replace("+00:00", "Z"), - "creator": str(document.creator.id), - "deleted_at": None, - "depth": 3, - "excerpt": document.excerpt, - "id": str(document.id), - "is_favorite": False, - "link_reach": document.link_reach, - "link_role": document.link_role, - "numchild": 2, - "nb_accesses_ancestors": 0, - "nb_accesses_direct": 0, - "path": document.path, - "title": document.title, - "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), - "user_role": None, - }, { "abilities": child1.get_abilities(AnonymousUser()), "ancestors_link_reach": "public", @@ -265,18 +204,15 @@ def test_api_documents_descendants_list_anonymous_public_parent(indexer_settings @pytest.mark.parametrize("reach", ["restricted", "authenticated"]) -def test_api_documents_descendants_list_anonymous_restricted_or_authenticated(reach, indexer_settings): +def test_api_documents_descendants_list_anonymous_restricted_or_authenticated(reach): """ Anonymous users should not be able to retrieve descendants of a document that is not public. """ - document = factories.DocumentFactory(title="parent", link_reach=reach) - child = factories.DocumentFactory(title="child", parent=document) - _grand_child = factories.DocumentFactory(title="grand child", parent=child) + document = factories.DocumentFactory(link_reach=reach) + child = factories.DocumentFactory(parent=document) + _grand_child = factories.DocumentFactory(parent=child) - response = APIClient().get( - "/api/v1.0/documents/search/", - data={"q": "doc", "path": document.path} - ) + response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/descendants/") assert response.status_code == 401 assert response.json() == { @@ -286,7 +222,7 @@ def test_api_documents_descendants_list_anonymous_restricted_or_authenticated(re @pytest.mark.parametrize("reach", ["public", "authenticated"]) def test_api_documents_descendants_list_authenticated_unrelated_public_or_authenticated( - reach, indexer_settings + reach, ): """ Authenticated users should be able to retrieve the descendants of a public/authenticated @@ -296,19 +232,17 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen client = APIClient() client.force_login(user) - document = factories.DocumentFactory(link_reach=reach, title="parent") + document = factories.DocumentFactory(link_reach=reach) child1, child2 = factories.DocumentFactory.create_batch( - 2, parent=document, link_reach="restricted", title="child" + 2, parent=document, link_reach="restricted" ) - grand_child = factories.DocumentFactory(parent=child1, title="grand child") + grand_child = factories.DocumentFactory(parent=child1) factories.UserDocumentAccessFactory(document=child1) response = client.get( - "/api/v1.0/documents/search/", - data={"q": "child", "path": document.path} + f"/api/v1.0/documents/{document.id!s}/descendants/", ) - assert response.status_code == 200 assert response.json() == { "count": 3, @@ -390,7 +324,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen @pytest.mark.parametrize("reach", ["public", "authenticated"]) def test_api_documents_descendants_list_authenticated_public_or_authenticated_parent( - reach, indexer_settings + reach, ): """ Authenticated users should be allowed to retrieve the descendants of a document who @@ -401,20 +335,17 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa client = APIClient() client.force_login(user) - grand_parent = factories.DocumentFactory(link_reach=reach, title="grand parent") - parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted", title="parent") - document = factories.DocumentFactory(link_reach="restricted", parent=parent, title="document") + grand_parent = factories.DocumentFactory(link_reach=reach) + parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted") + document = factories.DocumentFactory(link_reach="restricted", parent=parent) child1, child2 = factories.DocumentFactory.create_batch( - 2, parent=document, link_reach="restricted", title="child" + 2, parent=document, link_reach="restricted" ) - grand_child = factories.DocumentFactory(parent=child1, title="grand child") + grand_child = factories.DocumentFactory(parent=child1) factories.UserDocumentAccessFactory(document=child1) - response = client.get( - "/api/v1.0/documents/search/", - data={"q": "child", "path": document.path} - ) + response = client.get(f"/api/v1.0/documents/{document.id!s}/descendants/") assert response.status_code == 200 assert response.json() == { @@ -495,7 +426,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa } -def test_api_documents_descendants_list_authenticated_unrelated_restricted(indexer_settings): +def test_api_documents_descendants_list_authenticated_unrelated_restricted(): """ Authenticated users should not be allowed to retrieve the descendants of a document that is restricted and to which they are not related. @@ -504,47 +435,42 @@ def test_api_documents_descendants_list_authenticated_unrelated_restricted(index client = APIClient() client.force_login(user) - document = factories.DocumentFactory(link_reach="restricted", title="parent") - child1, _child2 = factories.DocumentFactory.create_batch(2, parent=document, title="child") - _grand_child = factories.DocumentFactory(parent=child1, title="grand child") + document = factories.DocumentFactory(link_reach="restricted") + child1, _child2 = factories.DocumentFactory.create_batch(2, parent=document) + _grand_child = factories.DocumentFactory(parent=child1) factories.UserDocumentAccessFactory(document=child1) response = client.get( - "/api/v1.0/documents/search/", - data={"q": "child", "path": document.path} + f"/api/v1.0/documents/{document.id!s}/descendants/", ) - assert response.status_code == 403 assert response.json() == { "detail": "You do not have permission to perform this action." } -def test_api_documents_descendants_list_authenticated_related_direct(indexer_settings): +def test_api_documents_descendants_list_authenticated_related_direct(): """ Authenticated users should be allowed to retrieve the descendants of a document to which they are directly related whatever the role. """ - indexer_settings.SEARCH_INDEXER_QUERY_URL = None - user = factories.UserFactory() client = APIClient() client.force_login(user) - document = factories.DocumentFactory(title="parent") + document = factories.DocumentFactory() access = factories.UserDocumentAccessFactory(document=document, user=user) factories.UserDocumentAccessFactory(document=document) - child1, child2 = factories.DocumentFactory.create_batch(2, parent=document, title="child") + child1, child2 = factories.DocumentFactory.create_batch(2, parent=document) factories.UserDocumentAccessFactory(document=child1) - grand_child = factories.DocumentFactory(parent=child1, title="grand child") + grand_child = factories.DocumentFactory(parent=child1) response = client.get( - "/api/v1.0/documents/search/", - data={"q": "child", "path": document.path} + f"/api/v1.0/documents/{document.id!s}/descendants/", ) assert response.status_code == 200 assert response.json() == { @@ -625,7 +551,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(indexer_set } -def test_api_documents_descendants_list_authenticated_related_parent(indexer_settings): +def test_api_documents_descendants_list_authenticated_related_parent(): """ Authenticated users should be allowed to retrieve the descendants of a document if they are related to one of its ancestors whatever the role. @@ -635,22 +561,21 @@ def test_api_documents_descendants_list_authenticated_related_parent(indexer_set client = APIClient() client.force_login(user) - grand_parent = factories.DocumentFactory(link_reach="restricted", title="parent") + grand_parent = factories.DocumentFactory(link_reach="restricted") grand_parent_access = factories.UserDocumentAccessFactory( document=grand_parent, user=user ) - parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted", title="parent") - document = factories.DocumentFactory(parent=parent, link_reach="restricted", title="document") + parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted") + document = factories.DocumentFactory(parent=parent, link_reach="restricted") - child1, child2 = factories.DocumentFactory.create_batch(2, parent=document, title="child") + child1, child2 = factories.DocumentFactory.create_batch(2, parent=document) factories.UserDocumentAccessFactory(document=child1) - grand_child = factories.DocumentFactory(parent=child1, title="grand child") + grand_child = factories.DocumentFactory(parent=child1) response = client.get( - "/api/v1.0/documents/search/", - data={"q": "child", "path": document.path} + f"/api/v1.0/documents/{document.id!s}/descendants/", ) assert response.status_code == 200 assert response.json() == { @@ -731,7 +656,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(indexer_set } -def test_api_documents_descendants_list_authenticated_related_child(indexer_settings): +def test_api_documents_descendants_list_authenticated_related_child(): """ Authenticated users should not be allowed to retrieve all the descendants of a document as a result of being related to one of its children. @@ -748,8 +673,7 @@ def test_api_documents_descendants_list_authenticated_related_child(indexer_sett factories.UserDocumentAccessFactory(document=document) response = client.get( - "/api/v1.0/documents/search/", - data={"q": "doc", "path": document.path} + f"/api/v1.0/documents/{document.id!s}/descendants/", ) assert response.status_code == 403 assert response.json() == { @@ -758,30 +682,24 @@ def test_api_documents_descendants_list_authenticated_related_child(indexer_sett def test_api_documents_descendants_list_authenticated_related_team_none( - mock_user_teams, indexer_settings + mock_user_teams, ): """ Authenticated users should not be able to retrieve the descendants of a restricted document related to teams in which the user is not. """ - indexer_settings.SEARCH_INDEXER_QUERY_URL = None - mock_user_teams.return_value = [] user = factories.UserFactory(with_owned_document=True) client = APIClient() client.force_login(user) - document = factories.DocumentFactory(link_reach="restricted", title="document") - factories.DocumentFactory.create_batch(2, parent=document, title="child") + document = factories.DocumentFactory(link_reach="restricted") + factories.DocumentFactory.create_batch(2, parent=document) factories.TeamDocumentAccessFactory(document=document, team="myteam") - response = client.get( - "/api/v1.0/documents/search/", - data={"q": "doc", "path": document.path} - ) - + response = client.get(f"/api/v1.0/documents/{document.id!s}/descendants/") assert response.status_code == 403 assert response.json() == { "detail": "You do not have permission to perform this action." @@ -789,7 +707,7 @@ def test_api_documents_descendants_list_authenticated_related_team_none( def test_api_documents_descendants_list_authenticated_related_team_members( - mock_user_teams, indexer_settings + mock_user_teams, ): """ Authenticated users should be allowed to retrieve the descendants of a document to which they @@ -801,16 +719,13 @@ def test_api_documents_descendants_list_authenticated_related_team_members( client = APIClient() client.force_login(user) - document = factories.DocumentFactory(link_reach="restricted", title="parent") - child1, child2 = factories.DocumentFactory.create_batch(2, parent=document, title="child") - grand_child = factories.DocumentFactory(parent=child1, title="grand child") + document = factories.DocumentFactory(link_reach="restricted") + child1, child2 = factories.DocumentFactory.create_batch(2, parent=document) + grand_child = factories.DocumentFactory(parent=child1) access = factories.TeamDocumentAccessFactory(document=document, team="myteam") - response = client.get( - "/api/v1.0/documents/search/", - data={"q": "child", "path": document.path} - ) + response = client.get(f"/api/v1.0/documents/{document.id!s}/descendants/") # pylint: disable=R0801 assert response.status_code == 200 diff --git a/src/backend/core/tests/documents/test_api_documents_search.py b/src/backend/core/tests/documents/test_api_documents_search.py index 389247d1..5b8151f9 100644 --- a/src/backend/core/tests/documents/test_api_documents_search.py +++ b/src/backend/core/tests/documents/test_api_documents_search.py @@ -83,78 +83,6 @@ def test_api_documents_search_fall_back_on_search_list(mock_list, indexer_settin q = "alpha" response = client.get("/api/v1.0/documents/search/", data={"q": q}) - assert mock_list.call_count == 1 - assert mock_list.call_args[0][0].GET.get("title") == q - assert response.json() == mocked_response - - -@mock.patch("core.api.viewsets.DocumentViewSet._list_descendants") -def test_api_documents_search_fallback_on_search_list_sub_docs( - mock_list_descendants, indexer_settings -): - """ - When indexer is not configured and path parameter is provided, - should call _list_descendants() method - """ - indexer_settings.SEARCH_URL = "http://find/api/v1.0/search" - assert get_document_indexer() is not None - - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - parent = factories.DocumentFactory(title="parent", users=[user]) - - mocked_response = { - "count": 0, - "next": None, - "previous": None, - "results": [{"title": "mocked _list_descendants result"}], - } - mock_list_descendants.return_value = drf_response.Response(mocked_response) - - q = "alpha" - response = client.get( - "/api/v1.0/documents/search/", data={"q": q, "path": parent.path} - ) - - assert mock_list_descendants.call_count == 1 - assert mock_list_descendants.call_args[0][0].GET.get("title") == q - assert mock_list_descendants.call_args[0][0].GET.get("path") == parent.path - assert response.json() == mocked_response - - -@mock.patch("core.api.viewsets.DocumentViewSet._title_search") -def test_api_documents_search_indexer_crashes(mock_title_search, indexer_settings): - """ - When indexer is configured but crashes -> falls back on title_search - """ - # indexer is properly configured - indexer_settings.SEARCH_URL = None - assert get_document_indexer() is None - # but returns an error when the query is sent - responses.add( - responses.POST, - "http://find/api/v1.0/search", - json=[{"error": "Some indexer error"}], - status=404, - ) - - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - mocked_response = { - "count": 0, - "next": None, - "previous": None, - "results": [{"title": "mocked title_search result"}], - } - mock_list.return_value = drf_response.Response(mocked_response) - - q = "alpha" - response = client.get("/api/v1.0/documents/search/", data={"q": q}) - assert mock_list.call_count == 1 assert mock_list.call_args[0][0].GET.get("q") == q assert response.json() == mocked_response diff --git a/src/backend/core/tests/documents/test_api_documents_search_descendants.py b/src/backend/core/tests/documents/test_api_documents_search_descendants.py index d916a297..a97321bb 100644 --- a/src/backend/core/tests/documents/test_api_documents_search_descendants.py +++ b/src/backend/core/tests/documents/test_api_documents_search_descendants.py @@ -288,9 +288,9 @@ def test_api_documents_search_descendants_list_anonymous_restricted_or_authentic "/api/v1.0/documents/search/", data={"q": "child", "path": document.path} ) - assert response.status_code == 401 + assert response.status_code == 403 assert response.json() == { - "detail": "Authentication credentials were not provided." + "detail": "You do not have permission to search within this document." } @@ -530,7 +530,7 @@ def test_api_documents_search_descendants_list_authenticated_unrelated_restricte assert response.status_code == 403 assert response.json() == { - "detail": "You do not have permission to perform this action." + "detail": "You do not have permission to search within this document." } @@ -769,7 +769,7 @@ def test_api_documents_search_descendants_list_authenticated_related_child(): ) assert response.status_code == 403 assert response.json() == { - "detail": "You do not have permission to perform this action." + "detail": "You do not have permission to search within this document." } @@ -797,7 +797,7 @@ def test_api_documents_search_descendants_list_authenticated_related_team_none( assert response.status_code == 403 assert response.json() == { - "detail": "You do not have permission to perform this action." + "detail": "You do not have permission to search within this document." } diff --git a/src/backend/core/tests/test_models_documents_indexer.py b/src/backend/core/tests/test_models_documents_indexer.py deleted file mode 100644 index ccbc3a22..00000000 --- a/src/backend/core/tests/test_models_documents_indexer.py +++ /dev/null @@ -1,441 +0,0 @@ -""" -Unit tests for the Document model -""" -# pylint: disable=too-many-lines - -from operator import itemgetter -from unittest import mock - -from django.core.cache import cache -from django.db import transaction - -import pytest - -from core import factories, models -from core.services.search_indexers import FindDocumentIndexer - -pytestmark = pytest.mark.django_db - - -def reset_batch_indexer_throttle(): - """Reset throttle flag""" - cache.delete("document-batch-indexer-throttle") - - -@pytest.fixture(autouse=True) -def reset_throttle(): - """Reset throttle flag before each test""" - reset_batch_indexer_throttle() - yield - reset_batch_indexer_throttle() - - -@mock.patch.object(FindDocumentIndexer, "push") -@pytest.mark.usefixtures("indexer_settings") -@pytest.mark.django_db(transaction=True) -def test_models_documents_post_save_indexer(mock_push): - """Test indexation task on document creation""" - with transaction.atomic(): - doc1, doc2, doc3 = factories.DocumentFactory.create_batch(3) - - accesses = {} - data = [call.args[0] for call in mock_push.call_args_list] - - indexer = FindDocumentIndexer() - - assert len(data) == 1 - - # One call - assert sorted(data[0], key=itemgetter("id")) == sorted( - [ - indexer.serialize_document(doc1, accesses), - indexer.serialize_document(doc2, accesses), - indexer.serialize_document(doc3, accesses), - ], - key=itemgetter("id"), - ) - - # The throttle counters should be reset - assert cache.get("document-batch-indexer-throttle") == 1 - - -@pytest.mark.django_db(transaction=True) -def test_models_documents_post_save_indexer_no_batches(indexer_settings): - """Test indexation task on doculment creation, no throttle""" - indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0 - - with mock.patch.object(FindDocumentIndexer, "push") as mock_push: - with transaction.atomic(): - doc1, doc2, doc3 = factories.DocumentFactory.create_batch(3) - - accesses = {} - data = [call.args[0] for call in mock_push.call_args_list] - - indexer = FindDocumentIndexer() - - # 3 calls - assert len(data) == 3 - # one document per call - assert [len(d) for d in data] == [1] * 3 - # all documents are indexed - assert sorted([d[0] for d in data], key=itemgetter("id")) == sorted( - [ - indexer.serialize_document(doc1, accesses), - indexer.serialize_document(doc2, accesses), - indexer.serialize_document(doc3, accesses), - ], - key=itemgetter("id"), - ) - - # The throttle counters should be reset - assert cache.get("file-batch-indexer-throttle") is None - - -@mock.patch.object(FindDocumentIndexer, "push") -@pytest.mark.django_db(transaction=True) -def test_models_documents_post_save_indexer_not_configured(mock_push, indexer_settings): - """Task should not start an indexation when disabled""" - indexer_settings.SEARCH_INDEXER_CLASS = None - - user = factories.UserFactory() - - with transaction.atomic(): - doc = factories.DocumentFactory() - factories.UserDocumentAccessFactory(document=doc, user=user) - - assert mock_push.assert_not_called - - -@mock.patch.object(FindDocumentIndexer, "push") -@pytest.mark.django_db(transaction=True) -def test_models_documents_post_save_indexer_wrongly_configured( - mock_push, indexer_settings -): - """Task should not start an indexation when disabled""" - indexer_settings.INDEXING_URL = None - - user = factories.UserFactory() - - with transaction.atomic(): - doc = factories.DocumentFactory() - factories.UserDocumentAccessFactory(document=doc, user=user) - - assert mock_push.assert_not_called - - -@mock.patch.object(FindDocumentIndexer, "push") -@pytest.mark.usefixtures("indexer_settings") -@pytest.mark.django_db(transaction=True) -def test_models_documents_post_save_indexer_with_accesses(mock_push): - """Test indexation task on document creation""" - user = factories.UserFactory() - - with transaction.atomic(): - doc1, doc2, doc3 = factories.DocumentFactory.create_batch(3) - - factories.UserDocumentAccessFactory(document=doc1, user=user) - factories.UserDocumentAccessFactory(document=doc2, user=user) - factories.UserDocumentAccessFactory(document=doc3, user=user) - - accesses = { - str(doc1.path): {"users": [user.sub]}, - str(doc2.path): {"users": [user.sub]}, - str(doc3.path): {"users": [user.sub]}, - } - - data = [call.args[0] for call in mock_push.call_args_list] - - indexer = FindDocumentIndexer() - - assert len(data) == 1 - assert sorted(data[0], key=itemgetter("id")) == sorted( - [ - indexer.serialize_document(doc1, accesses), - indexer.serialize_document(doc2, accesses), - indexer.serialize_document(doc3, accesses), - ], - key=itemgetter("id"), - ) - - -@mock.patch.object(FindDocumentIndexer, "push") -@pytest.mark.usefixtures("indexer_settings") -@pytest.mark.django_db(transaction=True) -def test_models_documents_post_save_indexer_deleted(mock_push): - """Indexation task on deleted or ancestor_deleted documents""" - user = factories.UserFactory() - - with transaction.atomic(): - doc = factories.DocumentFactory( - link_reach=models.LinkReachChoices.AUTHENTICATED - ) - main_doc = factories.DocumentFactory( - link_reach=models.LinkReachChoices.AUTHENTICATED - ) - child_doc = factories.DocumentFactory( - parent=main_doc, - link_reach=models.LinkReachChoices.AUTHENTICATED, - ) - - factories.UserDocumentAccessFactory(document=doc, user=user) - factories.UserDocumentAccessFactory(document=main_doc, user=user) - factories.UserDocumentAccessFactory(document=child_doc, user=user) - - # Manually reset the throttle flag here or the next indexation will be ignored for 1 second - reset_batch_indexer_throttle() - - with transaction.atomic(): - main_doc_deleted = models.Document.objects.get(pk=main_doc.pk) - main_doc_deleted.soft_delete() - - child_doc_deleted = models.Document.objects.get(pk=child_doc.pk) - - main_doc_deleted.refresh_from_db() - child_doc_deleted.refresh_from_db() - - assert main_doc_deleted.deleted_at is not None - assert child_doc_deleted.ancestors_deleted_at is not None - - assert child_doc_deleted.deleted_at is None - assert child_doc_deleted.ancestors_deleted_at is not None - - accesses = { - str(doc.path): {"users": [user.sub]}, - str(main_doc_deleted.path): {"users": [user.sub]}, - str(child_doc_deleted.path): {"users": [user.sub]}, - } - - data = [call.args[0] for call in mock_push.call_args_list] - - indexer = FindDocumentIndexer() - - assert len(data) == 2 - - # First indexation on document creation - assert sorted(data[0], key=itemgetter("id")) == sorted( - [ - indexer.serialize_document(doc, accesses), - indexer.serialize_document(main_doc, accesses), - indexer.serialize_document(child_doc, accesses), - ], - key=itemgetter("id"), - ) - - # Even deleted items are re-indexed : only update their status in the future - assert sorted(data[1], key=itemgetter("id")) == sorted( - [ - indexer.serialize_document(main_doc_deleted, accesses), # soft_delete() - indexer.serialize_document(child_doc_deleted, accesses), - ], - key=itemgetter("id"), - ) - - -@pytest.mark.django_db(transaction=True) -@pytest.mark.usefixtures("indexer_settings") -def test_models_documents_indexer_hard_deleted(): - """Indexation task on hard deleted document""" - user = factories.UserFactory() - - with transaction.atomic(): - doc = factories.DocumentFactory( - link_reach=models.LinkReachChoices.AUTHENTICATED - ) - factories.UserDocumentAccessFactory(document=doc, user=user) - - # Call task on deleted document. - with mock.patch.object(FindDocumentIndexer, "push") as mock_push: - doc.delete() - - # Hard delete document are not re-indexed. - assert mock_push.assert_not_called - - -@mock.patch.object(FindDocumentIndexer, "push") -@pytest.mark.usefixtures("indexer_settings") -@pytest.mark.django_db(transaction=True) -def test_models_documents_post_save_indexer_restored(mock_push): - """Restart indexation task on restored documents""" - user = factories.UserFactory() - - with transaction.atomic(): - doc = factories.DocumentFactory( - link_reach=models.LinkReachChoices.AUTHENTICATED - ) - doc_deleted = factories.DocumentFactory( - link_reach=models.LinkReachChoices.AUTHENTICATED - ) - doc_ancestor_deleted = factories.DocumentFactory( - parent=doc_deleted, - link_reach=models.LinkReachChoices.AUTHENTICATED, - ) - - factories.UserDocumentAccessFactory(document=doc, user=user) - factories.UserDocumentAccessFactory(document=doc_deleted, user=user) - factories.UserDocumentAccessFactory(document=doc_ancestor_deleted, user=user) - - doc_deleted.soft_delete() - - doc_deleted.refresh_from_db() - doc_ancestor_deleted.refresh_from_db() - - assert doc_deleted.deleted_at is not None - assert doc_deleted.ancestors_deleted_at is not None - - assert doc_ancestor_deleted.deleted_at is None - assert doc_ancestor_deleted.ancestors_deleted_at is not None - - # Manually reset the throttle flag here or the next indexation will be ignored for 1 second - reset_batch_indexer_throttle() - - with transaction.atomic(): - doc_restored = models.Document.objects.get(pk=doc_deleted.pk) - doc_restored.restore() - - doc_ancestor_restored = models.Document.objects.get(pk=doc_ancestor_deleted.pk) - - assert doc_restored.deleted_at is None - assert doc_restored.ancestors_deleted_at is None - - assert doc_ancestor_restored.deleted_at is None - assert doc_ancestor_restored.ancestors_deleted_at is None - - accesses = { - str(doc.path): {"users": [user.sub]}, - str(doc_deleted.path): {"users": [user.sub]}, - str(doc_ancestor_deleted.path): {"users": [user.sub]}, - } - - data = [call.args[0] for call in mock_push.call_args_list] - - indexer = FindDocumentIndexer() - - # All docs are re-indexed - assert len(data) == 2 - - # First indexation on items creation & soft delete (in the same transaction) - assert sorted(data[0], key=itemgetter("id")) == sorted( - [ - indexer.serialize_document(doc, accesses), - indexer.serialize_document(doc_deleted, accesses), - indexer.serialize_document(doc_ancestor_deleted, accesses), - ], - key=itemgetter("id"), - ) - - # Restored items are re-indexed : only update their status in the future - assert sorted(data[1], key=itemgetter("id")) == sorted( - [ - indexer.serialize_document(doc_restored, accesses), # restore() - indexer.serialize_document(doc_ancestor_restored, accesses), - ], - key=itemgetter("id"), - ) - - -@pytest.mark.django_db(transaction=True) -@pytest.mark.usefixtures("indexer_settings") -def test_models_documents_post_save_indexer_throttle(): - """Test indexation task skipping on document update""" - indexer = FindDocumentIndexer() - user = factories.UserFactory() - - with mock.patch.object(FindDocumentIndexer, "push"): - with transaction.atomic(): - docs = factories.DocumentFactory.create_batch(5, users=(user,)) - - accesses = {str(item.path): {"users": [user.sub]} for item in docs} - - with mock.patch.object(FindDocumentIndexer, "push") as mock_push: - # Simulate 1 running task - cache.set("document-batch-indexer-throttle", 1) - - # save doc to trigger the indexer, but nothing should be done since - # the flag is up - with transaction.atomic(): - docs[0].save() - docs[2].save() - docs[3].save() - - assert [call.args[0] for call in mock_push.call_args_list] == [] - - with mock.patch.object(FindDocumentIndexer, "push") as mock_push: - # No waiting task - cache.delete("document-batch-indexer-throttle") - - with transaction.atomic(): - docs[0].save() - docs[2].save() - docs[3].save() - - data = [call.args[0] for call in mock_push.call_args_list] - - # One call - assert len(data) == 1 - - assert sorted(data[0], key=itemgetter("id")) == sorted( - [ - indexer.serialize_document(docs[0], accesses), - indexer.serialize_document(docs[2], accesses), - indexer.serialize_document(docs[3], accesses), - ], - key=itemgetter("id"), - ) - - -@pytest.mark.django_db(transaction=True) -@pytest.mark.usefixtures("indexer_settings") -def test_models_documents_access_post_save_indexer(): - """Test indexation task on DocumentAccess update""" - users = factories.UserFactory.create_batch(3) - - with mock.patch.object(FindDocumentIndexer, "push"): - with transaction.atomic(): - doc = factories.DocumentFactory(users=users) - doc_accesses = models.DocumentAccess.objects.filter(document=doc).order_by( - "user__sub" - ) - - reset_batch_indexer_throttle() - - with mock.patch.object(FindDocumentIndexer, "push") as mock_push: - with transaction.atomic(): - for doc_access in doc_accesses: - doc_access.save() - - data = [call.args[0] for call in mock_push.call_args_list] - - # One call - assert len(data) == 1 - - assert [d["id"] for d in data[0]] == [str(doc.pk)] - - -@pytest.mark.django_db(transaction=True) -def test_models_items_access_post_save_indexer_no_throttle(indexer_settings): - """Test indexation task on ItemAccess update, no throttle""" - indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0 - - users = factories.UserFactory.create_batch(3) - - with transaction.atomic(): - doc = factories.DocumentFactory(users=users) - doc_accesses = models.DocumentAccess.objects.filter(document=doc).order_by( - "user__sub" - ) - - reset_batch_indexer_throttle() - - with mock.patch.object(FindDocumentIndexer, "push") as mock_push: - with transaction.atomic(): - for doc_access in doc_accesses: - doc_access.save() - - data = [call.args[0] for call in mock_push.call_args_list] - - # 3 calls - assert len(data) == 3 - # one document per call - assert [len(d) for d in data] == [1] * 3 - # the same document is indexed 3 times - assert [d[0]["id"] for d in data] == [str(doc.pk)] * 3