class OrganizationsViewsetsTestCase(TestCase): """ Test the API endpoints for organizations (list and details) """ def test_viewsets_organizations_retrieve(self): """ Happy path: the client requests an existing organization, gets it back """ factory = APIRequestFactory() request = factory.get("/api/v1.0/organizations/42") with mock.patch.object( settings.ES_CLIENT, "get", return_value={ "_id": 42, "_source": { "logo": { "fr": "/logo.png" }, "title": { "fr": "Université Paris 42" }, }, }, ): # Note: we need to use a separate argument for the ID as that is what the ViewSet uses response = OrganizationsViewSet.as_view({"get": "retrieve" })(request, 42, version="1.0") # The client received a proper response with the relevant organization self.assertEqual(response.status_code, 200) self.assertEqual( response.data, { "id": 42, "logo": "/logo.png", "title": "Université Paris 42" }, ) def test_viewsets_organizations_retrieve_unknown(self): """ Error case: the client is asking for an organization that does not exist """ factory = APIRequestFactory() request = factory.get("/api/v1.0/organizations/43") # Act like the ES client would when we attempt to get a non-existent document with mock.patch.object(settings.ES_CLIENT, "get", side_effect=NotFoundError): response = OrganizationsViewSet.as_view({"get": "retrieve" })(request, 43, version="1.0") # The client received a standard NotFound response self.assertEqual(response.status_code, 404) @mock.patch( "richie.apps.search.indexers.organizations.OrganizationsIndexer.build_es_query", lambda x: (2, 0, { "query": "something" }), ) @mock.patch.object(settings.ES_CLIENT, "search") def test_viewsets_organizations_search(self, mock_search): """ Happy path: the consumer is filtering the organizations by title """ factory = APIRequestFactory() request = factory.get( "/api/v1.0/organizations?query=Université&limit=2") mock_search.return_value = { "hits": { "hits": [ { "_id": 21, "_source": { "logo": { "fr": "/logo_21.png" }, "title": { "fr": "Université Paris 13" }, }, }, { "_id": 61, "_source": { "logo": { "fr": "/logo_61.png" }, "title": { "fr": "Université Paris 8" }, }, }, ], "total": 32, } } response = OrganizationsViewSet.as_view({"get": "list"})(request, version="1.0") # The client received a properly formatted response self.assertEqual(response.status_code, 200) self.assertEqual( response.data, { "meta": { "count": 2, "offset": 0, "total_count": 32 }, "objects": [ { "id": 21, "logo": "/logo_21.png", "title": "Université Paris 13" }, { "id": 61, "logo": "/logo_61.png", "title": "Université Paris 8" }, ], }, ) # The ES connector was called with a query that matches the client's request mock_search.assert_called_with( _source=["absolute_url", "logo", "title.*"], body={"query": "something"}, doc_type="organization", from_=0, index="richie_organizations", size=2, ) @mock.patch( "richie.apps.search.indexers.organizations.OrganizationsIndexer.build_es_query", side_effect=QueryFormatException({"limit": "incorrect value"}), ) def test_viewsets_organizations_search_with_invalid_params(self, _): """ Error case: the client used an incorrectly formatted request """ factory = APIRequestFactory() # The request contains incorrect params: limit should be a positive integer request = factory.get("/api/v1.0/organizations?title=&limit=-2") response = OrganizationsViewSet.as_view({"get": "list"})(request, version="1.0") # The client received a BadRequest response with the relevant data self.assertEqual(response.status_code, 400) self.assertTrue("limit" in response.data["errors"])
class CoursesViewsetsTestCase(TestCase): """ Test the API endpoints for courses (list and details) """ def setUp(self): """ Make sure all our tests are timezone-agnostic. Some of them parse ISO datetimes and those would be broken if we did not enforce timezone normalization. """ timezone.activate(pytz.utc) def test_viewsets_courses_retrieve(self, *_): """ Happy path: the client requests an existing course, gets it back """ factory = APIRequestFactory() request = factory.get("/api/v1.0/courses/42") with mock.patch.object(settings.ES_CLIENT, "get", return_value={"_id": 42}): # Note: we need to use a separate argument for the ID as that is what the ViewSet uses response = CoursesViewSet.as_view({"get": "retrieve"})(request, 42, version="1.0") # The client received a proper response with the relevant course self.assertEqual(response.status_code, 200) self.assertEqual(response.data, "Course #42") def test_viewsets_courses_retrieve_unknown(self, *_): """ Error case: the client is asking for a course that does not exist """ factory = APIRequestFactory() request = factory.get("/api/v1.0/courses/43") # Act like the ES client would when we attempt to get a non-existent document with mock.patch.object(settings.ES_CLIENT, "get", side_effect=NotFoundError): response = CoursesViewSet.as_view({"get": "retrieve"})(request, 43, version="1.0") # The client received a standard NotFound response self.assertEqual(response.status_code, 404) @mock.patch( "richie.apps.search.indexers.courses.CoursesIndexer.build_es_query", lambda *args: (2, 77, { "some": "query" }, { "some": "aggs" }), ) @mock.patch( "richie.apps.search.indexers.courses.CoursesIndexer.get_list_sorting_script", lambda *args: {"some": "sorting"}, ) @mock.patch.object(settings.ES_CLIENT, "search") @mock.patch( "richie.apps.search.viewsets.courses.FILTERS", new=[ ( "richie.apps.search.utils.filter_definitions.FilterDefinitionCustom", { "name": "availability", "human_name": "Availability", "choices": [ ("coming_soon", "Coming soon", [{ "is_coming_soon": True }]), ("current", "Current session", [{ "is_current": True }]), ("open", "Open for enrollment", [{ "is_open": True }]), ], }, ), ( "richie.apps.search.utils.filter_definitions.FilterDefinitionTerms", { "name": "organizations", "human_name": "Organizations" }, ), ], ) def test_viewsets_courses_search(self, mock_search, *_): """ Happy path: the consumer is filtering courses by matching text """ factory = APIRequestFactory() request = factory.get( "/api/v1.0/courses?query=some%20phrase%20terms&limit=2&offset=20") # We use a mock implementation instead of return_value as a pragmatic way to get results # from the whole filters pipeline without having to mock too many things. # pylint: disable=inconsistent-return-statements def mock_search_implementation(index, **_): if index == "richie_courses": return { "hits": { "hits": [{ "_id": 523 }, { "_id": 861 }], "total": 35 }, "aggregations": { "all_courses": { "availability@coming_soon": { "doc_count": 8 }, "availability@current": { "doc_count": 42 }, "availability@open": { "doc_count": 59 }, "organizations": { "organizations": { "buckets": [ { "key": "11", "doc_count": 17 }, { "key": "21", "doc_count": 19 }, ] } }, } }, } if index == "richie_organizations": return { "hits": { "hits": [ { "_id": "11", "_source": { "title": { "en": "Organization 11" } }, }, { "_id": "21", "_source": { "title": { "en": "Organization 21" } }, }, ] } } mock_search.side_effect = mock_search_implementation response = CoursesViewSet.as_view({"get": "list"})(request, version="1.0") # The client received a properly formatted response self.assertEqual(response.status_code, 200) self.assertEqual( response.data, { "meta": { "count": 2, "offset": 77, "total_count": 35 }, "objects": ["Course #523", "Course #861"], "facets": { "availability": { "coming_soon": 8, "current": 42, "open": 59 }, "organizations": { "11": 17, "21": 19 }, }, "filters": { "availability": { "human_name": "Availability", "is_drilldown": False, "name": "availability", "values": [ { "count": 8, "human_name": "Coming soon", "key": "coming_soon", }, { "count": 42, "human_name": "Current session", "key": "current", }, { "count": 59, "human_name": "Open for enrollment", "key": "open", }, ], }, "organizations": { "human_name": "Organizations", "is_drilldown": False, "name": "organizations", "values": [ { "count": 17, "human_name": "Organization 11", "key": "11" }, { "count": 19, "human_name": "Organization 21", "key": "21" }, ], }, }, }, ) # The ES connector was called with appropriate arguments for the client's request mock_search.assert_any_call( _source=[ "start", "end", "enrollment_start", "enrollment_end", "absolute_url", "cover_image", "languages", "organizations", "categories", "title.*", ], body={ "aggs": { "some": "aggs" }, "query": { "some": "query" }, "sort": { "some": "sorting" }, }, doc_type="course", from_=77, index="richie_courses", size=2, ) @mock.patch( "richie.apps.search.indexers.courses.CoursesIndexer.build_es_query", side_effect=QueryFormatException({"limit": "incorrect value"}), ) def test_viewsets_courses_search_with_invalid_params(self, *_): """ Error case: the query string params are not properly formatted """ factory = APIRequestFactory() # The request contains incorrect params: limit should be an integer, not a string request = factory.get("/api/v1.0/courses?limit=fail") response = CoursesViewSet.as_view({"get": "list"})(request, version="1.0") # The client received a BadRequest response with the relevant data self.assertEqual(response.status_code, 400) self.assertTrue("limit" in response.data["errors"])
class CoursesViewsetsTestCase(TestCase): """ Test the API endpoints for courses (list and details) """ def setUp(self): """ Make sure all our tests are timezone-agnostic. Some of them parse ISO datetimes and those would be broken if we did not enforce timezone normalization. """ timezone.activate(pytz.utc) def test_viewsets_courses_retrieve(self, *_): """ Happy path: the client requests an existing course, gets it back """ factory = APIRequestFactory() request = factory.get("/api/v1.0/courses/42") with mock.patch.object(settings.ES_CLIENT, "get", return_value={"_id": 42}): # Note: we need to use a separate argument for the ID as that is what the ViewSet uses response = CoursesViewSet.as_view({"get": "retrieve"})(request, 42, version="1.0") # The client received a proper response with the relevant course self.assertEqual(response.status_code, 200) self.assertEqual(response.data, "Course #42") def test_viewsets_courses_retrieve_unknown(self, *_): """ Error case: the client is asking for a course that does not exist """ factory = APIRequestFactory() request = factory.get("/api/v1.0/courses/43") # Act like the ES client would when we attempt to get a non-existent document with mock.patch.object(settings.ES_CLIENT, "get", side_effect=NotFoundError): response = CoursesViewSet.as_view({"get": "retrieve"})(request, 43, version="1.0") # The client received a standard NotFound response self.assertEqual(response.status_code, 404) @mock.patch( "richie.apps.search.indexers.courses.CoursesIndexer.build_es_query", lambda *args: (2, 77, { "some": "query" }, { "some": "aggs" }), ) @mock.patch( "richie.apps.search.indexers.courses.CoursesIndexer.get_list_sorting_script", lambda *args: {"some": "sorting"}, ) @mock.patch.object(settings.ES_CLIENT, "search") def test_viewsets_courses_search(self, mock_search, *_): """ Happy path: the consumer is filtering courses by matching text """ factory = APIRequestFactory() request = factory.get( "/api/v1.0/courses?query=some%20phrase%20terms&limit=2&offset=20") mock_search.return_value = { "hits": { "hits": [{ "_id": 523 }, { "_id": 861 }], "total": 35 }, "aggregations": { "all_courses": { "availability@coming_soon": { "doc_count": 8 }, "availability@current": { "doc_count": 42 }, "availability@open": { "doc_count": 59 }, "language@en": { "doc_count": 81 }, "language@fr": { "doc_count": 23 }, "subjects": { "subjects": { "buckets": [ { "key": "11", "doc_count": 17 }, { "key": "21", "doc_count": 19 }, ] } }, } }, } response = CoursesViewSet.as_view({"get": "list"})(request, version="1.0") # The client received a properly formatted response self.assertEqual(response.status_code, 200) self.assertEqual( response.data, { "meta": { "count": 2, "offset": 77, "total_count": 35 }, "objects": ["Course #523", "Course #861"], "facets": { "availability": { "coming_soon": 8, "current": 42, "open": 59 }, "language": { "en": 81, "fr": 23 }, "subjects": { "11": 17, "21": 19 }, }, }, ) # The ES connector was called with appropriate arguments for the client's request mock_search.assert_called_with( body={ "aggs": { "some": "aggs" }, "query": { "some": "query" }, "sort": { "some": "sorting" }, }, doc_type="course", from_=77, index="richie_courses", size=2, ) @mock.patch( "richie.apps.search.indexers.courses.CoursesIndexer.build_es_query", side_effect=QueryFormatException({"limit": "incorrect value"}), ) def test_viewsets_courses_search_with_invalid_params(self, *_): """ Error case: the query string params are not properly formatted """ factory = APIRequestFactory() # The request contains incorrect params: limit should be an integer, not a string request = factory.get("/api/v1.0/courses?limit=fail") response = CoursesViewSet.as_view({"get": "list"})(request, version="1.0") # The client received a BadRequest response with the relevant data self.assertEqual(response.status_code, 400) self.assertTrue("limit" in response.data["errors"])
class SubjectsViewsetsTestCase(TestCase): """ Test the API endpoints for subjects (list and details) """ def test_viewsets_subjects_retrieve(self): """ Happy path: the client requests an existing subject, gets it back """ factory = APIRequestFactory() request = factory.get("/api/v1.0/subjects/42") with mock.patch.object( settings.ES_CLIENT, "get", return_value={ "_id": 42, "_source": { "image": "example.com/image.png", "name": { "fr": "Some Subject" }, }, }, ): # Note: we need to use a separate argument for the ID as that is what the ViewSet uses response = SubjectsViewSet.as_view({"get": "retrieve"})(request, 42, version="1.0") # The client received a proper response with the relevant subject self.assertEqual(response.status_code, 200) self.assertEqual( response.data, { "id": 42, "image": "example.com/image.png", "name": "Some Subject" }, ) def test_viewsets_subjects_retrieve_unknown(self): """ Error case: the client is asking for a subject that does not exist """ factory = APIRequestFactory() request = factory.get("/api/v1.0/subjects/43") # Act like the ES client would when we attempt to get a non-existent document with mock.patch.object(settings.ES_CLIENT, "get", side_effect=NotFoundError): response = SubjectsViewSet.as_view({"get": "retrieve"})(request, 43, version="1.0") # The client received a standard NotFound response self.assertEqual(response.status_code, 404) @mock.patch( "richie.apps.search.indexers.subjects.SubjectsIndexer.build_es_query", lambda x: (2, 0, { "query": "example" }), ) @mock.patch.object(settings.ES_CLIENT, "search") def test_viewsets_subjects_search(self, mock_search): """ Happy path: the subject is filtering the subjects by name """ factory = APIRequestFactory() request = factory.get("/api/v1.0/subject?query=Science&limit=2") mock_search.return_value = { "hits": { "hits": [ { "_id": 21, "_source": { "image": "example.com/image.png", "name": { "fr": "Computer Science" }, }, }, { "_id": 61, "_source": { "image": "example.com/image.png", "name": { "fr": "Engineering Sciences" }, }, }, ], "total": 32, } } response = SubjectsViewSet.as_view({"get": "list"})(request, version="1.0") # The client received a properly formatted response self.assertEqual(response.status_code, 200) self.assertEqual( response.data, { "meta": { "count": 2, "offset": 0, "total_count": 32 }, "objects": [ { "id": 21, "image": "example.com/image.png", "name": "Computer Science", }, { "id": 61, "image": "example.com/image.png", "name": "Engineering Sciences", }, ], }, ) # The ES connector was called with a query that matches the client's request mock_search.assert_called_with( body={"query": "example"}, doc_type="subject", from_=0, index="richie_subjects", size=2, ) @mock.patch( "richie.apps.search.indexers.subjects.SubjectsIndexer.build_es_query", side_effect=QueryFormatException({"limit": "incorrect value"}), ) def test_viewsets_subjects_search_with_invalid_params(self, _): """ Error case: the client used an incorrectly formatted request """ factory = APIRequestFactory() # The request contains incorrect params: limit should be a positive integer request = factory.get("/api/v1.0/subject?name=&limit=-2") response = SubjectsViewSet.as_view({"get": "list"})(request, version="1.0") # The client received a BadRequest response with the relevant data self.assertEqual(response.status_code, 400) self.assertTrue("limit" in response.data["errors"])