Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions search/tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
""" Classes to populate test data """
import copy
from datetime import datetime


class DemoCourse:
""" Class for dispensing demo courses """
DEMO_COURSE_ID = "edX/DemoX/Demo_Course"
DEMO_COURSE = {
"start": datetime(2014, 2, 1),
"number": "DemoX",
"content": {
"short_description": "Short description",
"overview": "Long overview page",
"display_name": "edX Demonstration Course",
"number": "DemoX"
},
"course": "edX/DemoX/Demo_Course",
"image_url": "/c4x/edX/DemoX/asset/images_course_image.jpg",
"effort": "5:30",
"id": DEMO_COURSE_ID,
"enrollment_start": datetime(2014, 1, 1),
}

demo_course_count = 0

@classmethod
def get(cls, update_dict=None, remove_fields=None):
""" get a new demo course """
cls.demo_course_count += 1
course_copy = copy.deepcopy(cls.DEMO_COURSE)
if update_dict:
if "content" in update_dict:
course_copy["content"].update(update_dict["content"])
del update_dict["content"]
course_copy.update(update_dict)
course_copy.update({"id": "{}_{}".format(course_copy["id"], cls.demo_course_count)})
if remove_fields:
for remove_field in remove_fields:
if remove_field in course_copy: # pragma: no cover
del course_copy[remove_field]
return course_copy

@classmethod
def reset_count(cls):
""" go back to zero """
cls.demo_course_count = 0

@staticmethod
def index(searcher, course_info):
""" Adds course info dictionary to the index """
searcher.index(sources=course_info)

@classmethod
def get_and_index(cls, searcher, update_dict=None, remove_fields=None):
""" Adds course info dictionary to the index """
cls.index(searcher, [cls.get(update_dict, remove_fields)])
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
""" High-level view tests"""

from django.test.utils import override_settings

from search.tests.tests import TEST_INDEX_NAME
from search.tests.utils import post_discovery_request
from search.tests.utils import post_discovery_request, setup_democourse
from search.tests.factories import DemoCourse
from .test_views import MockSearchUrlTest
from .test_course_discovery import DemoCourse


@override_settings(ELASTIC_FIELD_MAPPINGS={
Expand All @@ -23,16 +22,7 @@ class DiscoveryUrlTest(MockSearchUrlTest):

def setUp(self):
super().setUp()
DemoCourse.reset_count()
DemoCourse.get_and_index(
self.searcher, {"org": "OrgA", "content": {"short_description": "Find this one with the right parameter"}}
)
DemoCourse.get_and_index(
self.searcher, {"org": "OrgB", "content": {"short_description": "Find this one with another parameter"}}
)
DemoCourse.get_and_index(
self.searcher, {"content": {"short_description": "Find this one somehow"}}
)
setup_democourse(self.searcher)

def test_search_from_url(self):
""" test searching using the url """
Expand Down
155 changes: 155 additions & 0 deletions search/tests/test_api_multi_value.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
""" High-level view tests"""
import uuid
import logging
import ddt

from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse

from search.search_engine_base import SearchEngine
from search.tests.utils import post_discovery_request, setup_meilisearch, setup_elasticsearch, setup_democourse


index_name = f"test_index_{uuid.uuid4().hex}"
logger = logging.getLogger(__name__)


@ddt.ddt
@override_settings(COURSEWARE_CONTENT_INDEX_NAME=index_name, COURSEWARE_INFO_INDEX_NAME=index_name)
class CourseListSearchMultiValueTest(TestCase):
"""
Multi-value tests (/course_list_search/) for both Meilisearch and Elasticsearch engines.
"""
url = reverse("course_list_search")
searcher = ...
wait = ...

def _init_engine(self, config):
"""Helper method to initialize the search engine"""
from django.conf import settings # pylint: disable=import-outside-toplevel
settings.SEARCH_ENGINE = config["search_engine"]
self.searcher = SearchEngine.get_search_engine(settings.COURSEWARE_INFO_INDEX_NAME)
setup_democourse(self.searcher)
self.wait = config["wait"]
self.wait()

def _post(self, params):
"""Helper method to send a post request"""
return post_discovery_request(params, address=self.url)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_search_string(self, label, config): # pylint: disable=unused-argument
"""Tests that keyword search returns correct number of matching documents."""
self._init_engine(config)

code, results = self._post({})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 3)

code, results = self._post({"search_string": "right"})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 1)

