diff --git a/soda-core/src/soda_core/common/soda_cloud.py b/soda-core/src/soda_core/common/soda_cloud.py index 12339c26f..5b74c3859 100644 --- a/soda-core/src/soda_core/common/soda_cloud.py +++ b/soda-core/src/soda_core/common/soda_cloud.py @@ -801,7 +801,7 @@ def _execute_cqrs_request( ) return response - except SodaCloudAuthenticationFailedException: + except (SodaCloudAuthenticationFailedException, AssertionError): raise except Exception as e: logger.critical( diff --git a/soda-tests/src/helpers/data_source_test_helper.py b/soda-tests/src/helpers/data_source_test_helper.py index 5c9e4f6ab..4320d6b9b 100644 --- a/soda-tests/src/helpers/data_source_test_helper.py +++ b/soda-tests/src/helpers/data_source_test_helper.py @@ -8,7 +8,7 @@ from textwrap import dedent from typing import Optional -from helpers.mock_soda_cloud import MockResponse, MockSodaCloud +from helpers.mock_soda_cloud import MockResponse, MockSodaCloud, SequentialResponseRequestHandler from helpers.test_table import ( TestColumn, TestDataType, @@ -108,8 +108,15 @@ def enable_soda_cloud(self): if logs.has_errors(): raise AssertionError(str(logs)) - def enable_soda_cloud_mock(self, responses: list[MockResponse]): - self.soda_cloud = MockSodaCloud(responses) + def enable_soda_cloud_mock(self) -> MockSodaCloud: + self.soda_cloud = MockSodaCloud() + return self.soda_cloud + + def enable_soda_cloud_mock_based_on_sequential_responses(self, responses: list[MockResponse]) -> MockSodaCloud: + self.soda_cloud = MockSodaCloud(request_handlers=[ + SequentialResponseRequestHandler(responses=responses) + ]) + return self.soda_cloud def _create_data_source_impl(self) -> "DataSourceImpl": """ diff --git a/soda-tests/src/helpers/mock_soda_cloud.py b/soda-tests/src/helpers/mock_soda_cloud.py index 6eed6f13f..9e29b796a 100644 --- a/soda-tests/src/helpers/mock_soda_cloud.py +++ b/soda-tests/src/helpers/mock_soda_cloud.py @@ -1,14 +1,15 @@ from __future__ import annotations import json -import logging -from dataclasses import dataclass +from abc import ABC, abstractmethod +from dataclasses import dataclass, field from enum import Enum from io import BytesIO from tempfile import TemporaryFile from typing import Optional from requests import Response + from soda_core.common.soda_cloud import SodaCloud @@ -17,41 +18,214 @@ class MockHttpMethod(Enum): GET = "get" +@dataclass +class MockRequest: + + method: MockHttpMethod + url: str + headers: dict[str, str] = None + json: Optional[dict] = None + data: Optional[TemporaryFile] = None + + def is_url_upload(self) -> bool: + return self.url.endswith("/upload") + + def is_command(self, command_type: str) -> bool: + return (self.url.endswith("/command") + and isinstance(self.json, dict) + and "type" in self.json + and self.json["type"] == command_type + ) + + def assert_json_subdict(self, expected: dict) -> None: + self.__assert_json_expected_in(expected=expected, actual=self.json) + + @classmethod + def __assert_json_expected_in(cls, expected, actual, path: str = "") -> None: + """ + Recursively checks if `expected` is present within `actual` recursively. + Handles dicts, lists, and basic data types. + """ + if isinstance(expected, AssertStringInJson): + expected.assert_string(actual, path) + + elif isinstance(expected, AssertFloatBetween): + expected.assert_float(actual, path) + + elif isinstance(expected, dict): + if not isinstance(actual, dict): + raise AssertionError(f"Type mismatch at {path}: {actual} is not a dict") + for key, expected_value in expected.items(): + cls.__assert_json_expected_in( + expected=expected_value, + actual=actual.get(key), + path=cls.__append_path(path=path, key=key) + ) + + elif isinstance(expected, list) or isinstance(expected, set) or isinstance(expected, tuple): + if not (isinstance(actual, list) or isinstance(actual, set) or isinstance(actual, tuple)): + raise AssertionError(f"Type mismatch at {path}: {actual} is not a dict") + # Each item in subset must match some item in superset + for index in range(0, len(expected)): + expected_element: any = expected[index] + actual_element: any = actual[index] + cls.__assert_json_expected_in(expected_element, actual_element, cls.__append_path(path, str(index))) + + else: + assert expected == actual, f"Value mismatch at {path}: Expected {expected}, but was {actual}" + + @classmethod + def __append_path(cls, path: str, key: str) -> str: + return key if path == "" else f"{path}.{key}" + + class MockResponse(Response): def __init__( self, - method: MockHttpMethod = MockHttpMethod.POST, status_code: int = 200, headers: Optional[dict[str, str]] = None, json_object: Optional[dict] = None, ): super().__init__() - self.method: MockHttpMethod = method self.status_code = status_code if isinstance(headers, dict): self.headers.update(headers) - # The json_object is stored for overwriting later. See below in _resolve_check_identities - self.json_object: Optional[dict] = None - self.set_json_object(json_object) - - def set_json_object(self, json_object: Optional[dict]): - self.json_object = json_object rows_json_str = json.dumps(json_object) rows_json_bytes = bytearray(rows_json_str, "utf-8") self.raw = BytesIO(rows_json_bytes) +class MockRequestHandler(ABC): + + @abstractmethod + def handle(self, request: MockRequest, request_index: int) -> Optional[Response]: + """ + returns a response if this MockRequestHandler is applicable, None if this handler is not for the given request and + MockSodaCloud should try the next MockRequestHandler. + """ + pass + + +class FileUploadRequestHandler(MockRequestHandler): + + def handle(self, request: MockRequest, request_index: int) -> Optional[Response]: + if not request.is_url_upload(): + return None + + return MockResponse( + json_object={"fileId": "the_file_id"} + ) + + +class SodaCoreInsertScanResultsHandler(MockRequestHandler): + + COMMAND_TYPE: str = "sodaCoreInsertScanResults" + DEFAULT_SCAN_ID: str = "default-scan-id" + + def handle(self, request: MockRequest, request_index: int) -> Optional[Response]: + if not request.is_command(self.COMMAND_TYPE): + return None + + # Leverages the check identities from the request and creates check ids + request_check_identities: list[str] = [] + request_check_results: Optional[list[dict]] = request.json.get("checks") + if request_check_results: + for request_check_result in request_check_results: + check_identities: Optional[dict] = request_check_result.get("identities") + if isinstance(check_identities, dict) and "vc1" in check_identities: + request_check_identities.append(check_identities["vc1"]) + + response_json_dict: dict = {} + response_json_dict["scanId"] = self.DEFAULT_SCAN_ID + response_json_dict.setdefault("checks", []) + response_checks: Optional[list[dict]] = response_json_dict.get("checks") + if isinstance(response_checks, list): + for index in range(0, len(request_check_identities)): + if len(response_checks) <= index: + response_checks.append({}) + response_check: dict = response_checks[index] + check_identity = request_check_identities[index] + response_check["identities"] = [check_identity] + check_id = f"checkid#for#{check_identity}################" # produces same length as UUI + response_check["id"] = check_id + + return MockResponse( + json_object=response_json_dict + ) + + +class SodaCoreAddFailedRowsDiagnosticsHandler(MockRequestHandler): + + COMMAND_TYPE: str = "sodaCoreAddFailedRowsDiagnostics" + + def handle(self, request: MockRequest, request_index: int) -> Optional[Response]: + if not request.is_command(self.COMMAND_TYPE): + return None + return MockResponse() + + +class CatchAllMockRequestHandler(MockRequestHandler): + + def handle(self, request: MockRequest, request_index: int) -> Optional[Response]: + return MockResponse() + + +class SequentialResponseRequestHandler(MockRequestHandler): + + def __init__(self, responses: list[MockResponse]): + self.responses: list[MockResponse] = responses + + def handle(self, request: MockRequest, request_index: int) -> Optional[Response]: + return self.responses[request_index] + + +class AssertStringInJson: + def __init__( + self, + contains: Optional[str] = None, + is_not_empty: bool = True + ): + self.contains: str = contains + self.is_not_empty: bool = is_not_empty + + def assert_string(self, actual: any, path: str): + assert isinstance(actual, str), f"Expected string at {path}, but was {actual}" + if self.contains: + assert self.contains in actual, f"Expected '{self.contains}' at {path}, but was {actual}" + if self.is_not_empty: + assert len(actual) > 0, f"Expected string at {path} to be non empty, but was empty" + + +class AssertFloatBetween: + def __init__( + self, + min: float, + max: float, + ): + self.min: float = min + self.max: float = max + + def assert_float(self, actual: any, path: str): + assert isinstance(actual, float), f"Expected string at {path}, but was {actual}" + assert self.min <= actual <= self.max, f"Expected value between {self.min} and {self.max}, but was {actual}" + + @dataclass -class MockRequest: - request_log_name: str = (None,) - url: Optional[str] = (None,) - headers: dict[str, str] = (None,) - json: Optional[dict] = (None,) - data: Optional[TemporaryFile] = None +class MockRequestResponse: + request: MockRequest + response: Response class MockSodaCloud(SodaCloud): - def __init__(self, responses: Optional[list[Optional[MockResponse]]] = None): + + DEFAULT_REQUEST_HANDLERS: list[MockRequestHandler] = [ + FileUploadRequestHandler(), + SodaCoreInsertScanResultsHandler(), + SodaCoreAddFailedRowsDiagnosticsHandler(), + CatchAllMockRequestHandler(), + ] + + def __init__(self, request_handlers: Optional[list[MockRequestHandler]] = None): super().__init__( host="mock.soda.io", api_key_id="mock-key-id", @@ -60,8 +234,11 @@ def __init__(self, responses: Optional[list[Optional[MockResponse]]] = None): port="9999", scheme="https", ) - self.requests: list[MockRequest] = [] - self.responses: list[Optional[MockResponse]] = responses if isinstance(responses, list) else [] + self._request_responses: list[MockRequestResponse] = [] + self._request_handlers: list[MockRequestHandler] = ( + self.DEFAULT_REQUEST_HANDLERS + if request_handlers is None else request_handlers + ) def _http_post( self, @@ -90,40 +267,31 @@ def _http_handle( json: Optional[dict], data: Optional[TemporaryFile], ) -> Response: - self.requests.append(MockRequest(url=url, headers=headers, json=json, data=data)) - if self.responses: - response = self.responses.pop(0) - if isinstance(response, MockResponse): - if method != response.method: - raise AssertionError("Wrong response method") - if self._is_send_scan_results_request(json): - self._resolve_check_identities(json, response) - logging.debug(f"MockSodaCloud responds to {method} {url} with provided response") + request: MockRequest = MockRequest(method=method, url=url, headers=headers, json=json, data=data) + for request_handler in self._request_handlers: + request_index: int = len(self._request_responses) + response: Optional[Response] = request_handler.handle(request, request_index) + if response: + self._request_responses.append(MockRequestResponse(request=request, response=response)) return response - logging.debug(f"MockSodaCloud responds to {method} {url} with default empty 200 OK response") - return MockResponse(status_code=200, headers={}, json_object={}) - - def _is_send_scan_results_request(self, request_json: Optional[dict]) -> bool: - return ( - isinstance(request_json, dict) - and "type" in request_json - and request_json["type"] == "sodaCoreInsertScanResults" - ) + raise NotImplementedError(f"No mock handler registered for request {request}.") - def _resolve_check_identities(self, request_json: dict, response: MockResponse): - request_check_identities: list[str] = [] - request_check_results: Optional[list[dict]] = request_json.get("checks") - if request_check_results: - for request_check_result in request_check_results: - check_identities: Optional[dict] = request_check_result.get("identities") - if isinstance(check_identities, dict) and "vc1" in check_identities: - request_check_identities.append(check_identities["vc1"]) + def get_request(self, command_type: str, index: int = 0) -> MockRequest: + return self.get_request_response(command_type=command_type, index=index).request - new_json_object: dict = response.json_object.copy() - response_checks: Optional[list[dict]] = new_json_object.get("checks") - if isinstance(response_checks, list) and len(response_checks) == len(request_check_identities): - for index in range(0, len(response_checks)): - response_check: dict = response_checks[index] - response_check["identities"] = [request_check_identities[index]] + def get_request_insert_scan_results(self, index: int = 0) -> MockRequest: + return self.get_request_response(command_type="sodaCoreInsertScanResults", index=index).request + + def get_request_add_failed_rows_diagnostics(self, index: int = 0) -> MockRequest: + return self.get_request_response(command_type="sodaCoreAddFailedRowsDiagnostics", index=index).request - response.set_json_object(new_json_object) + def get_request_response(self, command_type: str, index: int = 0) -> MockRequestResponse: + command_request_responses: list[MockRequestResponse] = [ + request_response + for request_response in self._request_responses + if request_response.request.is_command(command_type) + ] + if len(command_request_responses) > index: + return command_request_responses[index] + else: + raise AssertionError(f"No command request response found for command type {command_type}, index {index}") diff --git a/soda-tests/tests/components/test_contract_publication_api.py b/soda-tests/tests/components/test_contract_publication_api.py index b2b9bd8ab..64a6b8488 100644 --- a/soda-tests/tests/components/test_contract_publication_api.py +++ b/soda-tests/tests/components/test_contract_publication_api.py @@ -1,5 +1,5 @@ import pytest -from helpers.mock_soda_cloud import MockHttpMethod, MockResponse, MockSodaCloud +from helpers.mock_soda_cloud import MockHttpMethod, MockResponse, MockSodaCloud, SequentialResponseRequestHandler from soda_core.common.exceptions import InvalidDatasetQualifiedNameException, YamlParserException from soda_core.contracts.contract_publication import ( ContractPublication, @@ -65,33 +65,36 @@ def test_contract_publication_fails_on_missing_contract_file(): def test_contract_publication_returns_result_for_each_added_contract(): - responses = [ - MockResponse(method=MockHttpMethod.POST, status_code=200, json_object={"allowed": "true"}), - MockResponse(method=MockHttpMethod.POST, status_code=200, json_object={"fileId": "fake_file_id"}), - MockResponse( - method=MockHttpMethod.POST, - json_object={ - "publishedContract": { - "checksum": "check", - "fileId": "fake_file_id", - }, - "metadata": {"source": {"filePath": "contract1.yml", "type": "local"}}, - }, - ), - MockResponse(method=MockHttpMethod.POST, status_code=200, json_object={"allowed": "true"}), - MockResponse(method=MockHttpMethod.POST, status_code=200, json_object={"fileId": "fake_file_id2"}), - MockResponse( - method=MockHttpMethod.POST, - json_object={ - "publishedContract": { - "checksum": "check", - "fileId": "fake_file_id2", - }, - "metadata": {"source": {"filePath": "contract2.yml", "type": "local"}}, - }, - ), - ] - mock_cloud = MockSodaCloud(responses) + mock_cloud = MockSodaCloud( + request_handlers=[ + SequentialResponseRequestHandler( + responses=[ + MockResponse(status_code=200, json_object={"allowed": "true"}), + MockResponse(status_code=200, json_object={"fileId": "fake_file_id"}), + MockResponse( + json_object={ + "publishedContract": { + "checksum": "check", + "fileId": "fake_file_id", + }, + "metadata": {"source": {"filePath": "contract1.yml", "type": "local"}}, + }, + ), + MockResponse(status_code=200, json_object={"allowed": "true"}), + MockResponse(status_code=200, json_object={"fileId": "fake_file_id2"}), + MockResponse( + json_object={ + "publishedContract": { + "checksum": "check", + "fileId": "fake_file_id2", + }, + "metadata": {"source": {"filePath": "contract2.yml", "type": "local"}}, + }, + ), + ] + ) + ] + ) contract_publication_result = ( ContractPublication.builder() diff --git a/soda-tests/tests/components/test_soda_cloud.py b/soda-tests/tests/components/test_soda_cloud.py index ee5c90884..c45c0ef6e 100644 --- a/soda-tests/tests/components/test_soda_cloud.py +++ b/soda-tests/tests/components/test_soda_cloud.py @@ -8,7 +8,7 @@ MockHttpMethod, MockRequest, MockResponse, - MockSodaCloud, + MockSodaCloud, SequentialResponseRequestHandler, ) from helpers.test_table import TestTableSpecification from soda_core.common.datetime_conversions import convert_datetime_to_str @@ -75,29 +75,7 @@ def test_soda_cloud_results(data_source_test_helper: DataSourceTestHelper, env_v env_vars["SODA_SCAN_ID"] = "env_var_scan_id" - data_source_test_helper.enable_soda_cloud_mock( - [ - MockResponse(status_code=200, json_object={"fileId": "777ggg"}), - MockResponse(method=MockHttpMethod.POST, status_code=200, json_object={ - "scanId": "ssscanid", - "checks": [ - { - "id": "123e4567-e89b-12d3-a456-426655440000", - "identities": [ - "0e741893" - ] - }, - { - "id": "456e4567-e89b-12d3-a456-426655441111", - "identities": [ - "c12087d5" - ] - }, - ] - }), - - ] - ) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() data_source_test_helper.assert_contract_pass( test_table=test_table, @@ -120,15 +98,7 @@ def test_soda_cloud_results(data_source_test_helper: DataSourceTestHelper, env_v """, ) - request_index = 0 - request_1: MockRequest = data_source_test_helper.soda_cloud.requests[request_index] - assert request_1.url.endswith("api/scan/upload") - - request_index += 1 - request_2: MockRequest = data_source_test_helper.soda_cloud.requests[request_index] - assert request_2.url.endswith("api/command") - assert_dict(request_2.json, { - "type": "sodaCoreInsertScanResults", + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ "scanId": "env_var_scan_id", "checks": [ { @@ -173,18 +143,23 @@ def test_soda_cloud_results(data_source_test_helper: DataSourceTestHelper, env_v def test_execute_over_agent(data_source_test_helper: DataSourceTestHelper): test_table = data_source_test_helper.ensure_test_table(test_table_specification) - data_source_test_helper.enable_soda_cloud_mock( - [ + data_source_test_helper.enable_soda_cloud_mock_based_on_sequential_responses( + responses=[ MockResponse( status_code=200, json_object={ "allowed": True, }, ), - MockResponse(method=MockHttpMethod.POST, status_code=200, json_object={"fileId": "fffileid"}), - MockResponse(method=MockHttpMethod.POST, status_code=200, json_object={"scanId": "ssscanid"}), MockResponse( - method=MockHttpMethod.GET, + status_code=200, + json_object={"fileId": "fffileid"} + ), + MockResponse( + status_code=200, + json_object={"scanId": "ssscanid"} + ), + MockResponse( status_code=200, headers={"X-Soda-Next-Poll-Time": convert_datetime_to_str(datetime.now(timezone.utc))}, json_object={ @@ -193,7 +168,6 @@ def test_execute_over_agent(data_source_test_helper: DataSourceTestHelper): }, ), MockResponse( - method=MockHttpMethod.GET, status_code=200, json_object={ "scanId": "ssscanid", @@ -203,7 +177,6 @@ def test_execute_over_agent(data_source_test_helper: DataSourceTestHelper): }, ), MockResponse( - method=MockHttpMethod.GET, status_code=200, json_object={ "content": [ @@ -248,26 +221,31 @@ def test_execute_over_agent(data_source_test_helper: DataSourceTestHelper): def test_publish_contract(): - responses = [ - MockResponse( - status_code=200, - json_object={ - "allowed": True, - }, - ), - MockResponse(method=MockHttpMethod.POST, status_code=200, json_object={"fileId": "fake_file_id"}), - MockResponse( - method=MockHttpMethod.POST, - json_object={ - "publishedContract": { - "checksum": "check", - "fileId": "fake_file_id", - }, - "metadata": {"source": {"filePath": "yaml_string", "type": "local"}}, - }, - ), - ] - mock_cloud = MockSodaCloud(responses) + mock_cloud = MockSodaCloud(request_handlers=[ + SequentialResponseRequestHandler( + responses=[ + MockResponse( + status_code=200, + json_object={ + "allowed": True, + }, + ), + MockResponse( + status_code=200, + json_object={"fileId": "fake_file_id"} + ), + MockResponse( + json_object={ + "publishedContract": { + "checksum": "check", + "fileId": "fake_file_id", + }, + "metadata": {"source": {"filePath": "yaml_string", "type": "local"}}, + }, + ), + ] + ) + ]) res = mock_cloud.publish_contract( ContractYaml.parse( @@ -290,16 +268,20 @@ def test_publish_contract(): def test_verify_contract_on_agent_permission_check(): - responses = [ - MockResponse( - status_code=200, - json_object={ - "allowed": False, - "reason": "missingManageContracts", - }, - ), - ] - mock_cloud = MockSodaCloud(responses) + mock_cloud = MockSodaCloud( + request_handlers=[ + SequentialResponseRequestHandler( + responses=[ + MockResponse( + status_code=200, + json_object={ + "allowed": False, + "reason": "missingManageContracts", + }, + ), + ] + ) + ]) res = mock_cloud.verify_contract_on_agent( ContractYaml.parse( diff --git a/soda-tests/tests/features/test_aggregate_check.py b/soda-tests/tests/features/test_aggregate_check.py index 4be2422c5..5e5fb68b2 100644 --- a/soda-tests/tests/features/test_aggregate_check.py +++ b/soda-tests/tests/features/test_aggregate_check.py @@ -1,5 +1,5 @@ from helpers.data_source_test_helper import DataSourceTestHelper -from helpers.mock_soda_cloud import MockResponse, MockHttpMethod +from helpers.mock_soda_cloud import MockResponse, MockHttpMethod, MockSodaCloud, MockRequest from helpers.test_functions import get_diagnostic_value from helpers.test_table import TestTableSpecification from soda_core.contracts.contract_verification import ( @@ -30,9 +30,7 @@ def test_aggregate_function_avg(data_source_test_helper: DataSourceTestHelper): test_table = data_source_test_helper.ensure_test_table(test_table_specification) - data_source_test_helper.enable_soda_cloud_mock([ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ]) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() contract_verification_result: ContractVerificationResult = data_source_test_helper.assert_contract_pass( test_table=test_table, @@ -49,13 +47,19 @@ def test_aggregate_function_avg(data_source_test_helper: DataSourceTestHelper): check_result: CheckResult = contract_verification_result.check_results[0] assert get_diagnostic_value(check_result, "avg") == 5 - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - assert check_json["diagnostics"]["v4"] == { - "type": "aggregate", - "datasetRowsTested": 5, - "checkRowsTested": 5 - } + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ + "checks": [ + { + "diagnostics": { + "v4": { + "type": "aggregate", + "datasetRowsTested": 5, + "checkRowsTested": 5 + } + } + } + ] + }) def test_aggregate_function_avg_with_missing(data_source_test_helper: DataSourceTestHelper): diff --git a/soda-tests/tests/features/test_duplicate_column_check.py b/soda-tests/tests/features/test_duplicate_column_check.py index 4a919a3be..7ae30b6f4 100644 --- a/soda-tests/tests/features/test_duplicate_column_check.py +++ b/soda-tests/tests/features/test_duplicate_column_check.py @@ -1,5 +1,5 @@ from helpers.data_source_test_helper import DataSourceTestHelper -from helpers.mock_soda_cloud import MockResponse +from helpers.mock_soda_cloud import MockResponse, MockSodaCloud from helpers.test_functions import get_diagnostic_value from helpers.test_table import TestTableSpecification from soda_core.contracts.contract_verification import ( @@ -34,9 +34,7 @@ def test_duplicate_str_pass(data_source_test_helper: DataSourceTestHelper): test_table = data_source_test_helper.ensure_test_table(test_table_specification) - data_source_test_helper.enable_soda_cloud_mock([ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ]) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() contract_verification_result: ContractVerificationResult = data_source_test_helper.assert_contract_pass( test_table=test_table, @@ -48,15 +46,21 @@ def test_duplicate_str_pass(data_source_test_helper: DataSourceTestHelper): """, ) - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - assert check_json["diagnostics"]["v4"] == { - "type": "duplicate", - "failedRowsCount": 0, - "failedRowsPercent": 0.0, - "datasetRowsTested": 10, - "checkRowsTested": 10, - } + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ + "checks": [ + { + "diagnostics": { + "v4": { + "type": "duplicate", + "failedRowsCount": 0, + "failedRowsPercent": 0.0, + "datasetRowsTested": 10, + "checkRowsTested": 10, + } + } + } + ] + }) def test_duplicate_int_fail(data_source_test_helper: DataSourceTestHelper): diff --git a/soda-tests/tests/features/test_duplicate_dataset_check.py b/soda-tests/tests/features/test_duplicate_dataset_check.py index 33408cb79..1d95b9734 100644 --- a/soda-tests/tests/features/test_duplicate_dataset_check.py +++ b/soda-tests/tests/features/test_duplicate_dataset_check.py @@ -1,5 +1,5 @@ from helpers.data_source_test_helper import DataSourceTestHelper -from helpers.mock_soda_cloud import MockResponse +from helpers.mock_soda_cloud import MockResponse, MockSodaCloud, AssertFloatBetween from helpers.test_functions import get_diagnostic_value from helpers.test_table import TestTableSpecification from soda_core.contracts.contract_verification import ( @@ -37,9 +37,7 @@ def test_dataset_duplicate(data_source_test_helper: DataSourceTestHelper): test_table = data_source_test_helper.ensure_test_table(test_table_specification) - data_source_test_helper.enable_soda_cloud_mock([ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ]) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() contract_verification_result: ContractVerificationResult = data_source_test_helper.assert_contract_fail( test_table=test_table, @@ -50,20 +48,21 @@ def test_dataset_duplicate(data_source_test_helper: DataSourceTestHelper): """, ) - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - - multicolumn_duplicate_diagnostics: dict = check_json["diagnostics"]["v4"] - assert 15 < multicolumn_duplicate_diagnostics["failedRowsPercent"] < 16 - del multicolumn_duplicate_diagnostics["failedRowsPercent"] - - assert check_json["diagnostics"]["v4"] == { - "type": "duplicate", - "failedRowsCount": 2, - # "failedRowsPercent": 15.384615384615385, # float value tested and removed above - "datasetRowsTested": 13, - "checkRowsTested": 13, - } + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ + "checks": [ + { + "diagnostics": { + "v4": { + "type": "duplicate", + "failedRowsCount": 2, + "failedRowsPercent": AssertFloatBetween(15.3, 15.4), + "datasetRowsTested": 13, + "checkRowsTested": 13, + } + } + } + ] + }) def test_dataset_duplicate_percent(data_source_test_helper: DataSourceTestHelper): diff --git a/soda-tests/tests/features/test_freshness_check.py b/soda-tests/tests/features/test_freshness_check.py index 597b4e64e..09ca209ca 100644 --- a/soda-tests/tests/features/test_freshness_check.py +++ b/soda-tests/tests/features/test_freshness_check.py @@ -2,7 +2,7 @@ from freezegun import freeze_time from helpers.data_source_test_helper import DataSourceTestHelper -from helpers.mock_soda_cloud import MockResponse +from helpers.mock_soda_cloud import MockResponse, MockSodaCloud from helpers.test_functions import get_diagnostic_value from helpers.test_table import TestTableSpecification from soda_core.common.datetime_conversions import convert_datetime_to_str @@ -34,9 +34,7 @@ def test_freshness(data_source_test_helper: DataSourceTestHelper): test_table = data_source_test_helper.ensure_test_table(test_table_specification) - data_source_test_helper.enable_soda_cloud_mock([ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ]) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() contract_yaml_str: str = f""" checks: @@ -60,16 +58,22 @@ def test_freshness(data_source_test_helper: DataSourceTestHelper): assert str(check_result.unit) == "hour" assert get_diagnostic_value(check_result, "freshness_in_hours") == 1 - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - assert check_json["diagnostics"]["v4"] == { - "type": "freshness", - "actualTimestamp": "2025-01-04T09:00:00+00:00", - "actualTimestampUtc": "2025-01-04T09:00:00+00:00", - "expectedTimestamp": "2025-01-04T10:00:00+00:00", - "expectedTimestampUtc": "2025-01-04T10:00:00+00:00", - "datasetRowsTested": 6, - } + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ + "checks": [ + { + "diagnostics": { + "v4": { + "type": "freshness", + "actualTimestamp": "2025-01-04T09:00:00+00:00", + "actualTimestampUtc": "2025-01-04T09:00:00+00:00", + "expectedTimestamp": "2025-01-04T10:00:00+00:00", + "expectedTimestampUtc": "2025-01-04T10:00:00+00:00", + "datasetRowsTested": 6, + } + } + } + ] + }) def test_freshness_in_days(data_source_test_helper: DataSourceTestHelper): diff --git a/soda-tests/tests/features/test_invalid_check.py b/soda-tests/tests/features/test_invalid_check.py index b142e2882..f44f30598 100644 --- a/soda-tests/tests/features/test_invalid_check.py +++ b/soda-tests/tests/features/test_invalid_check.py @@ -1,7 +1,7 @@ import pytest from helpers.data_source_test_helper import DataSourceTestHelper -from helpers.mock_soda_cloud import MockResponse +from helpers.mock_soda_cloud import MockResponse, MockSodaCloud from helpers.test_functions import get_diagnostic_value from helpers.test_table import TestTableSpecification from soda_core.contracts.contract_verification import ContractVerificationResult @@ -45,9 +45,7 @@ def test_valid_count(data_source_test_helper: DataSourceTestHelper, contract_yaml_str: str): test_table = data_source_test_helper.ensure_test_table(test_table_specification) - data_source_test_helper.enable_soda_cloud_mock([ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ]) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() contract_verification_result: ContractVerificationResult = data_source_test_helper.assert_contract_fail( test_table=test_table, contract_yaml_str=contract_yaml_str @@ -57,15 +55,21 @@ def test_valid_count(data_source_test_helper: DataSourceTestHelper, contract_yam diagnostic_name="invalid_count" ) == 1 - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - assert check_json["diagnostics"]["v4"] == { - "type": "invalid", - "failedRowsCount": 1, - "failedRowsPercent": 25.0, - "datasetRowsTested": 4, - "checkRowsTested": 4, - } + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ + "checks": [ + { + "diagnostics": { + "v4": { + "type": "invalid", + "failedRowsCount": 1, + "failedRowsPercent": 25.0, + "datasetRowsTested": 4, + "checkRowsTested": 4, + } + } + } + ] + }) def test_valid_values_with_null(data_source_test_helper: DataSourceTestHelper): diff --git a/soda-tests/tests/features/test_missing_check.py b/soda-tests/tests/features/test_missing_check.py index f50f19aa2..d326a3428 100644 --- a/soda-tests/tests/features/test_missing_check.py +++ b/soda-tests/tests/features/test_missing_check.py @@ -1,5 +1,5 @@ from helpers.data_source_test_helper import DataSourceTestHelper -from helpers.mock_soda_cloud import MockResponse +from helpers.mock_soda_cloud import MockResponse, MockSodaCloud from helpers.test_table import TestTableSpecification test_table_specification = ( @@ -22,9 +22,7 @@ def test_missing_count(data_source_test_helper: DataSourceTestHelper): test_table = data_source_test_helper.ensure_test_table(test_table_specification) - data_source_test_helper.enable_soda_cloud_mock([ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ]) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() data_source_test_helper.assert_contract_fail( test_table=test_table, @@ -38,15 +36,21 @@ def test_missing_count(data_source_test_helper: DataSourceTestHelper): """, ) - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - assert check_json["diagnostics"]["v4"] == { - "type": "missing", - "failedRowsCount": 1, - "failedRowsPercent": 25.0, - "datasetRowsTested": 4, - "checkRowsTested": 4, - } + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ + "checks": [ + { + "diagnostics": { + "v4": { + "type": "missing", + "failedRowsCount": 1, + "failedRowsPercent": 25.0, + "datasetRowsTested": 4, + "checkRowsTested": 4, + } + } + } + ] + }) def test_missing_count_custom_missing_values(data_source_test_helper: DataSourceTestHelper): diff --git a/soda-tests/tests/features/test_row_count_check.py b/soda-tests/tests/features/test_row_count_check.py index 585f0350f..04f02edc1 100644 --- a/soda-tests/tests/features/test_row_count_check.py +++ b/soda-tests/tests/features/test_row_count_check.py @@ -3,7 +3,7 @@ import pytest from helpers.data_source_test_helper import DataSourceTestHelper -from helpers.mock_soda_cloud import MockResponse +from helpers.mock_soda_cloud import MockResponse, MockSodaCloud from helpers.test_functions import get_diagnostic_value from helpers.test_table import TestTableSpecification from soda_core.contracts.contract_verification import ( @@ -29,11 +29,7 @@ def test_row_count(data_source_test_helper: DataSourceTestHelper): test_table = data_source_test_helper.ensure_test_table(test_table_specification) - data_source_test_helper.enable_soda_cloud_mock( - [ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ] - ) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() data_source_test_helper.assert_contract_pass( test_table=test_table, @@ -43,13 +39,19 @@ def test_row_count(data_source_test_helper: DataSourceTestHelper): """, ) - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - assert check_json["diagnostics"]["v4"] == { - "type": "row_count", - "datasetRowsTested": 3, - "checkRowsTested": 3, - } + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ + "checks": [ + { + "diagnostics": { + "v4": { + "type": "row_count", + "datasetRowsTested": 3, + "checkRowsTested": 3, + } + } + } + ] + }) def test_row_count_with_check_filter(data_source_test_helper: DataSourceTestHelper): diff --git a/soda-tests/tests/features/test_schema_check.py b/soda-tests/tests/features/test_schema_check.py index 65cc44f36..91bababe0 100644 --- a/soda-tests/tests/features/test_schema_check.py +++ b/soda-tests/tests/features/test_schema_check.py @@ -1,5 +1,5 @@ from helpers.data_source_test_helper import DataSourceTestHelper -from helpers.mock_soda_cloud import MockResponse +from helpers.mock_soda_cloud import MockResponse, MockSodaCloud, MockRequest from helpers.test_table import TestDataType, TestTableSpecification from soda_core.contracts.contract_verification import ContractVerificationResult from soda_core.contracts.impl.check_types.schema_check import SchemaCheckResult @@ -17,9 +17,7 @@ def test_schema(data_source_test_helper: DataSourceTestHelper): test_table = data_source_test_helper.ensure_test_table(test_table_specification) - data_source_test_helper.enable_soda_cloud_mock([ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ]) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() data_source_test_helper.assert_contract_pass( test_table=test_table, @@ -35,9 +33,8 @@ def test_schema(data_source_test_helper: DataSourceTestHelper): """, ) - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - schema_diagnostics: dict = check_json["diagnostics"]["v4"] + mock_request: MockRequest = mock_soda_cloud.get_request_insert_scan_results() + schema_diagnostics: dict = mock_request.json["checks"][0]["diagnostics"]["v4"] assert schema_diagnostics["type"] == "schema" assert set([c["name"] for c in schema_diagnostics["actual"]]) == {"id", "size", "created"} assert set([c["name"] for c in schema_diagnostics["expected"]]) == {"id", "size", "created"} diff --git a/soda-tests/tests/features/test_udf_failed_rows_check.py b/soda-tests/tests/features/test_udf_failed_rows_check.py index ee4715cfc..fd7c85fda 100644 --- a/soda-tests/tests/features/test_udf_failed_rows_check.py +++ b/soda-tests/tests/features/test_udf_failed_rows_check.py @@ -1,5 +1,5 @@ from helpers.data_source_test_helper import DataSourceTestHelper -from helpers.mock_soda_cloud import MockResponse +from helpers.mock_soda_cloud import MockResponse, MockSodaCloud, AssertFloatBetween from helpers.test_functions import get_diagnostic_value from helpers.test_table import TestTableSpecification from soda_core.contracts.contract_verification import ( @@ -29,9 +29,7 @@ def test_failed_rows_expression(data_source_test_helper: DataSourceTestHelper): end_quoted = data_source_test_helper.quote_column("end") start_quoted = data_source_test_helper.quote_column("start") - data_source_test_helper.enable_soda_cloud_mock([ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ]) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() contract_verification_result: ContractVerificationResult = data_source_test_helper.assert_contract_fail( test_table=test_table, @@ -43,20 +41,21 @@ def test_failed_rows_expression(data_source_test_helper: DataSourceTestHelper): """, ) - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - - multicolumn_duplicate_diagnostics: dict = check_json["diagnostics"]["v4"] - assert 66.6 < multicolumn_duplicate_diagnostics["failedRowsPercent"] < 66.7 - del multicolumn_duplicate_diagnostics["failedRowsPercent"] - - assert check_json["diagnostics"]["v4"] == { - "type": "failed_rows", - "failedRowsCount": 2, - # "failedRowsPercent": 66.66666666666667, # float value tested and removed above - "datasetRowsTested": 3, - "checkRowsTested": 3, - } + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ + "checks": [ + { + "diagnostics": { + "v4": { + "type": "failed_rows", + "failedRowsCount": 2, + "failedRowsPercent": AssertFloatBetween(66, 67), + "datasetRowsTested": 3, + "checkRowsTested": 3, + } + } + } + ] + }) def test_failed_rows_query(data_source_test_helper: DataSourceTestHelper): @@ -65,9 +64,7 @@ def test_failed_rows_query(data_source_test_helper: DataSourceTestHelper): end_quoted = data_source_test_helper.quote_column("end") start_quoted = data_source_test_helper.quote_column("start") - data_source_test_helper.enable_soda_cloud_mock([ - MockResponse(status_code=200, json_object={"fileId": "a81bc81b-dead-4e5d-abff-90865d1e13b1"}), - ]) + mock_soda_cloud: MockSodaCloud = data_source_test_helper.enable_soda_cloud_mock() contract_verification_result: ContractVerificationResult = data_source_test_helper.assert_contract_fail( test_table=test_table, @@ -81,15 +78,20 @@ def test_failed_rows_query(data_source_test_helper: DataSourceTestHelper): """, ) - soda_core_insert_scan_results_command = data_source_test_helper.soda_cloud.requests[1].json - check_json: dict = soda_core_insert_scan_results_command["checks"][0] - - assert check_json["diagnostics"]["v4"] == { - "type": "failed_rows", - "failedRowsCount": 2, - - # TODO remove after issue DTL-922 is fixed - "datasetRowsTested": 0, - "checkRowsTested": 0, - "failedRowsPercent": 0, - } + mock_soda_cloud.get_request_insert_scan_results().assert_json_subdict({ + "checks": [ + { + "diagnostics": { + "v4": { + "type": "failed_rows", + "failedRowsCount": 2, + + # TODO remove after issue DTL-922 is fixed + "datasetRowsTested": 0, + "checkRowsTested": 0, + "failedRowsPercent": 0, + } + } + } + ] + })