Exemple #1
0
    def test_search_courses_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"])
Exemple #2
0
    def test_retrieve_unknown_course(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)
Exemple #3
0
    def test_retrieve_course(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")
Exemple #4
0
    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,
        )
    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,
        )
Exemple #6
0
    def test_search_all_courses(self, mock_search, *_):
        """
        Happy path: the consumer is not filtering the courses at all
        """
        factory = APIRequestFactory()
        request = factory.get("/api/v1.0/courses?limit=2&offset=10")

        mock_search.return_value = {
            "hits": {"hits": [{"_id": 89}, {"_id": 94}], "total": 90},
            "aggregations": {
                "all_courses": {
                    "organizations": {
                        "organizations": {
                            "buckets": [
                                {"key": "1", "doc_count": 7},
                                {"key": "2", "doc_count": 9},
                            ]
                        }
                    }
                }
            },
        }

        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": 10, "total_count": 90},
                "objects": ["Course #89", "Course #94"],
                "facets": {"organizations": {"1": 7, "2": 9}},
            },
        )
        # The ES connector was called with appropriate arguments for the client's request
        mock_search.assert_called_with(
            body={
                "aggs": {
                    "all_courses": {
                        "global": {},
                        "aggregations": {
                            "organizations": {
                                "filter": {"bool": {"must": []}},
                                "aggregations": {
                                    "organizations": {
                                        "terms": {"field": "organizations"}
                                    }
                                },
                            },
                            "subjects": {
                                "filter": {"bool": {"must": []}},
                                "aggregations": {
                                    "subjects": {"terms": {"field": "subjects"}}
                                },
                            },
                        },
                    }
                },
                "query": {"match_all": {}},
            },
            doc_type="course",
            from_=10,
            index="richie_courses",
            size=2,
        )
Exemple #7
0
    def test_combined_search_courses(self, mock_search, *_):
        """
        Happy path: the consumer is using several filters at the same time
        """
        factory = APIRequestFactory()

        start_date = json.dumps(["2018-01-01T06:00:00Z", None])
        end_date = json.dumps(["2018-04-30T06:00:00Z", "2018-06-30T06:00:00Z"])

        request = factory.get(
            "/api/v1.0/courses?subjects=42&subjects=84&query=these%20phrase%20terms&limit=2&"
            + "start_date={start_date}&end_date={end_date}".format(
                start_date=start_date, end_date=end_date
            )
        )

        mock_search.return_value = {
            "hits": {"hits": [{"_id": 999}, {"_id": 888}], "total": 3},
            "aggregations": {
                "all_courses": {
                    "subjects": {
                        "subjects": {
                            "buckets": [
                                {"key": "42", "doc_count": 3},
                                {"key": "84", "doc_count": 1},
                            ]
                        }
                    }
                }
            },
        }

        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": 0, "total_count": 3},
                "objects": ["Course #999", "Course #888"],
                "facets": {"subjects": {"42": 3, "84": 1}},
            },
        )
        # The ES connector was called with appropriate arguments for the client's request
        range_end_date = {
            "range": {
                "end_date": {
                    "gte": datetime.datetime(2018, 4, 30, 6, 0, tzinfo=pytz.utc),
                    "lte": datetime.datetime(2018, 6, 30, 6, 0, tzinfo=pytz.utc),
                }
            }
        }
        multi_match = {
            "multi_match": {
                "fields": ["short_description.*", "title.*"],
                "query": "these phrase terms",
                "type": "cross_fields",
            }
        }
        range_start_date = {
            "range": {
                "start_date": {
                    "gte": datetime.datetime(2018, 1, 1, 6, 0, tzinfo=pytz.utc),
                    "lte": None,
                }
            }
        }
        terms_subjects = {"terms": {"subjects": [42, 84]}}
        mock_search.assert_called_with(
            body={
                "aggs": {
                    "all_courses": {
                        "global": {},
                        "aggregations": {
                            "organizations": {
                                "filter": {
                                    "bool": {
                                        "must": [
                                            range_end_date,
                                            multi_match,
                                            range_start_date,
                                            terms_subjects,
                                        ]
                                    }
                                },
                                "aggregations": {
                                    "organizations": {
                                        "terms": {"field": "organizations"}
                                    }
                                },
                            },
                            "subjects": {
                                "filter": {
                                    "bool": {
                                        "must": [
                                            range_end_date,
                                            multi_match,
                                            range_start_date,
                                        ]
                                    }
                                },
                                "aggregations": {
                                    "subjects": {"terms": {"field": "subjects"}}
                                },
                            },
                        },
                    }
                },
                "query": {
                    "bool": {
                        "must": [
                            range_end_date,
                            multi_match,
                            range_start_date,
                            terms_subjects,
                        ]
                    }
                },
            },
            doc_type="course",
            from_=0,
            index="richie_courses",
            size=2,
        )