code, results = self._post({"search_string": "parameter"})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 2)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_org_filter(self, label, config): # pylint: disable=unused-argument
"""Tests filtering results by the 'org' facet."""
self._init_engine(config)

code, results = self._post({"org": "OrgA"})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 1)
self.assertEqual(results["results"][0]["data"]["org"], "OrgA")

code, results = self._post({"org": "OrgB"})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 1)
self.assertEqual(results["results"][0]["data"]["org"], "OrgB")

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_search_with_pagination(self, label, config): # pylint: disable=unused-argument
"""Tests that pagination limits and offsets results correctly."""
self._init_engine(config)

code, results = self._post({"page_size": 2})
self.assertEqual(code, 200)
self.assertEqual(len(results["results"]), 2)

code, results = self._post({"page_size": 2, "page_index": 1})
self.assertEqual(code, 200)
self.assertEqual(len(results["results"]), 1)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_bad_search_string(self, label, config): # pylint: disable=unused-argument
"""Tests that non-matching search terms return no results."""
self._init_engine(config)

code, results = self._post({"search_string": "doesnotexist123"})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 0)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_no_filters_returns_all_aggregations(self, label, config): # pylint: disable=unused-argument
"""Tests that full facet counts are returned when no filters are applied."""
self._init_engine(config)

code, results = self._post({}) # pylint: disable=unused-variable
aggs = results.get("aggs", {})
self.assertIn("org", aggs)
self.assertIn("language", aggs)
self.assertEqual(aggs["org"]["terms"]["OrgA"], 1)
self.assertEqual(aggs["org"]["terms"]["OrgB"], 1)
self.assertEqual(aggs["org"]["terms"]["OrgC"], 1)
self.assertEqual(aggs["language"]["terms"]["en"], 2)
self.assertEqual(aggs["language"]["terms"]["fr"], 1)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_single_value_filter_keeps_full_facet(self, label, config): # pylint: disable=unused-argument
"""Tests that single-value filters preserve all facet options in aggregations."""
self._init_engine(config)

_, results = self._post({"language": ["en"]})
aggs = results.get("aggs", {})
self.assertIn("fr", aggs["language"]["terms"])

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_multi_value_filter_keeps_full_facet(self, label, config): # pylint: disable=unused-argument
"""Tests that multi-value filters preserve all facet options in aggregations."""
self._init_engine(config)

code, results = self._post({"language": ["en", "fr"]})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 3)

aggs = results.get("aggs", {})
self.assertIn("language", aggs)
self.assertIn("en", aggs["language"]["terms"])
self.assertIn("fr", aggs["language"]["terms"])
self.assertEqual(aggs["language"]["terms"]["en"], 2)
self.assertEqual(aggs["language"]["terms"]["fr"], 1)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_combined_facet_filter_aggregated_correctly(self, label, config): # pylint: disable=unused-argument
"""Tests that combining multiple facet filters returns correct aggregations."""
self._init_engine(config)

code, results = self._post({"language": ["en"], "org": ["OrgA", "OrgC"]})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 2)

aggs = results.get("aggs", {})
self.assertIn("org", aggs)
self.assertIn("OrgA", aggs["org"]["terms"])
self.assertIn("OrgC", aggs["org"]["terms"])
127 changes: 127 additions & 0 deletions search/tests/test_api_single_value.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
""" High-level view tests"""
import uuid
import logging
import ddt

from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse

from search.search_engine_base import SearchEngine
from search.tests.utils import post_discovery_request, setup_meilisearch, setup_elasticsearch, setup_democourse


