"""Functional tests for search endpoint.""" import json from typing import Any from unittest import mock from unittest.mock import MagicMock, Mock import pytest from flask.ctx import AppContext from flask.testing import FlaskClient from moto import mock_aws from opensearchpy.helpers import query from vectororder import config from vectororder.connectors import dynamodb from vectororder.constants import fields, header, pagination @pytest.mark.parametrize( "payload, err_field, err_field_idx", ( ({"product_ids": [-42]}, "product_ids", "0"), ({"product_ids": [1, -42]}, "product_ids", "1"), ), ) def test_func_search_handler_validation_error( payload: dict[str, Any], err_field: str, err_field_idx: str, fixture_client: FlaskClient, ) -> None: """Test search endpoint validation is properly configured.""" headers = { header.CONTENT_TYPE: header.CONTENT_TYPE_JSON_VAL, "Orchard-User-Id": "oa:123", } result = fixture_client.post( "/jobs/search", headers=headers, data=json.dumps(payload) ) res_data = json.loads(result.data) assert result.status_code == 400 assert res_data["message"][err_field][err_field_idx] @mock.patch("vectororder.models.search.g", spec=["ows"]) @mock_aws def test_func_search_handler_success( mock_g: MagicMock, fixture_client: FlaskClient, mocker: Mock, app_context: AppContext, ) -> None: """Test search endpoint properly returns search data.""" headers = { header.CONTENT_TYPE: header.CONTENT_TYPE_JSON_VAL, "Orchard-User-Id": "oa:123", } es_results = ({"result": 1}, {"result": 2}) payload = {"statuses": ["foo", "bar"]} expected_query_expr = query.Q("terms", status=["foo", "bar"]) offset = 30 limit = 15 order_by = "-status" query_params = { "page_offset": offset, "page_limit": limit, "order_by": order_by, } # TODO: Replace mocked ES calls in these functional tests somehow? hit_mock = mocker.Mock() hit_mock.to_dict = mocker.Mock(side_effect=es_results) query_result_mock = mocker.Mock() query_result_mock.hits.__iter__ = mocker.Mock() query_result_mock.hits.__iter__.return_value = iter([hit_mock] * len(es_results)) query_result_mock.hits.total = len(es_results) search_request_query_mock = mocker.Mock() search_request_query_mock.execute.return_value = query_result_mock search_request_query_mock.to_dict.return_value = {"any": "value"} search_request_mock_sliced = mocker.Mock() search_request_mock_sliced.filter.return_value = search_request_query_mock search_request_mock = mocker.Mock() search_request_mock.sort.return_value = search_request_mock search_request_mock.update_from_dict.return_value = search_request_mock_sliced search_obj_mock = mocker.Mock(return_value=search_request_mock) es_dsl_search_mock = mocker.patch("vectororder.models.search.search") es_dsl_search_mock.Search = search_obj_mock _create_vectororder_search_requests_table() # Perform a regular request without request_id. result = fixture_client.post( "/jobs/search", headers=headers, data=json.dumps(payload), query_string=query_params, ) # Check response. res_data = json.loads(result.data) assert result.status_code == 200 search_request_mock_sliced.filter.assert_called_with(expected_query_expr) # Save request id to query DynamoDB later. saved_request_id = res_data.pop("request_id") expected_result: dict[str, Any] = { fields.RESPONSE_ITEMS: list(es_results), pagination.PAGINATION_KEY: { pagination.PAGINATION_TYPE: pagination.PAGINATION_TYPE_STANDARD, pagination.PAGINATION_OFFSET: offset, pagination.PAGINATION_LIMIT: limit, pagination.PAGINATION_TOTAL: len(es_results), }, } assert res_data == expected_result # Check DynamoDB has the saved search request under expected key. dynamodb_resource = dynamodb.get_dynamodb_resource() table = dynamodb_resource.Table(config.DDB_SEARCH_REQUESTS_TABLE) table_data = table.get_item(Key={"id": saved_request_id}) assert json.loads(str(table_data["Item"]["payload_json"])) == payload # Check saved request is loaded, executed and returns the same result. # Mock ES results again (the iterator is already exhausted at this stage). hit_mock.to_dict = mocker.Mock(side_effect=es_results) query_result_mock.hits.__iter__.return_value = iter([hit_mock] * len(es_results)) # Perform another request with request_id (without payload). query_params["request_id"] = saved_request_id result = fixture_client.post( "/jobs/search", headers=headers, query_string=query_params ) # Check response is the same as the original one. res_data = json.loads(result.data.decode()) search_request_mock_sliced.filter.assert_called_with(expected_query_expr) assert result.status_code == 200 expected_result["original_request"] = payload assert res_data == expected_result def _create_vectororder_search_requests_table() -> None: """Create search requests DynamoDB table. Should be used with 'moto.mock.dynamodb2'decorator. """ dynamodb_resource = dynamodb.get_dynamodb_resource() dynamodb_resource.create_table( TableName=config.DDB_SEARCH_REQUESTS_TABLE, KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, )