Exemple #8
0
    def test_search_courses_by_range_datetimes(self, mock_search, *_):
        """
        Happy path: the consumer is filtering courses using datetimes
        """
        factory = APIRequestFactory()

        start_date = json.dumps(["2018-01-01T06:00:00Z", None])
        end_date = json.dumps(["2018-04-30T06:00:00Z", "2018-06-30T06:00:00Z"])
        request = factory.get(
            "/api/v1.0/courses?start_date={start_date}&end_date={end_date}&limit=2".format(
                start_date=start_date, end_date=end_date
            )
        )

        mock_search.return_value = {
            "hits": {"hits": [{"_id": 13}, {"_id": 15}], "total": 7},
            "aggregations": {
                "all_courses": {
                    "subjects": {
                        "subjects": {
                            "buckets": [
                                {"key": "61", "doc_count": 4},
                                {"key": "122", "doc_count": 5},
                            ]
                        }
                    }
                }
            },
        }

        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": 0, "total_count": 7},
                "objects": ["Course #13", "Course #15"],
                "facets": {"subjects": {"61": 4, "122": 5}},
            },
        )
        # The ES connector was called with appropriate arguments for the client's request
        range_end_date = {
            "range": {
                "end_date": {
                    "gte": datetime.datetime(2018, 4, 30, 6, 0, tzinfo=pytz.utc),
                    "lte": datetime.datetime(2018, 6, 30, 6, 0, tzinfo=pytz.utc),
                }
            }
        }
        range_start_date = {
            "range": {
                "start_date": {
                    "gte": datetime.datetime(2018, 1, 1, 6, 0, tzinfo=pytz.utc),
                    "lte": None,
                }
            }
        }
        mock_search.assert_called_with(
            body={
                "aggs": {
                    "all_courses": {
                        "global": {},
                        "aggregations": {
                            "organizations": {
                                "filter": {
                                    "bool": {"must": [range_end_date, range_start_date]}
                                },
                                "aggregations": {
                                    "organizations": {
                                        "terms": {"field": "organizations"}
                                    }
                                },
                            },
                            "subjects": {
                                "filter": {
                                    "bool": {"must": [range_end_date, range_start_date]}
                                },
                                "aggregations": {
                                    "subjects": {"terms": {"field": "subjects"}}
                                },
                            },
                        },
                    }
                },
                "query": {"bool": {"must": [range_end_date, range_start_date]}},
            },
            doc_type="course",
            from_=0,
            index="richie_courses",
            size=2,
        )
Exemple #9
0
    def test_search_courses_by_single_term_organizations(self, mock_search, *_):
        """
        Happy path: make sure a single term (eg. subject or organization) which should be
        valid, is accepted (added after catching an error during manual testing)
        """
        factory = APIRequestFactory()
        request = factory.get("/api/v1.0/courses?organizations=345&limit=2")

        mock_search.return_value = {
            "hits": {"hits": [{"_id": 37}, {"_id": 98}], "total": 12},
            "aggregations": {
                "all_courses": {
                    "organizations": {
                        "organizations": {
                            "buckets": [
                                {"key": "3", "doc_count": 6},
                                {"key": "14", "doc_count": 7},
                            ]
                        }
                    }
                }
            },
        }

        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": 0, "total_count": 12},
                "objects": ["Course #37", "Course #98"],
                "facets": {"organizations": {"3": 6, "14": 7}},
            },
        )
        # The ES connector was called with appropriate arguments for the client's request
        term_organization = {"terms": {"organizations": [345]}}
        mock_search.assert_called_with(
            body={
                "aggs": {
                    "all_courses": {
                        "global": {},
                        "aggregations": {
                            "organizations": {
                                "filter": {"bool": {"must": []}},
                                "aggregations": {
                                    "organizations": {
                                        "terms": {"field": "organizations"}
                                    }
                                },
                            },
                            "subjects": {
                                "filter": {"bool": {"must": [term_organization]}},
                                "aggregations": {
                                    "subjects": {"terms": {"field": "subjects"}}
                                },
                            },
                        },
                    }
                },
                "query": {"bool": {"must": [term_organization]}},
            },
            doc_type="course",
            from_=0,
            index="richie_courses",
            size=2,
        )