index_name = f"test_index_{uuid.uuid4().hex}"
logger = logging.getLogger(__name__)


@ddt.ddt
@override_settings(COURSEWARE_CONTENT_INDEX_NAME=index_name, COURSEWARE_INFO_INDEX_NAME=index_name)
class CourseListSearchSingleValueTest(TestCase):
"""
Single-value tests (/course_discovery/) for both Meilisearch and Elasticsearch engines.
"""

url = reverse("course_discovery")
searcher = ...
wait = ...

def _init_engine(self, config):
"""Helper method to initialize the search engine"""
from django.conf import settings # pylint: disable=import-outside-toplevel
settings.SEARCH_ENGINE = config["search_engine"]
self.searcher = SearchEngine.get_search_engine(settings.COURSEWARE_INFO_INDEX_NAME)
setup_democourse(self.searcher)
self.wait = config["wait"]
self.wait()

def _post(self, params):
"""Helper method to send a post request"""
return post_discovery_request(params, address=self.url)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_search_string(self, label, config): # pylint: disable=unused-argument
"""Tests that keyword search returns correct number of matching documents."""
self._init_engine(config)

code, results = self._post({})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 3)

code, results = self._post({"search_string": "right"})
self.assertEqual(results["total"], 1)

code, results = self._post({"search_string": "parameter"})
self.assertEqual(code, 200)
self.assertEqual(results["total"], 2)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_org_filter(self, label, config): # pylint: disable=unused-argument
"""Tests filtering results by the 'org' facet."""
self._init_engine(config)

code, results = self._post({"org": "OrgA"}) # pylint: disable=unused-variable
self.assertEqual(results["total"], 1)

code, results = self._post({"org": "OrgB"})
self.assertEqual(results["total"], 1)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_search_with_pagination(self, label, config): # pylint: disable=unused-argument
"""Tests that pagination limits and offsets results correctly."""
self._init_engine(config)

code, results = self._post({"page_size": 2}) # pylint: disable=unused-variable
self.assertEqual(len(results["results"]), 2)

code, results = self._post({"page_size": 2, "page_index": 1})
self.assertEqual(len(results["results"]), 1)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_bad_search_string(self, label, config): # pylint: disable=unused-argument
"""Tests that non-matching search terms return no results."""
self._init_engine(config)

code, results = self._post({"search_string": "doesnotexist123"}) # pylint: disable=unused-variable
self.assertEqual(results["total"], 0)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_aggregations_basic(self, label, config): # pylint: disable=unused-argument
"""Tests that facet aggregations include all indexed orgs."""
self._init_engine(config)

code, results = self._post({})
self.assertEqual(code, 200)
aggs = results.get("aggs", {})

self.assertIn("org", aggs)
self.assertEqual(aggs["org"]["terms"].get("OrgA", 0), 1)
self.assertEqual(aggs["org"]["terms"].get("OrgB", 0), 1)

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_aggregations_filtered_down(self, label, config): # pylint: disable=unused-argument
"""Tests that aggregations reflect active filters correctly."""
self._init_engine(config)

code, results = self._post({"org": "OrgA"}) # pylint: disable=unused-variable
aggs = results.get("aggs", {})
self.assertIn("org", aggs)
self.assertEqual(aggs["org"]["terms"].get("OrgA", 0), 1)
self.assertNotIn("OrgB", aggs["org"]["terms"])

@ddt.data(("meili", setup_meilisearch(index_name, logger)), ("es", setup_elasticsearch(index_name)))
@ddt.unpack
def test_aggregations_empty_search(self, label, config): # pylint: disable=unused-argument
"""Tests that aggregations are returned even if there are no matches."""
self._init_engine(config)
code, results = self._post({"org": "DoesNotExist"})
self.assertEqual(code, 200)
aggs = results.get("aggs", {})
self.assertIn("org", aggs)
self.assertEqual(aggs["org"]["terms"], {})
Loading