diff --git a/.gitignore b/.gitignore index 6c34b475..887d4d11 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ venv/ .idea/ *.pyc logs.log* -config.json +config.json* .vscode/ .nox/ .python-version diff --git a/datagateway_api/src/common/base_query_filter_factory.py b/datagateway_api/src/common/base_query_filter_factory.py new file mode 100644 index 00000000..147b36ca --- /dev/null +++ b/datagateway_api/src/common/base_query_filter_factory.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractstaticmethod + + +class QueryFilterFactory(ABC): + @abstractstaticmethod + def get_query_filter(request_filter, entity_name=None): # noqa: B902, N805 + """ + Given a filter, return a matching Query filter object + + :param request_filter: The filter to create the QueryFilter for + :type request_filter: :class:`dict` + :param entity_name: Entity name of the endpoint, optional (only used for search + API, not DataGateway API) + :type entity_name: :class:`str` + :return: The QueryFilter object created + """ + pass diff --git a/datagateway_api/src/common/helpers.py b/datagateway_api/src/common/helpers.py index 961ffec3..79d8c7ec 100644 --- a/datagateway_api/src/common/helpers.py +++ b/datagateway_api/src/common/helpers.py @@ -18,7 +18,6 @@ MissingCredentialsError, ) from datagateway_api.src.datagateway_api.database import models -from datagateway_api.src.datagateway_api.query_filter_factory import QueryFilterFactory from datagateway_api.src.resources.entities.entity_endpoint_dict import endpoints log = logging.getLogger() @@ -88,20 +87,42 @@ def is_valid_json(string): return True -def get_filters_from_query_string(): +def get_filters_from_query_string(api_type, entity_name=None): """ Gets a list of filters from the query_strings arg,value pairs, and returns a list of QueryFilter Objects + :param api_type: Type of API this function is being used for i.e. DataGateway API or + Search API + :type api_type: :class:`str` + :param entity_name: Entity name of the endpoint, optional (only used for search + API, not DataGateway API) + :type entity_name: :class:`str` + :raises ApiError: If `api_type` isn't a valid value :return: The list of filters """ + if api_type == "search_api": + from datagateway_api.src.search_api.query_filter_factory import ( + SearchAPIQueryFilterFactory as QueryFilterFactory, + ) + elif api_type == "datagateway_api": + from datagateway_api.src.datagateway_api.query_filter_factory import ( + DataGatewayAPIQueryFilterFactory as QueryFilterFactory, + ) + else: + raise ApiError( + "Incorrect api_type passed into `get_filter_from_query_string(): " + f"{api_type}", + ) log.info(" Getting filters from query string") try: filters = [] for arg in request.args: for value in request.args.getlist(arg): - filters.append( - QueryFilterFactory.get_query_filter({arg: json.loads(value)}), + filters.extend( + QueryFilterFactory.get_query_filter( + {arg: json.loads(value)}, entity_name, + ), ) return filters except Exception as e: diff --git a/datagateway_api/src/datagateway_api/query_filter_factory.py b/datagateway_api/src/datagateway_api/query_filter_factory.py index 65e744a5..bedb4b6c 100644 --- a/datagateway_api/src/datagateway_api/query_filter_factory.py +++ b/datagateway_api/src/datagateway_api/query_filter_factory.py @@ -1,5 +1,6 @@ import logging +from datagateway_api.src.common.base_query_filter_factory import QueryFilterFactory from datagateway_api.src.common.config import Config from datagateway_api.src.common.exceptions import ( ApiError, @@ -9,9 +10,9 @@ log = logging.getLogger() -class QueryFilterFactory(object): +class DataGatewayAPIQueryFilterFactory(QueryFilterFactory): @staticmethod - def get_query_filter(request_filter): + def get_query_filter(request_filter, entity_name=None): """ Given a filter, return a matching Query filter object @@ -22,6 +23,11 @@ def get_query_filter(request_filter): :param request_filter: The filter to create the QueryFilter for :type request_filter: :class:`dict` + :param entity_name: Not utilised in DataGateway API implementation of this + static function, used in the search API. It is part of the method signature + as the same function call (called in `get_filters_from_query_string()`) is + used for both implementations + :type entity_name: :class:`str` :return: The QueryFilter object created :raises ApiError: If the backend type contains an invalid value :raises FilterError: If the filter name is not recognised @@ -57,18 +63,18 @@ def get_query_filter(request_filter): field = list(request_filter[filter_name].keys())[0] operation = list(request_filter[filter_name][field].keys())[0] value = request_filter[filter_name][field][operation] - return WhereFilter(field, value, operation) + return [WhereFilter(field, value, operation)] elif filter_name == "order": field = request_filter["order"].split(" ")[0] direction = request_filter["order"].split(" ")[1] - return OrderFilter(field, direction) + return [OrderFilter(field, direction)] elif filter_name == "skip": - return SkipFilter(request_filter["skip"]) + return [SkipFilter(request_filter["skip"])] elif filter_name == "limit": - return LimitFilter(request_filter["limit"]) + return [LimitFilter(request_filter["limit"])] elif filter_name == "include": - return IncludeFilter(request_filter["include"]) + return [IncludeFilter(request_filter["include"])] elif filter_name == "distinct": - return DistinctFieldFilter(request_filter["distinct"]) + return [DistinctFieldFilter(request_filter["distinct"])] else: raise FilterError(f" Bad filter: {request_filter}") diff --git a/datagateway_api/src/resources/entities/entity_endpoint.py b/datagateway_api/src/resources/entities/entity_endpoint.py index 873ebc2e..45bb2788 100644 --- a/datagateway_api/src/resources/entities/entity_endpoint.py +++ b/datagateway_api/src/resources/entities/entity_endpoint.py @@ -30,7 +30,7 @@ def get(self): backend.get_with_filters( get_session_id_from_auth_header(), entity_type, - get_filters_from_query_string(), + get_filters_from_query_string("datagateway_api"), **kwargs, ), 200, @@ -321,7 +321,7 @@ def get_count_endpoint(name, entity_type, backend, **kwargs): class CountEndpoint(Resource): def get(self): - filters = get_filters_from_query_string() + filters = get_filters_from_query_string("datagateway_api") return ( backend.count_with_filters( get_session_id_from_auth_header(), entity_type, filters, **kwargs, @@ -380,7 +380,7 @@ def get_find_one_endpoint(name, entity_type, backend, **kwargs): class FindOneEndpoint(Resource): def get(self): - filters = get_filters_from_query_string() + filters = get_filters_from_query_string("datagateway_api") return ( backend.get_one_with_filters( get_session_id_from_auth_header(), entity_type, filters, **kwargs, diff --git a/datagateway_api/src/resources/table_endpoints/table_endpoints.py b/datagateway_api/src/resources/table_endpoints/table_endpoints.py index dbd9973c..0ad33d35 100644 --- a/datagateway_api/src/resources/table_endpoints/table_endpoints.py +++ b/datagateway_api/src/resources/table_endpoints/table_endpoints.py @@ -65,7 +65,7 @@ def get(self, id_): backend.get_facility_cycles_for_instrument_with_filters( get_session_id_from_auth_header(), id_, - get_filters_from_query_string(), + get_filters_from_query_string("datagateway_api"), **kwargs, ), 200, @@ -126,7 +126,7 @@ def get(self, id_): backend.get_facility_cycles_for_instrument_count_with_filters( get_session_id_from_auth_header(), id_, - get_filters_from_query_string(), + get_filters_from_query_string("datagateway_api"), **kwargs, ), 200, @@ -202,7 +202,7 @@ def get(self, instrument_id, cycle_id): get_session_id_from_auth_header(), instrument_id, cycle_id, - get_filters_from_query_string(), + get_filters_from_query_string("datagateway_api"), **kwargs, ), 200, @@ -272,7 +272,7 @@ def get(self, instrument_id, cycle_id): get_session_id_from_auth_header(), instrument_id, cycle_id, - get_filters_from_query_string(), + get_filters_from_query_string("datagateway_api"), **kwargs, ), 200, diff --git a/datagateway_api/src/search_api/filters.py b/datagateway_api/src/search_api/filters.py index 7e28ef16..85c2988a 100644 --- a/datagateway_api/src/search_api/filters.py +++ b/datagateway_api/src/search_api/filters.py @@ -1,9 +1,12 @@ +from icat.query import Query + from datagateway_api.src.datagateway_api.icat.filters import ( PythonICATIncludeFilter, PythonICATLimitFilter, PythonICATSkipFilter, PythonICATWhereFilter, ) +from datagateway_api.src.search_api.session_handler import SessionHandler # TODO - Implement each of these filters for Search API, inheriting from the Python ICAT # versions @@ -16,6 +19,22 @@ def __init__(self, field, value, operation): def apply_filter(self, query): return super().apply_filter(query) + def __str__(self): + # TODO - can't just hardcode investigation entity. Might need `icat_entity_name` + # to be passed into init + query = Query(SessionHandler.client, "Investigation") + query.addConditions(self.create_filter()) + str_conds = query.where_clause + str_conds = str_conds.replace("WHERE ", "") + + return str_conds + + def __repr__(self): + return ( + f"Field: '{self.field}', Value: '{self.value}', Operation:" + f" '{self.operation}'" + ) + class SearchAPISkipFilter(PythonICATSkipFilter): def __init__(self, skip_value): diff --git a/datagateway_api/src/search_api/nested_where_filters.py b/datagateway_api/src/search_api/nested_where_filters.py new file mode 100644 index 00000000..a3a38a25 --- /dev/null +++ b/datagateway_api/src/search_api/nested_where_filters.py @@ -0,0 +1,53 @@ +class NestedWhereFilters: + def __init__(self, lhs, rhs, joining_operator): + """ + Class to represent nested conditions that use different boolean operators e.g. + `(A OR B) AND (C OR D)`. This works by joining the two conditions with a boolean + operator + + :param lhs: Left hand side of the condition - either a string condition, WHERE + filter or instance of this class + :type lhs: Any class that has `__str__()` implemented, but use cases will be for + :class:`str` or :class:`SearchAPIWhereFilter` or :class:`NestedWhereFilters` + :param rhs: Right hand side of the condition - either a string condition, WHERE + filter or instance of this class + :type rhs: Any class that has `__str__()` implemented, but use cases will be for + :class:`str` or :class:`SearchAPIWhereFilter` or :class:`NestedWhereFilters` + :param joining_operator: Boolean operator used to join the conditions of `lhs` + `rhs` (e.g. `AND` or `OR`) + :type joining_operator: :class:`str` + """ + + # Ensure each side is in a list for consistency for string conversion + if not isinstance(lhs, list): + lhs = [lhs] + if not isinstance(rhs, list): + rhs = [rhs] + + self.lhs = lhs + self.rhs = rhs + self.joining_operator = joining_operator + + def __str__(self): + """ + Join the condition on the left with the one on the right with the boolean + operator + """ + boolean_algebra_list = [self.lhs, self.rhs] + try: + boolean_algebra_list.remove([None]) + except ValueError: + # If neither side contains `None`, we should continue as normal + pass + + # If either side contains a list of WHERE filter objects, flatten the conditions + conditions = [str(m) for n in (i for i in boolean_algebra_list) for m in n] + operator = f" {self.joining_operator} " + + return f"({operator.join(conditions)})" + + def __repr__(self): + return ( + f"LHS: {repr(self.lhs)}, RHS: {repr(self.rhs)}, Operator:" + f" {repr(self.joining_operator)}" + ) diff --git a/datagateway_api/src/search_api/query_filter_factory.py b/datagateway_api/src/search_api/query_filter_factory.py new file mode 100644 index 00000000..b53a28d8 --- /dev/null +++ b/datagateway_api/src/search_api/query_filter_factory.py @@ -0,0 +1,284 @@ +import logging + +from datagateway_api.src.common.base_query_filter_factory import QueryFilterFactory +from datagateway_api.src.common.exceptions import FilterError +from datagateway_api.src.search_api.filters import ( + SearchAPIIncludeFilter, + SearchAPILimitFilter, + SearchAPISkipFilter, + SearchAPIWhereFilter, +) +from datagateway_api.src.search_api.nested_where_filters import NestedWhereFilters + +log = logging.getLogger() + + +class SearchAPIQueryFilterFactory(QueryFilterFactory): + @staticmethod + def get_query_filter(request_filter, entity_name=None): + """ + Given a filter, return a list of matching query filter objects + + :param request_filter: The filter from which to create a list of query filter + objects + :type request_filter: :class:`dict` + :param entity_name: Entity name of the endpoint or the name of the included + entity - this is needed for when there is a text operator inside a where + filter + :type entity_name: :class:`str` + :return: The list of query filter objects created + :raises FilterError: If the filter name is not recognised + """ + query_param_name = list(request_filter)[0].lower() + query_filters = [] + + if query_param_name == "filter": + log.debug("Filter: %s", request_filter["filter"]) + for filter_name, filter_input in request_filter["filter"].items(): + if filter_name == "where": + query_filters.extend( + SearchAPIQueryFilterFactory.get_where_filter( + filter_input, entity_name, + ), + ) + elif filter_name == "include": + query_filters.extend( + SearchAPIQueryFilterFactory.get_include_filter(filter_input), + ) + elif filter_name == "limit": + query_filters.append(SearchAPILimitFilter(filter_input)) + elif filter_name == "skip": + query_filters.append(SearchAPISkipFilter(filter_input)) + else: + raise FilterError( + "No valid filter name given within filter query param", + ) + elif query_param_name == "where": + # For the count endpoints + query_filters.extend( + SearchAPIQueryFilterFactory.get_query_filter( + {"filter": request_filter}, entity_name, + ), + ) + else: + raise FilterError( + f"Bad filter, please check query parameters: {request_filter}", + ) + + return query_filters + + @staticmethod + def get_where_filter(where_filter_input, entity_name): + """ + Given a where filter input, return a list of `NestedWhereFilters` and/ or + `SearchAPIWhereFilter` objects + + `NestedWhereFilters` objects are created when there is an AND or OR inside the + where filter input, otherwise `SearchAPIWhereFilter` objects are created. If + there is a text operator inside the where filter input then the number of + `SearchAPIWhereFilter` objects that will be created depends on the number of + text operator fields that will be matched for the provided entity. + + :param where_filter_input: The filter from which to create a list of + `NestedWhereFilters` and/ or `SearchAPIWhereFilter` objects + :type where_filter_input: :class:`dict` + :param entity_name: Entity name of the endpoint or the name of the included + entity - this is needed for when there is a text operator inside a where + filter so that the value provided can be matched with the relevant text + operator fields for the entity. + :type entity_name: :class:`str` + :return: The list of `NestedWhereFilters` and/ or `SearchAPIWhereFilter` objects + created + """ + where_filters = [] + if ( + list(where_filter_input.keys())[0] == "and" + or list(where_filter_input.keys())[0] == "or" + ): + boolean_operator = list(where_filter_input.keys())[0] + conditions = list(where_filter_input.values())[0] + conditional_where_filters = [] + + for condition in conditions: + # Could be nested AND/OR + where_filter = { + "filter": {"where": condition}, + } + conditional_where_filters.extend( + SearchAPIQueryFilterFactory.get_query_filter( + where_filter, entity_name, + ), + ) + + nested = NestedWhereFilters( + conditional_where_filters[:-1], + conditional_where_filters[-1], + boolean_operator, + ) + where_filters.append(nested) + elif list(where_filter_input.keys())[0] == "text": + # TODO - we might want to move this to the data + # definitions at a later point + text_operator_fields = { + "datasets": ["title"], + "documents": ["title", "summary"], + "files": ["name"], + "instrument": ["name", "facility"], + "samples": ["name", "description"], + "techniques": ["name"], + } + + try: + or_conditional_filters = [] + field_names = text_operator_fields[entity_name] + for field_name in field_names: + or_conditional_filters.append( + {field_name: {"like": where_filter_input["text"]}}, + ) + + where_filter = { + "filter": {"where": {"or": or_conditional_filters}}, + } + where_filters.extend( + SearchAPIQueryFilterFactory.get_query_filter( + where_filter, entity_name, + ), + ) + except KeyError: + # Do not raise FilterError nor attempt to create filters. Simply + # ignore text operator queries on fields that are not part of the + # text_operator_fields dict. + pass + else: + filter_data = SearchAPIQueryFilterFactory.get_condition_values( + where_filter_input, + ) + where_filters.append( + SearchAPIWhereFilter( + field=filter_data[0], + value=filter_data[1], + operation=filter_data[2], + ), + ) + + return where_filters + + @staticmethod + def get_include_filter(include_filter_input): + """ + Given an include filter input, return a list of `SearchAPIIncludeFilter` and any + `NestedWhereFilters` and/ or `SearchAPIWhereFilter` objects if there is a scope + filter inside the filter input + + Currently, we do not support limit and skip filters inside scope filters that + are part of include filters. + + :param include_filter_input: The filter from which to create a list of + `SearchAPIIncludeFilter` and any `NestedWhereFilters` and/ or + `SearchAPIWhereFilter` objects + :type include_filter_input: :class:`dict` + :return: The list of `SearchAPIIncludeFilter` and any `NestedWhereFilters` and/ + or `SearchAPIWhereFilter` objects created + :raises FilterError: If scope filter has a limit or skip filter + """ + query_filters = [] + for related_model in include_filter_input: + included_entity = related_model["relation"] + + nested_include = False + if "scope" in related_model: + if "limit" in related_model["scope"]: + raise FilterError( + "Bad Include filter: Scope filter cannot have a limit filter", + ) + if "skip" in related_model["scope"]: + raise FilterError( + "Bad Include filter: Scope filter cannot have a skip filter", + ) + + # Scope filter can have WHERE and/ or INCLUDE filters + scope_query_filters = SearchAPIQueryFilterFactory.get_query_filter( + {"filter": related_model["scope"]}, included_entity, + ) + + for scope_query_filter in scope_query_filters: + if isinstance( + scope_query_filter, (NestedWhereFilters, SearchAPIWhereFilter), + ): + SearchAPIQueryFilterFactory.prefix_where_filter_field_with_entity_name( # noqa: B950 + scope_query_filter, included_entity, + ) + if isinstance(scope_query_filter, SearchAPIIncludeFilter): + nested_include = True + included_filter = scope_query_filter.included_filters[0] + + scope_query_filter.included_filters[ + 0 + ] = f"{included_entity}.{included_filter}" + + query_filters.extend(scope_query_filters) + + if not nested_include: + query_filters.append(SearchAPIIncludeFilter(included_entity)) + + return query_filters + + @staticmethod + def get_condition_values(conditions_dict): + """ + Given a simplified where filter input, return a field name, value and operation + as a tuple + + :param conditions_dict: The filter from which to return a field name, value and + operation + :type conditions_dict: :class:`dict` + :return: The tuple that includes field name, value and operation + """ + field = list(conditions_dict.keys())[0] + filter_data = list(conditions_dict.values())[0] + + if isinstance(filter_data, str): + # Format: {"where": {"property": "value"}} + value = conditions_dict[field] + operation = "eq" + elif isinstance(filter_data, dict): + # Format: {"where": {"property": {"operator": "value"}}} + value = list(conditions_dict[field].values())[0] + operation = list(conditions_dict[field].keys())[0] + + return field, value, operation + + @staticmethod + def prefix_where_filter_field_with_entity_name(where_filters, entity_name): + """ + Given a `NestedWhereFilters` or `SearchAPIWhereFilter` object, or a list of + these objects, prefix the field attribute of the `SearchAPIWhereFilter` object + with the provided entity name + + When dealing with `NestedWhereFilters`, this function is called recursively in + order to drill down and get hold of the `SearchAPIWhereFilter` objects so that + their field attributes can be prefixed. The field attributes of are prefixed + only if the where filters are part of a scope filter that is inside an include + filter. This is done to make it clear that the where filter is related to the + included entity rather than the endpoint entity. + + :param where_filters: The filter(s) whose field attribute(s) require(s) + prefixing + :type where_filters: :class:`NestedWhereFilters` or `SearchAPIWhereFilter`, or a + :class:`list` of :class:`NestedWhereFilters` and/ or `SearchAPIWhereFilter` + :param entity_name: The name of the included entity to prefix the where filter + field with + :type entity_name: :class:`str` + """ + if not isinstance(where_filters, list): + where_filters = [where_filters] + + for where_filter in where_filters: + if isinstance(where_filter, NestedWhereFilters): + nested_where_filters = where_filter.lhs + where_filter.rhs + for nested_where_filter in nested_where_filters: + SearchAPIQueryFilterFactory.prefix_where_filter_field_with_entity_name( # noqa: B950 + nested_where_filter, entity_name, + ) + if isinstance(where_filter, SearchAPIWhereFilter): + where_filter.field = f"{entity_name}.{where_filter.field}" diff --git a/test/datagateway_api/db/test_query_filter_factory.py b/test/datagateway_api/db/test_query_filter_factory.py index 8c5e59b7..afd26203 100644 --- a/test/datagateway_api/db/test_query_filter_factory.py +++ b/test/datagateway_api/db/test_query_filter_factory.py @@ -8,16 +8,20 @@ DatabaseSkipFilter, DatabaseWhereFilter, ) -from datagateway_api.src.datagateway_api.query_filter_factory import QueryFilterFactory +from datagateway_api.src.datagateway_api.query_filter_factory import ( + DataGatewayAPIQueryFilterFactory, +) -class TestQueryFilterFactory: +# TODO - Move outside of db/ +class TestDataGatewayAPIQueryFilterFactory: @pytest.mark.usefixtures("flask_test_app_db") def test_valid_distinct_filter(self): - assert isinstance( - QueryFilterFactory.get_query_filter({"distinct": "TEST"}), - DatabaseDistinctFieldFilter, + test_filter = DataGatewayAPIQueryFilterFactory.get_query_filter( + {"distinct": "TEST"}, ) + assert isinstance(test_filter[0], DatabaseDistinctFieldFilter) + assert len(test_filter) == 1 @pytest.mark.usefixtures("flask_test_app_db") @pytest.mark.parametrize( @@ -32,28 +36,29 @@ def test_valid_distinct_filter(self): ], ) def test_valid_include_filter(self, filter_input): - assert isinstance( - QueryFilterFactory.get_query_filter(filter_input), DatabaseIncludeFilter, - ) + test_filter = DataGatewayAPIQueryFilterFactory.get_query_filter(filter_input) + assert isinstance(test_filter[0], DatabaseIncludeFilter) + assert len(test_filter) == 1 @pytest.mark.usefixtures("flask_test_app_db") def test_valid_limit_filter(self): - assert isinstance( - QueryFilterFactory.get_query_filter({"limit": 10}), DatabaseLimitFilter, - ) + test_filter = DataGatewayAPIQueryFilterFactory.get_query_filter({"limit": 10}) + assert isinstance(test_filter[0], DatabaseLimitFilter) + assert len(test_filter) == 1 @pytest.mark.usefixtures("flask_test_app_db") def test_valid_order_filter(self): - assert isinstance( - QueryFilterFactory.get_query_filter({"order": "id DESC"}), - DatabaseOrderFilter, + test_filter = DataGatewayAPIQueryFilterFactory.get_query_filter( + {"order": "id DESC"}, ) + assert isinstance(test_filter[0], DatabaseOrderFilter) + assert len(test_filter) == 1 @pytest.mark.usefixtures("flask_test_app_db") def test_valid_skip_filter(self): - assert isinstance( - QueryFilterFactory.get_query_filter({"skip": 10}), DatabaseSkipFilter, - ) + test_filter = DataGatewayAPIQueryFilterFactory.get_query_filter({"skip": 10}) + assert isinstance(test_filter[0], DatabaseSkipFilter) + assert len(test_filter) == 1 @pytest.mark.usefixtures("flask_test_app_db") @pytest.mark.parametrize( @@ -70,6 +75,6 @@ def test_valid_skip_filter(self): ], ) def test_valid_where_filter(self, filter_input): - assert isinstance( - QueryFilterFactory.get_query_filter(filter_input), DatabaseWhereFilter, - ) + test_filter = DataGatewayAPIQueryFilterFactory.get_query_filter(filter_input) + assert isinstance(test_filter[0], DatabaseWhereFilter) + assert len(test_filter) == 1 diff --git a/test/search_api/test_nested_where_filters.py b/test/search_api/test_nested_where_filters.py new file mode 100644 index 00000000..f4c851f9 --- /dev/null +++ b/test/search_api/test_nested_where_filters.py @@ -0,0 +1,151 @@ +import pytest + +from datagateway_api.src.search_api.filters import SearchAPIWhereFilter +from datagateway_api.src.search_api.nested_where_filters import NestedWhereFilters + + +class TestNestedWhereFilters: + @pytest.mark.parametrize( + "lhs, rhs, joining_operator, expected_where_clause", + [ + pytest.param("A", None, "AND", "(A)", id="LHS (A) w/ misc. AND"), + pytest.param("A", None, "OR", "(A)", id="LHS (A) w/ misc. OR"), + pytest.param([], "A", "AND", "(A)", id="RHS (A) w/ misc. AND"), + pytest.param([], "A", "OR", "(A)", id="RHS (A) w/ misc. OR"), + pytest.param("A", "B", "AND", "(A AND B)", id="(A AND B)"), + pytest.param("A", "B", "OR", "(A OR B)", id="(A OR B)"), + pytest.param( + "(A AND B)", + "(C AND D)", + "AND", + "((A AND B) AND (C AND D))", + id="((A AND B) AND (C AND D))", + ), + pytest.param( + "(A AND B)", + "(C AND D)", + "OR", + "((A AND B) OR (C AND D))", + id="((A AND B) OR (C AND D))", + ), + pytest.param( + "(A OR B)", + "(C OR D)", + "OR", + "((A OR B) OR (C OR D))", + id="((A OR B) OR (C OR D))", + ), + pytest.param( + "(A OR B)", + "(C OR D)", + "AND", + "((A OR B) AND (C OR D))", + id="((A OR B) AND (C OR D))", + ), + pytest.param( + "(A AND B AND C) OR (C AND D)", + "(E AND F)", + "OR", + "((A AND B AND C) OR (C AND D) OR (E AND F))", + id="((A AND B AND C) OR (C AND D) OR (E AND F))", + ), + pytest.param( + "(A AND B AND C) AND (C OR D)", + "(E AND F)", + "OR", + "((A AND B AND C) AND (C OR D) OR (E AND F))", + id="((A AND B AND C) AND (C OR D)) OR (E AND F))", + ), + pytest.param( + "((A AND B AND C) AND (C OR D))", + "(E AND F)", + "OR", + "(((A AND B AND C) AND (C OR D)) OR (E AND F))", + id="(((A AND B AND C) AND (C OR D))) OR (E AND F))", + ), + ], + ) + def test_str_filters(self, lhs, rhs, joining_operator, expected_where_clause): + test_nest = NestedWhereFilters(lhs, rhs, joining_operator) + + where_clause = str(test_nest) + + assert where_clause == expected_where_clause + + @pytest.mark.parametrize( + "lhs, rhs, joining_operator, expected_where_clause", + [ + pytest.param( + SearchAPIWhereFilter("name", "test name", "eq"), + SearchAPIWhereFilter("id", 3, "eq"), + "OR", + "(o.name = 'test name' OR o.id = '3')", + id="(o.name = 'test name' OR o.id = '3')", + ), + pytest.param( + [SearchAPIWhereFilter("name", "test name", "eq")], + SearchAPIWhereFilter("id", 3, "eq"), + "OR", + "(o.name = 'test name' OR o.id = '3')", + id="Single filter list in LHS", + ), + pytest.param( + [SearchAPIWhereFilter("name", "test name", "eq")], + [SearchAPIWhereFilter("id", 3, "eq")], + "OR", + "(o.name = 'test name' OR o.id = '3')", + id="Single filter list in LHS and RHS", + ), + pytest.param( + [ + SearchAPIWhereFilter("name", "test name", "eq"), + SearchAPIWhereFilter("id", 10, "lt"), + ], + [SearchAPIWhereFilter("id", 3, "gt")], + "AND", + "(o.name = 'test name' AND o.id < '10' AND o.id > '3')", + id="Multiple filters on LHS", + ), + pytest.param( + [ + SearchAPIWhereFilter("name", "test name", "eq"), + SearchAPIWhereFilter("id", 10, "lt"), + ], + [ + SearchAPIWhereFilter("id", 3, "gt"), + SearchAPIWhereFilter("doi", "Test DOI", "like"), + ], + "AND", + "(o.name = 'test name' AND o.id < '10' AND o.id > '3' AND o.doi like" + " '%Test DOI%')", + id="Multiple filters on LHS and RHS", + ), + ], + ) + def test_search_api_filters( + self, lhs, rhs, joining_operator, expected_where_clause, + ): + # TODO - Is creating clients causing this to be slow? Test once session handler + # work merged + test_nest = NestedWhereFilters(lhs, rhs, joining_operator) + where_clause = str(test_nest) + assert where_clause == expected_where_clause + + @pytest.mark.parametrize( + "lhs, rhs, joining_operator, expected_where_clause", + [ + pytest.param( + NestedWhereFilters("A", "B", "OR"), + NestedWhereFilters("C", "D", "AND"), + "OR", + "((A OR B) OR (C AND D))", + id="((A OR B) OR (C AND D))", + ), + ], + ) + def test_nested_classes( + self, lhs, rhs, joining_operator, expected_where_clause, + ): + test_nest = NestedWhereFilters(lhs, rhs, joining_operator) + where_clause = str(test_nest) + assert where_clause == expected_where_clause diff --git a/test/search_api/test_search_api_query_filter_factory.py b/test/search_api/test_search_api_query_filter_factory.py new file mode 100644 index 00000000..9af76fa9 --- /dev/null +++ b/test/search_api/test_search_api_query_filter_factory.py @@ -0,0 +1,2028 @@ +import pytest + +from datagateway_api.src.common.exceptions import FilterError +from datagateway_api.src.search_api.filters import ( + SearchAPIIncludeFilter, + SearchAPILimitFilter, + SearchAPISkipFilter, + SearchAPIWhereFilter, +) +from datagateway_api.src.search_api.nested_where_filters import NestedWhereFilters +from datagateway_api.src.search_api.query_filter_factory import ( + SearchAPIQueryFilterFactory, +) + + +class TestSearchAPIQueryFilterFactory: + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_where", + [ + pytest.param( + {"filter": {"where": {"title": "My Title"}}}, + "documents", + SearchAPIWhereFilter("title", "My Title", "eq"), + id="Property value with no operator", + ), + pytest.param( + {"filter": {"where": {"summary": {"like": "My Test Summary"}}}}, + "documents", + SearchAPIWhereFilter("summary", "My Test Summary", "like"), + id="Property value with operator", + ), + pytest.param( + {"where": {"summary": {"like": "My Test Summary"}}}, + "documents", + SearchAPIWhereFilter("summary", "My Test Summary", "like"), + id="WHERE filter in syntax for count endpoints", + ), + ], + ) + def test_valid_where_filter( + self, test_request_filter, test_entity_name, expected_where, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == 1 + assert repr(filters[0]) == repr(expected_where) + + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_lhs, expected_rhs" + ", expected_joining_operator", + [ + pytest.param( + {"filter": {"where": {"text": "Dataset 1"}}}, + "datasets", + [], + [SearchAPIWhereFilter("title", "Dataset 1", "like")], + "or", + id="Text operator on dataset", + ), + pytest.param( + {"filter": {"where": {"text": "Instrument 1"}}}, + "instrument", + [SearchAPIWhereFilter("name", "Instrument 1", "like")], + [SearchAPIWhereFilter("facility", "Instrument 1", "like")], + "or", + id="Text operator on instrument", + ), + ], + ) + def test_valid_where_filter_text_operator( + self, + test_request_filter, + test_entity_name, + expected_lhs, + expected_rhs, + expected_joining_operator, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == 1 + assert isinstance(filters[0], NestedWhereFilters) + assert repr(filters[0].lhs) == repr(expected_lhs) + assert repr(filters[0].rhs) == repr(expected_rhs) + assert filters[0].joining_operator == expected_joining_operator + + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_lhs, expected_rhs" + ", expected_joining_operator", + [ + pytest.param( + {"filter": {"where": {"and": [{"summary": "My Test Summary"}]}}}, + "documents", + [], + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + "and", + id="Single condition, property value with no operator", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + {"summary": "My Test Summary"}, + {"title": "Test title"}, + ], + }, + }, + }, + "documents", + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "eq")], + "and", + id="Multiple conditions (two), property values with no operator", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + {"summary": "My Test Summary"}, + {"title": "Test title"}, + {"type": "Test type"}, + ], + }, + }, + }, + "documents", + [ + SearchAPIWhereFilter("summary", "My Test Summary", "eq"), + SearchAPIWhereFilter("title", "Test title", "eq"), + ], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "and", + id="Multiple conditions (three), property values with no operator", + ), + pytest.param( + {"filter": {"where": {"and": [{"value": {"lt": 50}}]}}}, + "parameters", + [], + [SearchAPIWhereFilter("value", 50, "lt")], + "and", + id="Single condition, property value with operator", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + {"name": {"like": "Test name"}}, + {"value": {"gte": 275}}, + ], + }, + }, + }, + "parameters", + [SearchAPIWhereFilter("name", "Test name", "like")], + [SearchAPIWhereFilter("value", 275, "gte")], + "and", + id="Multiple conditions (two), property values with operator", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + {"name": {"like": "Test name"}}, + {"value": {"gte": 275}}, + {"unit": {"nlike": "Test unit"}}, + ], + }, + }, + }, + "parameters", + [ + SearchAPIWhereFilter("name", "Test name", "like"), + SearchAPIWhereFilter("value", 275, "gte"), + ], + [SearchAPIWhereFilter("unit", "Test unit", "nlike")], + "and", + id="Multiple conditions (three), property values with operator", + ), + pytest.param( + {"filter": {"where": {"and": [{"text": "Dataset 1"}]}}}, + "datasets", + [], + [ + NestedWhereFilters( + [], SearchAPIWhereFilter("title", "Dataset 1", "like"), "or", + ), + ], + "and", + id="Single condition, text operator on dataset", + ), + pytest.param( + {"filter": {"where": {"and": [{"text": "Instrument 1"}]}}}, + "instrument", + [], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("name", "Instrument 1", "like")], + [SearchAPIWhereFilter("facility", "Instrument 1", "like")], + "or", + ), + ], + "and", + id="Single condition, text operator on instrument", + ), + pytest.param( + { + "filter": { + "where": {"and": [{"text": "Dataset 1"}, {"pid": "Test pid"}]}, + }, + }, + "datasets", + [ + NestedWhereFilters( + [], [SearchAPIWhereFilter("title", "Dataset 1", "like")], "or", + ), + ], + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + "and", + id="Multiple conditions (two), text operator on dataset and " + "property value with no operator", + ), + pytest.param( + { + "filter": { + "where": { + "and": [{"text": "Instrument 1"}, {"pid": "Test pid"}], + }, + }, + }, + "instrument", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("name", "Instrument 1", "like")], + [SearchAPIWhereFilter("facility", "Instrument 1", "like")], + "or", + ), + ], + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + "and", + id="Multiple conditions (two), text operator on instrument and " + "property value with no operator", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + {"text": "Dataset 1"}, + {"pid": {"eq": "Test pid"}}, + ], + }, + }, + }, + "datasets", + [ + NestedWhereFilters( + [], [SearchAPIWhereFilter("title", "Dataset 1", "like")], "or", + ), + ], + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + "and", + id="Multiple conditions (two), text operator on dataset and " + "property value with operator", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + {"text": "Instrument 1"}, + {"pid": {"eq": "Test pid"}}, + ], + }, + }, + }, + "instrument", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("name", "Instrument 1", "like")], + [SearchAPIWhereFilter("facility", "Instrument 1", "like")], + "or", + ), + ], + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + "and", + id="Multiple conditions (two), text operator on instrument and " + "property value with operator", + ), + ], + ) + def test_valid_where_filter_with_and_boolean_operator( + self, + test_request_filter, + test_entity_name, + expected_lhs, + expected_rhs, + expected_joining_operator, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == 1 + assert isinstance(filters[0], NestedWhereFilters) + assert repr(filters[0].lhs) == repr(expected_lhs) + assert repr(filters[0].rhs) == repr(expected_rhs) + assert filters[0].joining_operator == expected_joining_operator + + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_lhs, expected_rhs" + ", expected_joining_operator", + [ + pytest.param( + {"filter": {"where": {"or": [{"summary": "My Test Summary"}]}}}, + "documents", + [], + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + "or", + id="Single condition, property value with no operator", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + {"summary": "My Test Summary"}, + {"title": "Test title"}, + ], + }, + }, + }, + "documents", + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "eq")], + "or", + id="Multiple conditions (two), property values with no operator", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + {"summary": "My Test Summary"}, + {"title": "Test title"}, + {"type": "Test type"}, + ], + }, + }, + }, + "documents", + [ + SearchAPIWhereFilter("summary", "My Test Summary", "eq"), + SearchAPIWhereFilter("title", "Test title", "eq"), + ], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "or", + id="Multiple conditions (three), property values with no operator", + ), + pytest.param( + {"filter": {"where": {"or": [{"value": {"lt": 50}}]}}}, + "parameters", + [], + [SearchAPIWhereFilter("value", 50, "lt")], + "or", + id="Single condition, property value with operator", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + {"name": {"like": "Test name"}}, + {"value": {"gte": 275}}, + ], + }, + }, + }, + "parameters", + [SearchAPIWhereFilter("name", "Test name", "like")], + [SearchAPIWhereFilter("value", 275, "gte")], + "or", + id="Multiple conditions (two), property values with operator", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + {"name": {"like": "Test name"}}, + {"value": {"gte": 275}}, + {"unit": {"nlike": "Test unit"}}, + ], + }, + }, + }, + "parameters", + [ + SearchAPIWhereFilter("name", "Test name", "like"), + SearchAPIWhereFilter("value", 275, "gte"), + ], + [SearchAPIWhereFilter("unit", "Test unit", "nlike")], + "or", + id="Multiple conditions (three), property values with operator", + ), + pytest.param( + {"filter": {"where": {"or": [{"text": "Dataset 1"}]}}}, + "datasets", + [], + [ + NestedWhereFilters( + [], SearchAPIWhereFilter("title", "Dataset 1", "like"), "or", + ), + ], + "or", + id="Single condition, text operator on dataset", + ), + pytest.param( + {"filter": {"where": {"or": [{"text": "Instrument 1"}]}}}, + "instrument", + [], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("name", "Instrument 1", "like")], + [SearchAPIWhereFilter("facility", "Instrument 1", "like")], + "or", + ), + ], + "or", + id="Single condition, text operator on instrument", + ), + pytest.param( + { + "filter": { + "where": {"or": [{"text": "Dataset 1"}, {"pid": "Test pid"}]}, + }, + }, + "datasets", + [ + NestedWhereFilters( + [], [SearchAPIWhereFilter("title", "Dataset 1", "like")], "or", + ), + ], + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + "or", + id="Multiple conditions (two), text operator on dataset and " + "property value with no operator", + ), + pytest.param( + { + "filter": { + "where": { + "or": [{"text": "Instrument 1"}, {"pid": "Test pid"}], + }, + }, + }, + "instrument", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("name", "Instrument 1", "like")], + [SearchAPIWhereFilter("facility", "Instrument 1", "like")], + "or", + ), + ], + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + "or", + id="Multiple conditions (two), text operator on instrument and " + "property value with no operator", + ), + pytest.param( + { + "filter": { + "where": { + "or": [{"text": "Dataset 1"}, {"pid": {"eq": "Test pid"}}], + }, + }, + }, + "datasets", + [ + NestedWhereFilters( + [], [SearchAPIWhereFilter("title", "Dataset 1", "like")], "or", + ), + ], + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + "or", + id="Multiple conditions (two), text operator on dataset and " + "property value with operator", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + {"text": "Instrument 1"}, + {"pid": {"eq": "Test pid"}}, + ], + }, + }, + }, + "instrument", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("name", "Instrument 1", "like")], + [SearchAPIWhereFilter("facility", "Instrument 1", "like")], + "or", + ), + ], + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + "or", + id="Multiple conditions (two), text operator on instrument and " + "property value with operator", + ), + ], + ) + def test_valid_where_filter_with_or_boolean_operator( + self, + test_request_filter, + test_entity_name, + expected_lhs, + expected_rhs, + expected_joining_operator, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == 1 + assert isinstance(filters[0], NestedWhereFilters) + assert repr(filters[0].lhs) == repr(expected_lhs) + assert repr(filters[0].rhs) == repr(expected_rhs) + assert filters[0].joining_operator == expected_joining_operator + + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_lhs, expected_rhs" + ", expected_joining_operator", + [ + pytest.param( + { + "filter": { + "where": { + "and": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "and": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "and", + ), + ], + "and", + id="With two AND boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "or": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "or", + ), + ], + "and", + id="With AND and OR boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + { + "or": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "or": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "or", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "or", + ), + ], + "and", + id="With two OR boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "and": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "and": [ + {"doi": "Test doi"}, + {"license": {"like": "Test license"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "and", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("doi", "Test doi", "eq")], + [SearchAPIWhereFilter("license", "Test license", "like")], + "and", + ), + ], + "and", + id="With three AND boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "and": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "or": [ + {"doi": "Test doi"}, + {"license": {"like": "Test license"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "and", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("doi", "Test doi", "eq")], + [SearchAPIWhereFilter("license", "Test license", "like")], + "or", + ), + ], + "and", + id="With two AND and one OR boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "or": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "or": [ + {"doi": "Test doi"}, + {"license": {"like": "Test license"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "or", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("doi", "Test doi", "eq")], + [SearchAPIWhereFilter("license", "Test license", "like")], + "or", + ), + ], + "and", + id="With one AND and two OR boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + { + "or": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "or": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "or": [ + {"doi": "Test doi"}, + {"license": {"like": "Test license"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "or", + ), + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "or", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("doi", "Test doi", "eq")], + [SearchAPIWhereFilter("license", "Test license", "like")], + "or", + ), + ], + "and", + id="With three OR boolean operators", + ), + ], + ) + def test_valid_where_filter_with_nested_and_boolean_operator( + self, + test_request_filter, + test_entity_name, + expected_lhs, + expected_rhs, + expected_joining_operator, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == 1 + assert isinstance(filters[0], NestedWhereFilters) + assert repr(filters[0].lhs) == repr(expected_lhs) + assert repr(filters[0].rhs) == repr(expected_rhs) + assert filters[0].joining_operator == expected_joining_operator + + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_lhs, expected_rhs" + ", expected_joining_operator", + [ + pytest.param( + { + "filter": { + "where": { + "or": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "and": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "and", + ), + ], + "or", + id="With two AND boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "or": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "or", + ), + ], + "or", + id="With AND and OR boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + { + "or": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "or": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "or", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "or", + ), + ], + "or", + id="With two OR boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "and": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "and": [ + {"doi": "Test doi"}, + {"license": {"like": "Test license"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "and", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("doi", "Test doi", "eq")], + [SearchAPIWhereFilter("license", "Test license", "like")], + "and", + ), + ], + "or", + id="With three AND boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "and": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "or": [ + {"doi": "Test doi"}, + {"license": {"like": "Test license"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "and", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("doi", "Test doi", "eq")], + [SearchAPIWhereFilter("license", "Test license", "like")], + "or", + ), + ], + "or", + id="With two AND and one OR boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "or": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "or": [ + {"doi": "Test doi"}, + {"license": {"like": "Test license"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "or", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("doi", "Test doi", "eq")], + [SearchAPIWhereFilter("license", "Test license", "like")], + "or", + ), + ], + "or", + id="With one AND and two OR boolean operators", + ), + pytest.param( + { + "filter": { + "where": { + "or": [ + { + "or": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "or": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "or": [ + {"doi": "Test doi"}, + {"license": {"like": "Test license"}}, + ], + }, + ], + }, + }, + }, + "documents", + [ + NestedWhereFilters( + [SearchAPIWhereFilter("summary", "My Test Summary", "eq")], + [SearchAPIWhereFilter("title", "Test title", "like")], + "or", + ), + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "or", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("doi", "Test doi", "eq")], + [SearchAPIWhereFilter("license", "Test license", "like")], + "or", + ), + ], + "or", + id="With three OR boolean operators", + ), + ], + ) + def test_valid_where_filter_with_nested_or_boolean_operator( + self, + test_request_filter, + test_entity_name, + expected_lhs, + expected_rhs, + expected_joining_operator, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == 1 + assert isinstance(filters[0], NestedWhereFilters) + assert repr(filters[0].lhs) == repr(expected_lhs) + assert repr(filters[0].rhs) == repr(expected_rhs) + assert filters[0].joining_operator == expected_joining_operator + + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_length" + ", expected_included_entities", + [ + pytest.param( + {"filter": {"include": [{"relation": "files"}]}}, + "datasets", + 1, + [["files"]], + id="Single related model", + ), + pytest.param( + { + "filter": { + "include": [{"relation": "files"}, {"relation": "instrument"}], + }, + }, + "datasets", + 2, + [["files"], ["instrument"]], + id="Multiple related models", + ), + ], + ) + def test_valid_include_filter( + self, + test_request_filter, + test_entity_name, + expected_length, + expected_included_entities, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == expected_length + + for test_filter, included_entities in zip(filters, expected_included_entities): + if isinstance(test_filter, SearchAPIIncludeFilter): + assert test_filter.included_filters == included_entities + + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_length" + ", expected_included_entities, expected_where_filter_data" + ", expected_nested_wheres", + [ + pytest.param( + { + "filter": { + "include": [ + { + "relation": "parameters", + "scope": {"where": {"name": "My parameter"}}, + }, + ], + }, + }, + "datasets", + 2, + [["parameters"]], + [SearchAPIWhereFilter("parameters.name", "My parameter", "eq")], + "", + id="Property value with no operator", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "parameters", + "scope": {"where": {"name": {"ne": "My parameter"}}}, + }, + ], + }, + }, + "datasets", + 2, + [["parameters"]], + [SearchAPIWhereFilter("parameters.name", "My parameter", "ne")], + "", + id="Property value with operator", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "files", + "scope": {"where": {"text": "file1"}}, + }, + ], + }, + }, + "datasets", + 2, + [["files"]], + [], + [ + NestedWhereFilters( + [], [SearchAPIWhereFilter("files.name", "file1", "like")], "or", + ), + ], + id="Text operator on defined field mapping to single field", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "parameters", + "scope": {"where": {"text": "My parameter"}}, + }, + ], + }, + }, + "datasets", + 1, + [["parameters"]], + [], + [], + id="Text operator on non-defined field", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "documents", + "scope": {"where": {"text": "document1"}}, + }, + ], + }, + }, + "datasets", + 2, + [["documents"]], + [], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("documents.title", "document1", "like")], + [ + SearchAPIWhereFilter( + "documents.summary", "document1", "like", + ), + ], + "or", + ), + ], + id="Text operator on defined field mapping to multiple field", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "documents", + "scope": { + "where": { + "and": [ + {"summary": "My Test Summary"}, + {"title": "Test title"}, + ], + }, + }, + }, + ], + }, + }, + "datasets", + 2, + [["documents"]], + [], + [ + NestedWhereFilters( + [ + SearchAPIWhereFilter( + "documents.summary", "My Test Summary", "eq", + ), + ], + [SearchAPIWhereFilter("documents.title", "Test title", "eq")], + "and", + ), + ], + id="AND boolean operator", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "documents", + "scope": { + "where": { + "or": [ + {"summary": "My Test Summary"}, + {"title": "Test title"}, + ], + }, + }, + }, + ], + }, + }, + "datasets", + 2, + [["documents"]], + [], + [ + NestedWhereFilters( + [ + SearchAPIWhereFilter( + "documents.summary", "My Test Summary", "eq", + ), + ], + [SearchAPIWhereFilter("documents.title", "Test title", "eq")], + "or", + ), + ], + id="OR boolean operator", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "documents", + "scope": { + "where": { + "and": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "and": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "or": [ + {"doi": "Test doi"}, + { + "license": { + "like": "Test license", + }, + }, + ], + }, + ], + }, + }, + }, + ], + }, + }, + "datasets", + 2, + [["documents"]], + [], + [ + NestedWhereFilters( + [ + NestedWhereFilters( + [ + SearchAPIWhereFilter( + "documents.summary", "My Test Summary", "eq", + ), + ], + [ + SearchAPIWhereFilter( + "documents.title", "Test title", "like", + ), + ], + "and", + ), + NestedWhereFilters( + [ + SearchAPIWhereFilter( + "documents.pid", "Test pid", "eq", + ), + ], + [ + SearchAPIWhereFilter( + "documents.type", "Test type", "eq", + ), + ], + "and", + ), + ], + [ + NestedWhereFilters( + [ + SearchAPIWhereFilter( + "documents.doi", "Test doi", "eq", + ), + ], + [ + SearchAPIWhereFilter( + "documents.license", "Test license", "like", + ), + ], + "or", + ), + ], + "and", + ), + ], + id="Nested AND boolean operator", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "documents", + "scope": { + "where": { + "or": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "and": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "or": [ + {"doi": "Test doi"}, + { + "license": { + "like": "Test license", + }, + }, + ], + }, + ], + }, + }, + }, + ], + }, + }, + "datasets", + 2, + [["documents"]], + [], + [ + NestedWhereFilters( + [ + NestedWhereFilters( + [ + SearchAPIWhereFilter( + "documents.summary", "My Test Summary", "eq", + ), + ], + [ + SearchAPIWhereFilter( + "documents.title", "Test title", "like", + ), + ], + "and", + ), + NestedWhereFilters( + [ + SearchAPIWhereFilter( + "documents.pid", "Test pid", "eq", + ), + ], + [ + SearchAPIWhereFilter( + "documents.type", "Test type", "eq", + ), + ], + "and", + ), + ], + [ + NestedWhereFilters( + [ + SearchAPIWhereFilter( + "documents.doi", "Test doi", "eq", + ), + ], + [ + SearchAPIWhereFilter( + "documents.license", "Test license", "like", + ), + ], + "or", + ), + ], + "or", + ), + ], + id="Nested OR boolean operator", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "parameters", + "scope": {"where": {"name": "My parameter"}}, + }, + { + "relation": "documents", + "scope": {"where": {"title": "Document title"}}, + }, + ], + }, + }, + "datasets", + 4, + [["parameters"], ["documents"]], + [ + SearchAPIWhereFilter("parameters.name", "My parameter", "eq"), + SearchAPIWhereFilter("documents.title", "Document title", "eq"), + ], + [], + id="Multiple related models", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "datasets", + "scope": { + "where": {"title": "Dataset 1"}, + "include": [ + { + "relation": "instrument", + "scope": { + "where": {"name": "Instrument 1"}, + }, + }, + ], + }, + }, + ], + }, + }, + "documents", + 3, + [["datasets.instrument"]], + [ + SearchAPIWhereFilter("datasets.title", "Dataset 1", "eq"), + SearchAPIWhereFilter( + "datasets.instrument.name", "Instrument 1", "eq", + ), + ], + [], + id="Nested related models", + ), + ], + ) + def test_valid_include_filter_with_where_filter_in_scope( + self, + test_request_filter, + test_entity_name, + expected_length, + expected_included_entities, + expected_where_filter_data, + expected_nested_wheres, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == expected_length + for test_filter in filters: + if isinstance(test_filter, SearchAPIIncludeFilter): + for expected_include in expected_included_entities: + assert test_filter.included_filters == expected_include + expected_included_entities.remove(expected_include) + if isinstance(test_filter, NestedWhereFilters): + for expected_nested in expected_nested_wheres: + assert repr(test_filter) == repr(expected_nested) + expected_nested_wheres.remove(expected_nested) + if isinstance(test_filter, SearchAPIWhereFilter): + for expected_where in expected_where_filter_data: + assert repr(test_filter) == repr(expected_where) + expected_where_filter_data.remove(expected_where) + + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_length" + ", expected_included_entities", + [ + pytest.param( + { + "filter": { + "include": [ + { + "relation": "datasets", + "scope": {"include": [{"relation": "parameters"}]}, + }, + ], + }, + }, + "documents", + 1, + [["datasets.parameters"]], + id="Single related model", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "datasets", + "scope": { + "include": [ + {"relation": "parameters"}, + {"relation": "instrument"}, + ], + }, + }, + ], + }, + }, + "documents", + 2, + [["datasets.parameters"], ["datasets.instrument"]], + id="Multiple related models", + ), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "datasets", + "scope": { + "include": [ + { + "relation": "documents", + "scope": { + "include": [{"relation": "parameters"}], + }, + }, + ], + }, + }, + ], + }, + }, + "instruments", + 1, + [["datasets.documents.parameters"]], + id="Nested related models", + ), + ], + ) + def test_valid_include_filter_with_include_filter_in_scope( + self, + test_request_filter, + test_entity_name, + expected_length, + expected_included_entities, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == expected_length + for test_filter in filters: + if isinstance(test_filter, SearchAPIIncludeFilter): + for expected_include in expected_included_entities: + assert test_filter.included_filters == expected_include + expected_included_entities.remove(expected_include) + + @pytest.mark.parametrize( + "test_request_filter, expected_limit_value", + [ + pytest.param({"filter": {"limit": 0}}, 0, id="Limit 0 values"), + pytest.param({"filter": {"limit": 50}}, 50, id="Limit 50 values"), + ], + ) + def test_valid_limit_filter(self, test_request_filter, expected_limit_value): + filters = SearchAPIQueryFilterFactory.get_query_filter(test_request_filter) + + assert len(filters) == 1 + assert isinstance(filters[0], SearchAPILimitFilter) + assert filters[0].limit_value == expected_limit_value + + @pytest.mark.parametrize( + "test_request_filter, expected_skip_value", + [ + pytest.param({"filter": {"skip": 0}}, 0, id="Skip 0 values"), + pytest.param({"filter": {"skip": 50}}, 50, id="Skip 50 values"), + ], + ) + def test_valid_skip_filter( + self, test_request_filter, expected_skip_value, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter(test_request_filter) + + assert len(filters) == 1 + assert isinstance(filters[0], SearchAPISkipFilter) + assert filters[0].skip_value == expected_skip_value + + @pytest.mark.parametrize( + "test_request_filter, test_entity_name, expected_length" + ", expected_included_entities, expected_where_filter_data" + ", expected_nested_wheres, expected_limit_values, expected_skip_values", + [ + pytest.param( + { + "filter": { + "where": {"title": "My Title"}, + "include": [{"relation": "instrument"}], + "limit": 50, + "skip": 20, + }, + }, + "datasets", + 4, + [["instrument"]], + [SearchAPIWhereFilter("title", "My Title", "eq")], + [], + [50], + [20], + id="Simple case", + ), + pytest.param( + { + "filter": { + "where": { + "and": [ + { + "and": [ + {"summary": "My Test Summary"}, + {"title": {"like": "Test title"}}, + ], + }, + { + "and": [ + {"pid": "Test pid"}, + {"type": {"eq": "Test type"}}, + ], + }, + { + "or": [ + {"doi": "Test doi"}, + {"license": {"like": "Test license"}}, + ], + }, + ], + }, + "include": [ + { + "relation": "instrument", + "scope": {"where": {"name": "Instrument 1"}}, + }, + ], + "limit": 50, + "skip": 20, + }, + }, + "datasets", + 5, + [["instrument"]], + [SearchAPIWhereFilter("instrument.name", "Instrument 1", "eq")], + [ + NestedWhereFilters( + [ + NestedWhereFilters( + [ + SearchAPIWhereFilter( + "summary", "My Test Summary", "eq", + ), + ], + [SearchAPIWhereFilter("title", "Test title", "like")], + "and", + ), + NestedWhereFilters( + [SearchAPIWhereFilter("pid", "Test pid", "eq")], + [SearchAPIWhereFilter("type", "Test type", "eq")], + "and", + ), + ], + [ + NestedWhereFilters( + [SearchAPIWhereFilter("doi", "Test doi", "eq")], + [ + SearchAPIWhereFilter( + "license", "Test license", "like", + ), + ], + "or", + ), + ], + "and", + ), + ], + [50], + [20], + id="Complex case", + ), + ], + ) + def test_valid_filter_input_with_all_filters( + self, + test_request_filter, + test_entity_name, + expected_length, + expected_included_entities, + expected_where_filter_data, + expected_nested_wheres, + expected_limit_values, + expected_skip_values, + ): + filters = SearchAPIQueryFilterFactory.get_query_filter( + test_request_filter, test_entity_name, + ) + + assert len(filters) == expected_length + for test_filter in filters: + if isinstance(test_filter, SearchAPIIncludeFilter): + for expected_include in expected_included_entities: + assert test_filter.included_filters == expected_include + expected_included_entities.remove(expected_include) + if isinstance(test_filter, NestedWhereFilters): + for expected_nested in expected_nested_wheres: + assert repr(test_filter) == repr(expected_nested) + expected_nested_wheres.remove(expected_nested) + if isinstance(test_filter, SearchAPIWhereFilter): + for expected_where in expected_where_filter_data: + assert repr(test_filter) == repr(expected_where) + expected_where_filter_data.remove(expected_where) + if isinstance(test_filter, SearchAPILimitFilter): + for expected_limit in expected_limit_values: + assert test_filter.limit_value == expected_limit + expected_limit_values.remove(expected_limit) + if isinstance(test_filter, SearchAPISkipFilter): + for expected_skip in expected_skip_values: + assert test_filter.skip_value == expected_skip + expected_skip_values.remove(expected_skip) + + @pytest.mark.parametrize( + "test_request_filter", + [ + pytest.param("invalid query filter input", id="Generally invalid input"), + pytest.param( + { + "filter": { + "include": [ + { + "relation": "parameters", + "scope": {"text": "My parameter"}, + }, + ], + }, + }, + id="Invalid scope syntax on include filter", + ), + pytest.param( + { + "filter": { + "include": [ + {"relation": "parameters", "scope": {"limit": 50}}, + ], + }, + }, + id="Unsupported limit filter in scope of include filter", + ), + pytest.param( + { + "filter": { + "include": [{"relation": "parameters", "scope": {"skip": 20}}], + }, + }, + id="Unsupported skip filter in scope of include filter", + ), + ], + ) + def test_invalid_filter_input(self, test_request_filter): + with pytest.raises(FilterError): + SearchAPIQueryFilterFactory.get_query_filter(test_request_filter) + + @pytest.mark.parametrize( + "filter_input, expected_return", + [ + pytest.param( + {"property": "value"}, + ("property", "value", "eq"), + id="No operator specified", + ), + pytest.param( + {"property": {"ne": "value"}}, + ("property", "value", "ne"), + id="Specific operator given in input", + ), + ], + ) + def test_get_condition_values(self, filter_input, expected_return): + test_condition_values = SearchAPIQueryFilterFactory.get_condition_values( + filter_input, + ) + + assert test_condition_values == expected_return diff --git a/test/test_base_query_filter_factory.py b/test/test_base_query_filter_factory.py new file mode 100644 index 00000000..84978770 --- /dev/null +++ b/test/test_base_query_filter_factory.py @@ -0,0 +1,15 @@ +from datagateway_api.src.common.base_query_filter_factory import QueryFilterFactory + + +class TestBaseQueryFilterFactory: + def test_abstract_class(self): + QueryFilterFactory.__abstractmethods__ = set() + + class DummyQueryFilterFactory(QueryFilterFactory): + pass + + d = DummyQueryFilterFactory() + + request_filter = "request_filter" + + assert d.get_query_filter(request_filter) is None diff --git a/test/test_get_filters_from_query.py b/test/test_get_filters_from_query.py index a13796b5..4e232db0 100644 --- a/test/test_get_filters_from_query.py +++ b/test/test_get_filters_from_query.py @@ -1,6 +1,6 @@ import pytest -from datagateway_api.src.common.exceptions import FilterError +from datagateway_api.src.common.exceptions import ApiError, FilterError from datagateway_api.src.common.helpers import get_filters_from_query_string from datagateway_api.src.datagateway_api.database.filters import ( DatabaseDistinctFieldFilter, @@ -16,14 +16,14 @@ def test_valid_no_filters(self, flask_test_app_db): with flask_test_app_db: flask_test_app_db.get("/") - assert [] == get_filters_from_query_string() + assert [] == get_filters_from_query_string("datagateway_api") def test_invalid_filter(self, flask_test_app_db): with flask_test_app_db: flask_test_app_db.get('/?test="test"') with pytest.raises(FilterError): - get_filters_from_query_string() + get_filters_from_query_string("datagateway_api") @pytest.mark.parametrize( "filter_input, filter_type", @@ -42,13 +42,25 @@ def test_invalid_filter(self, flask_test_app_db): def test_valid_filter(self, flask_test_app_db, filter_input, filter_type): with flask_test_app_db: flask_test_app_db.get(f"/?{filter_input}") - filters = get_filters_from_query_string() + filters = get_filters_from_query_string("datagateway_api") assert isinstance(filters[0], filter_type) def test_valid_multiple_filters(self, flask_test_app_db): with flask_test_app_db: flask_test_app_db.get("/?limit=10&skip=4") - filters = get_filters_from_query_string() + filters = get_filters_from_query_string("datagateway_api") assert len(filters) == 2 + + def test_valid_search_api_filter(self, flask_test_app_db): + with flask_test_app_db: + flask_test_app_db.get('/?filter={"skip": 5, "limit": 10}') + + filters = get_filters_from_query_string("search_api", "Dataset") + + assert len(filters) == 2 + + def test_invalid_api_type(self): + with pytest.raises(ApiError): + get_filters_from_query_string("unknown_api") diff --git a/test/test_query_filter.py b/test/test_query_filter.py index 0148d6cd..d874f706 100644 --- a/test/test_query_filter.py +++ b/test/test_query_filter.py @@ -3,7 +3,9 @@ from datagateway_api.src.common.config import Config from datagateway_api.src.common.exceptions import ApiError from datagateway_api.src.common.filters import QueryFilter -from datagateway_api.src.datagateway_api.query_filter_factory import QueryFilterFactory +from datagateway_api.src.datagateway_api.query_filter_factory import ( + DataGatewayAPIQueryFilterFactory, +) class TestQueryFilter: @@ -25,4 +27,4 @@ class DummyQueryFilter(QueryFilter): def test_invalid_query_filter_getter(self): Config.config.datagateway_api.backend = "invalid_backend" with pytest.raises(ApiError): - QueryFilterFactory.get_query_filter({"order": "id DESC"}) + DataGatewayAPIQueryFilterFactory.get_query_filter({"order": "id DESC"})