Exemple #10
0
    def test_search_courses_by_terms_organizations(self, mock_search, *_):
        """
        Happy path: the consumer is filtering courses by organization ID
        """
        factory = APIRequestFactory()
        request = factory.get(
            "/api/v1.0/courses?organizations=13&organizations=15&limit=2"
        )

        mock_search.return_value = {
            "hits": {"hits": [{"_id": 221}, {"_id": 42}], "total": 29},
            "aggregations": {
                "all_courses": {
                    "organizations": {
                        "organizations": {
                            "buckets": [
                                {"key": "13", "doc_count": 21},
                                {"key": "15", "doc_count": 13},
                            ]
                        }
                    },
                    "subjects": {
                        "subjects": {
                            "buckets": [
                                {"key": "12", "doc_count": 3},
                                {"key": "22", "doc_count": 5},
                            ]
                        }
                    },
                }
            },
        }

        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": 0, "total_count": 29},
                "objects": ["Course #221", "Course #42"],
                "facets": {
                    "organizations": {"13": 21, "15": 13},
                    "subjects": {"12": 3, "22": 5},
                },
            },
        )
        # The ES connector was called with appropriate arguments for the client's request
        terms_organizations = {"terms": {"organizations": [13, 15]}}
        mock_search.assert_called_with(
            body={
                "aggs": {
                    "all_courses": {
                        "global": {},
                        "aggregations": {
                            "organizations": {
                                "filter": {"bool": {"must": []}},
                                "aggregations": {
                                    "organizations": {
                                        "terms": {"field": "organizations"}
                                    }
                                },
                            },
                            "subjects": {
                                "filter": {"bool": {"must": [terms_organizations]}},
                                "aggregations": {
                                    "subjects": {"terms": {"field": "subjects"}}
                                },
                            },
                        },
                    }
                },
                "query": {"bool": {"must": [terms_organizations]}},
            },
            doc_type="course",
            from_=0,
            index="richie_courses",
            size=2,
        )
Exemple #11
0
    def test_search_courses_by_match_text(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": {
                    "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": 20, "total_count": 35},
                "objects": ["Course #523", "Course #861"],
                "facets": {"subjects": {"11": 17, "21": 19}},
            },
        )
        # The ES connector was called with appropriate arguments for the client's request
        multi_match = {
            "multi_match": {
                "fields": ["short_description.*", "title.*"],
                "query": "some phrase terms",
                "type": "cross_fields",
            }
        }
        mock_search.assert_called_with(
            body={
                "aggs": {
                    "all_courses": {
                        "global": {},
                        "aggregations": {
                            "organizations": {
                                "filter": {"bool": {"must": [multi_match]}},
                                "aggregations": {
                                    "organizations": {
                                        "terms": {"field": "organizations"}
                                    }
                                },
                            },
                            "subjects": {
                                "filter": {"bool": {"must": [multi_match]}},
                                "aggregations": {
                                    "subjects": {"terms": {"field": "subjects"}}
                                },
                            },
                        },
                    }
                },
                "query": {
                    "bool": {
                        "must": [
                            {
                                "multi_match": {
                                    "fields": ["short_description.*", "title.*"],
                                    "query": "some phrase terms",
                                    "type": "cross_fields",
                                }
                            }
                        ]
                    }
                },
            },
            doc_type="course",
            from_=20,
            index="richie_courses",
            size=2,
        )