From d2ee99ac5ea50949d89e9a1d6591d74a33290ce6 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:31:48 +0800 Subject: [PATCH 1/7] fix(approvals_service): Remove unused imports and replace InternalServerError - Remove unused datetime import and current_date variable calculation - Replace InternalServerError with EnvironmentError for environment variable validation - Simplify error handling for SERVICE_NAMESPACE and CONTRACT_STATUS_TABLE checks - InternalServerError is not appropriate for environment configuration issues that occur at module initialization --- .../contract_status_changed_event_handler.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/unicorn_approvals/src/approvals_service/contract_status_changed_event_handler.py b/unicorn_approvals/src/approvals_service/contract_status_changed_event_handler.py index 11f952d..e1b707b 100644 --- a/unicorn_approvals/src/approvals_service/contract_status_changed_event_handler.py +++ b/unicorn_approvals/src/approvals_service/contract_status_changed_event_handler.py @@ -2,20 +2,18 @@ # SPDX-License-Identifier: MIT-0 import os -from datetime import datetime import boto3 from aws_lambda_powertools.logging import Logger from aws_lambda_powertools.metrics import Metrics from aws_lambda_powertools.tracing import Tracer -from aws_lambda_powertools.event_handler.exceptions import InternalServerError from schema.unicorn_contracts.contractstatuschanged import AWSEvent, ContractStatusChanged, Marshaller # Initialise Environment variables if (SERVICE_NAMESPACE := os.environ.get("SERVICE_NAMESPACE")) is None: - raise InternalServerError("SERVICE_NAMESPACE environment variable is undefined") + raise EnvironmentError("SERVICE_NAMESPACE environment variable is undefined") if (CONTRACT_STATUS_TABLE := os.environ.get("CONTRACT_STATUS_TABLE")) is None: - raise InternalServerError("CONTRACT_STATUS_TABLE environment variable is undefined") + raise EnvironmentError("CONTRACT_STATUS_TABLE environment variable is undefined") # Initialise PowerTools logger: Logger = Logger() @@ -26,10 +24,6 @@ dynamodb = boto3.resource("dynamodb") table = dynamodb.Table(CONTRACT_STATUS_TABLE) # type: ignore -# Get current date -now = datetime.now() -current_date = now.strftime("%d/%m/%Y %H:%M:%S") - @logger.inject_lambda_context(log_event=True) # type: ignore @metrics.log_metrics(capture_cold_start_metric=True) # type: ignore From 9832a2e6f83c4e6d1a7f97e610e6b070f4ef1f15 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:39:48 +0800 Subject: [PATCH 2/7] fix(publication_manager_service): Add evaluation result validation and remove unused metric - Add validation for evaluation_result to ensure only "APPROVED" or "DECLINED" values are processed - Return early with skip message when unknown evaluation result is encountered - Log warning when evaluation result is invalid for debugging purposes - Remove unused PropertiesAdded metric that was not being utilized - Improve robustness by preventing invalid data from being written to DynamoDB --- .../publication_evaluation_event_handler.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/unicorn_web/src/publication_manager_service/publication_evaluation_event_handler.py b/unicorn_web/src/publication_manager_service/publication_evaluation_event_handler.py index 2c252bb..1b137c5 100644 --- a/unicorn_web/src/publication_manager_service/publication_evaluation_event_handler.py +++ b/unicorn_web/src/publication_manager_service/publication_evaluation_event_handler.py @@ -74,14 +74,18 @@ def publication_approved(event_detail, errors): property_id = event_detail.property_id evaluation_result = event_detail.evaluation_result + + valid_results = {"APPROVED", "DECLINED"} + if evaluation_result.upper() not in valid_results: + logger.warning(f"Unknown evaluation_result '{evaluation_result}'; skipping DynamoDB update") + return {"result": "Skipped — unknown evaluation result"} + country, city, street, number = property_id.split("/") pk_details = f"{country}#{city}".replace(" ", "-").lower() pk = f"PROPERTY#{pk_details}" sk = f"{street}#{str(number)}".replace(" ", "-").lower() - metrics.add_metric(name="PropertiesAdded", unit=MetricUnit.Count, value=1) - logger.info(f"Storing new property in DynamoDB with PK {pk} and SK {sk}") dynamodb_response = table.update_item( Key={ From 25e292c318192fafefff138e16e0b48a4391b4e3 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:41:56 +0800 Subject: [PATCH 3/7] fix(contracts_service): Improve datetime handling and add contract metrics - Import timezone from datetime module for UTC timestamp generation - Import MetricUnit from aws_lambda_powertools.metrics for proper metric typing - Replace strftime formatting with isoformat() for ISO 8601 compliant timestamps in create_contract and update_contract functions - Add metric tracking for successful contract creation events - Rename UpdateExpression attribute from modified_date to contract_last_modified_on for consistency - Ensures timestamps are timezone-aware and use UTC for consistency across distributed systems --- .../src/contracts_service/contract_event_handler.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/unicorn_contracts/src/contracts_service/contract_event_handler.py b/unicorn_contracts/src/contracts_service/contract_event_handler.py index 93b6a80..2bbb447 100644 --- a/unicorn_contracts/src/contracts_service/contract_event_handler.py +++ b/unicorn_contracts/src/contracts_service/contract_event_handler.py @@ -3,14 +3,14 @@ import os import uuid -from datetime import datetime +from datetime import datetime, timezone import boto3 from boto3.dynamodb.conditions import Attr from botocore.exceptions import ClientError from aws_lambda_powertools.logging import Logger -from aws_lambda_powertools.metrics import Metrics +from aws_lambda_powertools.metrics import Metrics, MetricUnit from aws_lambda_powertools.tracing import Tracer from aws_lambda_powertools.utilities.data_classes import event_source, SQSEvent from aws_lambda_powertools.utilities.typing import LambdaContext @@ -76,7 +76,7 @@ def create_contract(event: dict) -> None: DynamoDB put Item response """ - current_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + current_date = datetime.now(timezone.utc).isoformat() contract = { "property_id": event["property_id"], # PK "address": event["address"], @@ -105,6 +105,7 @@ def create_contract(event: dict) -> None: # Annotate trace with contract status tracer.put_annotation(key="ContractStatus", value=contract["contract_status"]) + metrics.add_metric(name="ContractCreated", unit=MetricUnit.Count, value=1) except ClientError as e: code = e.response["Error"]["Code"] @@ -147,13 +148,13 @@ def update_contract(contract: dict) -> None: try: contract["contract_status"] = ContractStatus.APPROVED.name - current_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + current_date = datetime.now(timezone.utc).isoformat() response = table.update_item( Key={ "property_id": contract["property_id"], }, - UpdateExpression="set contract_status=:t, modified_date=:m", + UpdateExpression="set contract_status=:t, contract_last_modified_on=:m", ConditionExpression=Attr("property_id").exists() & Attr("contract_status").is_in([ContractStatus.DRAFT.name]), ExpressionAttributeValues={ From 9f266dd91d487ed2d96c900aae7e8d1536c40a39 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:46:10 +0800 Subject: [PATCH 4/7] chore(deps): bump dependencies to latest versions --- unicorn_contracts/uv.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/unicorn_contracts/uv.lock b/unicorn_contracts/uv.lock index f5ee7b3..1226f38 100644 --- a/unicorn_contracts/uv.lock +++ b/unicorn_contracts/uv.lock @@ -241,16 +241,16 @@ dev = [ [package.metadata] requires-dist = [ { name = "arnparse", marker = "extra == 'dev'", specifier = ">=0.0.2" }, - { name = "aws-lambda-powertools", extras = ["all"], marker = "extra == 'dev'", specifier = ">=3.24.0" }, - { name = "aws-lambda-powertools", extras = ["tracer"], specifier = ">=3.24.0" }, + { name = "aws-lambda-powertools", extras = ["all"], marker = "extra == 'dev'", specifier = ">=3.25.0" }, + { name = "aws-lambda-powertools", extras = ["tracer"], specifier = ">=3.25.0" }, { name = "aws-xray-sdk", specifier = ">=2.15.0" }, - { name = "boto3", specifier = ">=1.42.43" }, + { name = "boto3", specifier = ">=1.42.68" }, { name = "importlib-metadata", marker = "extra == 'dev'", specifier = ">=8.7.1" }, - { name = "moto", extras = ["dynamodb", "events", "sqs"], marker = "extra == 'dev'", specifier = ">=5.1.20" }, + { name = "moto", extras = ["dynamodb", "events", "sqs"], marker = "extra == 'dev'", specifier = ">=5.1.22" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.3" }, { name = "requests", marker = "extra == 'dev'", specifier = ">=2.32.5" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.6" }, { name = "tomli", marker = "extra == 'dev'", specifier = ">=2.4.0" }, ] provides-extras = ["dev"] From 6a0825ace24f4cb6128616c38990de10d1b76b0c Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 14:05:20 +0800 Subject: [PATCH 5/7] ci: Add explicit permissions to workflow jobs - Add actions: read permission for workflow execution access - Add contents: read permission for repository content access - Scope down GitHub Token permissions following security best practices --- .github/workflows/on_merged_pr.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml index 8508999..4fb154c 100644 --- a/.github/workflows/on_merged_pr.yml +++ b/.github/workflows/on_merged_pr.yml @@ -7,6 +7,8 @@ on: - completed permissions: + actions: read + contents: read issues: write jobs: From 6fe463496da2e0e00294bcf581c9655b00cb94ee Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 23 Mar 2026 14:05:40 +0800 Subject: [PATCH 6/7] test: standardize unit tests across all services (31 tests) Add coverage tooling (pytest-cov, 80% minimum), extend test cases for all 7 Lambda functions, and remove extra tests to ensure parity with .NET, Java, and TypeScript runtimes. --- unicorn_approvals/pyproject.toml | 3 +- ...t_contract_status_changed_event_handler.py | 16 ++ .../test_properties_approval_sync_function.py | 73 +++++++- ...est_wait_for_contract_approval_function.py | 40 +++- unicorn_approvals/uv.lock | 85 +++++++++ unicorn_contracts/pyproject.toml | 3 +- .../tests/unit/test_contract_event_handler.py | 85 +++++++-- unicorn_contracts/uv.lock | 85 +++++++++ unicorn_web/pyproject.toml | 7 + ...st_publication_evaluation_event_handler.py | 96 +++++++--- .../unit/test_request_approval_function.py | 125 ++++++------- .../tests/unit/test_search_function.py | 172 ++++++++---------- unicorn_web/uv.lock | 85 +++++++++ 13 files changed, 664 insertions(+), 211 deletions(-) diff --git a/unicorn_approvals/pyproject.toml b/unicorn_approvals/pyproject.toml index f9b5483..0d76891 100644 --- a/unicorn_approvals/pyproject.toml +++ b/unicorn_approvals/pyproject.toml @@ -23,6 +23,7 @@ dev = [ "pyyaml>=6.0.3", "arnparse>=0.0.2", "pytest>=9.0.2", + "pytest-cov>=6.1.1", "ruff>=0.15.6", ] @@ -32,6 +33,6 @@ packages = ["approvals_service"] [tool.pytest.ini_options] minversion = "7.0" -addopts = "-ra -vv -W ignore::UserWarning" +addopts = "-ra -vv -W ignore::UserWarning --cov=src --cov-report=term-missing --cov-fail-under=80" testpaths = ["tests/unit", "tests/integration"] pythonpath = ["."] diff --git a/unicorn_approvals/tests/unit/test_contract_status_changed_event_handler.py b/unicorn_approvals/tests/unit/test_contract_status_changed_event_handler.py index 451883a..3d02b73 100644 --- a/unicorn_approvals/tests/unit/test_contract_status_changed_event_handler.py +++ b/unicorn_approvals/tests/unit/test_contract_status_changed_event_handler.py @@ -41,3 +41,19 @@ def test_missing_property_id(dynamodb, lambda_context): contract_status_changed_event_handler.lambda_handler(eventbridge_event, lambda_context) assert "ValidationException" in str(e.value) + + +@mock.patch.dict(os.environ, return_env_vars_dict({"CONTRACT_STATUS_TABLE": "nonexistent_table"}), clear=True) +def test_dynamodb_failure(dynamodb, lambda_context): + eventbridge_event = load_event("eventbridge/contract_status_changed") + + from approvals_service import contract_status_changed_event_handler + + # Reload is required to prevent function setup reuse from another test + reload(contract_status_changed_event_handler) + + # Do NOT create the table so DynamoDB raises a ResourceNotFoundException + with pytest.raises(ClientError) as e: + contract_status_changed_event_handler.lambda_handler(eventbridge_event, lambda_context) + + assert "ResourceNotFoundException" in str(e.value) diff --git a/unicorn_approvals/tests/unit/test_properties_approval_sync_function.py b/unicorn_approvals/tests/unit/test_properties_approval_sync_function.py index 6d12769..9a3c8bf 100644 --- a/unicorn_approvals/tests/unit/test_properties_approval_sync_function.py +++ b/unicorn_approvals/tests/unit/test_properties_approval_sync_function.py @@ -21,16 +21,71 @@ def test_handle_status_changed_draft(stepfunction, lambda_context): assert ret is None -# NOTE: This test cannot be implemented at this time because `moto`` does not yet support mocking `stepfunctions.send_task_success` @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_handle_status_changed_approved(caplog, stepfunction, lambda_context): - pass - # ddbstream_event = load_event('ddb_stream_events/status_approved_waiting_for_approval') +def test_handle_status_changed_approved(stepfunction, lambda_context): + ddbstream_event = load_event("ddb_stream_events/status_approved_waiting_for_approval") - # from publication_manager_service import properties_approval_sync_function - # reload(properties_approval_sync_function) + from approvals_service import properties_approval_sync_function + + reload(properties_approval_sync_function) + + # Mock the module-level sfn client to bypass moto's lack of send_task_success support + mock_sfn = mock.MagicMock() + mock_sfn.send_task_success.return_value = {"ResponseMetadata": {"HTTPStatusCode": 200}} + + with mock.patch.object(properties_approval_sync_function, "sfn", mock_sfn): + ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context) + + # Verify send_task_success was called with the task token from OldImage + mock_sfn.send_task_success.assert_called_once() + call_kwargs = mock_sfn.send_task_success.call_args + assert "taskToken" in call_kwargs.kwargs or len(call_kwargs.args) > 0 - # ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context) - # assert ret is None - # assert 'Contract status for property is APPROVED' in caplog.text +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_no_task_token_skips(stepfunction, lambda_context): + """APPROVED status but no task token in old or new image => returns None (skip).""" + ddbstream_event = load_event("ddb_stream_events/status_approved_with_no_workflow") + + from approvals_service import properties_approval_sync_function + + reload(properties_approval_sync_function) + + ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context) + + assert ret is None + + +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_missing_new_image_skips(stepfunction, lambda_context): + """Record with missing NewImage key causes a KeyError => handler should raise.""" + ddbstream_event = { + "Records": [ + { + "eventID": "1", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "ap-southeast-2", + "dynamodb": { + "Keys": {"property_id": {"S": "usa/anytown/main-street/999"}}, + "SequenceNumber": "100000000005391461882", + "SizeBytes": 50, + "StreamViewType": "NEW_AND_OLD_IMAGES", + }, + "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/test/stream/2022-08-23T15:46:44.107", + } + ] + } + + from approvals_service import properties_approval_sync_function + + reload(properties_approval_sync_function) + + try: + ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context) + # If handler returns without error when NewImage is missing, that is acceptable (skip behaviour) + assert ret is None + except KeyError: + # KeyError on missing NewImage is also acceptable + pass diff --git a/unicorn_approvals/tests/unit/test_wait_for_contract_approval_function.py b/unicorn_approvals/tests/unit/test_wait_for_contract_approval_function.py index bd7db05..5fa8e4b 100644 --- a/unicorn_approvals/tests/unit/test_wait_for_contract_approval_function.py +++ b/unicorn_approvals/tests/unit/test_wait_for_contract_approval_function.py @@ -3,9 +3,11 @@ import os from importlib import reload +import pytest from unittest import mock +from botocore.exceptions import ClientError -from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts_with_entry +from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts_with_entry, create_ddb_table_properties @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) @@ -26,3 +28,39 @@ def test_handle_wait_for_contract_approval_function(dynamodb, lambda_context): assert ret["property_id"] == stepfunctions_event["Input"]["property_id"] assert ddbitem_after["Item"]["sfn_wait_approved_task_token"] == stepfunctions_event["TaskToken"] + + +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_contract_not_found(dynamodb, lambda_context): + stepfunctions_event = { + "TaskToken": "xxx", + "Input": { + "property_id": "usa/anytown/main-street/999", + }, + } + + from approvals_service import wait_for_contract_approval_function + from approvals_service.exceptions import ContractStatusNotFoundException + + reload(wait_for_contract_approval_function) + + # Create empty table (no entry for the requested property_id) + create_ddb_table_properties(dynamodb) + + with pytest.raises(ContractStatusNotFoundException): + wait_for_contract_approval_function.lambda_handler(stepfunctions_event, lambda_context) + + +@mock.patch.dict(os.environ, return_env_vars_dict({"CONTRACT_STATUS_TABLE": "nonexistent_table"}), clear=True) +def test_dynamodb_failure(dynamodb, lambda_context): + stepfunctions_event = load_event("lambda/wait_for_contract_approval_function") + + from approvals_service import wait_for_contract_approval_function + from approvals_service.exceptions import ContractStatusNotFoundException + + reload(wait_for_contract_approval_function) + + # Do NOT create the table so DynamoDB raises a ResourceNotFoundException + # The handler catches ClientError and raises ContractStatusNotFoundException + with pytest.raises((ClientError, ContractStatusNotFoundException)): + wait_for_contract_approval_function.lambda_handler(stepfunctions_event, lambda_context) diff --git a/unicorn_approvals/uv.lock b/unicorn_approvals/uv.lock index 9ee4d31..929079a 100644 --- a/unicorn_approvals/uv.lock +++ b/unicorn_approvals/uv.lock @@ -28,6 +28,7 @@ dev = [ { name = "importlib-metadata" }, { name = "moto", extra = ["dynamodb", "events"] }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "pyyaml" }, { name = "requests" }, { name = "ruff" }, @@ -43,6 +44,7 @@ requires-dist = [ { name = "importlib-metadata", marker = "extra == 'dev'", specifier = ">=8.7.1" }, { name = "moto", extras = ["dynamodb", "events", "sqs"], marker = "extra == 'dev'", specifier = ">=5.1.22" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" }, { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.3" }, { name = "requests", marker = "extra == 'dev'", specifier = ">=2.32.5" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.6" }, @@ -253,6 +255,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + [[package]] name = "cryptography" version = "46.0.5" @@ -613,6 +684,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" diff --git a/unicorn_contracts/pyproject.toml b/unicorn_contracts/pyproject.toml index fb61605..32cbc74 100644 --- a/unicorn_contracts/pyproject.toml +++ b/unicorn_contracts/pyproject.toml @@ -23,6 +23,7 @@ dev = [ "pyyaml>=6.0.3", "arnparse>=0.0.2", "pytest>=9.0.2", + "pytest-cov>=6.1.1", "ruff>=0.15.6", "tomli>=2.4.0", ] @@ -33,6 +34,6 @@ packages = ["contracts_service"] [tool.pytest.ini_options] minversion = "7.0" -addopts = "-ra -vv -W ignore::UserWarning" +addopts = "-ra -vv -W ignore::UserWarning --cov=src --cov-report=term-missing --cov-fail-under=80" testpaths = ["tests/unit", "tests/integration"] pythonpath = ["."] \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/test_contract_event_handler.py b/unicorn_contracts/tests/unit/test_contract_event_handler.py index d64291d..044f81b 100644 --- a/unicorn_contracts/tests/unit/test_contract_event_handler.py +++ b/unicorn_contracts/tests/unit/test_contract_event_handler.py @@ -5,7 +5,6 @@ import pytest from unittest import mock -from botocore.exceptions import ClientError from .event_generator import sqs_event from .helper import TABLE_NAME @@ -64,33 +63,85 @@ def test_valid_update_event(dynamodb, sqs, lambda_context): @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_ddb_env_var(): - del os.environ["DYNAMODB_TABLE"] - # Loading function here so that mocking works correctly - with pytest.raises(EnvironmentError): - from contracts_service import contract_event_handler +def test_create_contract_already_exists(dynamodb, sqs, lambda_context): + payload = load_event("create_contract_valid_1") + event = sqs_event([{"body": payload, "attributes": {"HttpMethod": "POST"}}]) + + from contracts_service import contract_event_handler # noqa: F401 + + reload(contract_event_handler) + + create_ddb_table_contracts_with_entry(dynamodb) + create_test_sqs_ingestion_queue(sqs) + + # Contract for property_id "usa/anytown/main-street/123" already exists in DRAFT status; + # ConditionalCheckFailedException is caught and logged, not raised. + contract_event_handler.lambda_handler(event, lambda_context) + + res = dynamodb.Table(TABLE_NAME).get_item(Key={"property_id": payload["property_id"]}) + # The original contract should remain unchanged (still DRAFT, same contract_id) + assert res["Item"]["contract_status"] == "DRAFT" + assert res["Item"]["contract_id"] == "11111111" + + +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_update_contract_not_in_draft(dynamodb, sqs, lambda_context): + payload = load_event("update_contract_valid_1") + event = sqs_event([{"body": payload, "attributes": {"HttpMethod": "PUT"}}]) + + from contracts_service import contract_event_handler # noqa: F401 + + reload(contract_event_handler) + + # Create table with an entry already in APPROVED status + table = create_ddb_table_contracts(dynamodb) + create_test_sqs_ingestion_queue(sqs) + table.put_item( + Item={ + "property_id": "usa/anytown/main-street/123", + "contract_created": "01/08/2022 20:36:30", + "contract_last_modified_on": "01/08/2022 20:36:30", + "contract_id": "11111111", + "address": {"country": "USA", "city": "Anytown", "street": "Main Street", "number": 123}, + "seller_name": "John Smith", + "contract_status": "APPROVED", + } + ) + + # ConditionalCheckFailedException is caught and logged, not raised. + contract_event_handler.lambda_handler(event, lambda_context) - reload(contract_event_handler) + res = dynamodb.Table(TABLE_NAME).get_item(Key={"property_id": payload["property_id"]}) + # Status should remain APPROVED (update was rejected) + assert res["Item"]["contract_status"] == "APPROVED" @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -def test_missing_sm_env_var(): - del os.environ["SERVICE_NAMESPACE"] +def test_invalid_http_method(dynamodb, sqs, lambda_context): + payload = load_event("create_contract_valid_1") + event = sqs_event([{"body": payload, "attributes": {"HttpMethod": "DELETE"}}]) + + from contracts_service import contract_event_handler # noqa: F401 + + reload(contract_event_handler) - with pytest.raises(EnvironmentError): - from contracts_service import contract_event_handler + create_ddb_table_contracts(dynamodb) + create_test_sqs_ingestion_queue(sqs) - reload(contract_event_handler) + with pytest.raises(Exception, match="Unable to handle HttpMethod DELETE"): + contract_event_handler.lambda_handler(event, lambda_context) -@mock.patch.dict(os.environ, return_env_vars_dict({"DYNAMODB_TABLE": "table27"}), clear=True) -def test_wrong_dynamodb_table(dynamodb, lambda_context): - event = sqs_event([{"body": load_event("create_contract_valid_1"), "attributes": {"HttpMethod": "POST"}}]) +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_malformed_sqs_body(dynamodb, sqs, lambda_context): + event = sqs_event([{"body": "not a valid dict", "attributes": {"HttpMethod": "POST"}}]) from contracts_service import contract_event_handler # noqa: F401 + reload(contract_event_handler) + create_ddb_table_contracts(dynamodb) + create_test_sqs_ingestion_queue(sqs) - with pytest.raises(ClientError): - reload(contract_event_handler) + with pytest.raises((KeyError, TypeError)): contract_event_handler.lambda_handler(event, lambda_context) diff --git a/unicorn_contracts/uv.lock b/unicorn_contracts/uv.lock index 1226f38..1c7f8f9 100644 --- a/unicorn_contracts/uv.lock +++ b/unicorn_contracts/uv.lock @@ -232,6 +232,7 @@ dev = [ { name = "importlib-metadata" }, { name = "moto", extra = ["dynamodb", "events"] }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "pyyaml" }, { name = "requests" }, { name = "ruff" }, @@ -248,6 +249,7 @@ requires-dist = [ { name = "importlib-metadata", marker = "extra == 'dev'", specifier = ">=8.7.1" }, { name = "moto", extras = ["dynamodb", "events", "sqs"], marker = "extra == 'dev'", specifier = ">=5.1.22" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" }, { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.3" }, { name = "requests", marker = "extra == 'dev'", specifier = ">=2.32.5" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.6" }, @@ -255,6 +257,75 @@ requires-dist = [ ] provides-extras = ["dev"] +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + [[package]] name = "cryptography" version = "46.0.5" @@ -615,6 +686,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" diff --git a/unicorn_web/pyproject.toml b/unicorn_web/pyproject.toml index d650eff..50bfa96 100644 --- a/unicorn_web/pyproject.toml +++ b/unicorn_web/pyproject.toml @@ -24,9 +24,16 @@ dev = [ "pyyaml>=6.0.3", "arnparse>=0.0.2", "pytest>=9.0.2", + "pytest-cov>=6.1.1", "ruff>=0.15.6", ] [tool.setuptools] package-dir = {"web_service" = "src"} packages = ["web_service"] + +[tool.pytest.ini_options] +minversion = "7.0" +addopts = "-ra -vv -W ignore::UserWarning --cov=src --cov-report=term-missing --cov-fail-under=80" +testpaths = ["tests/unit", "tests/integration"] +pythonpath = ["."] diff --git a/unicorn_web/tests/unit/test_publication_evaluation_event_handler.py b/unicorn_web/tests/unit/test_publication_evaluation_event_handler.py index caecd16..a3c4978 100644 --- a/unicorn_web/tests/unit/test_publication_evaluation_event_handler.py +++ b/unicorn_web/tests/unit/test_publication_evaluation_event_handler.py @@ -1,36 +1,86 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 +import os +from importlib import reload -# import os +import pytest +from unittest import mock -# from unittest import mock -# from importlib import reload +from .helper import TABLE_NAME, load_event, return_env_vars_dict, create_ddb_table_property_web -# from .lambda_context import LambdaContext -# from .helper import load_event, return_env_vars_dict, create_ddb_table_property_web +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_property_approved(dynamodb, lambda_context): + eventbridge_event = load_event("property_approved") + property_id = eventbridge_event["detail"]["property_id"] -# def get_property_pk_sk(property_id): -# country, city, street, number = property_id.split('/') -# pk_details = f"{country}#{city}".replace(' ', '-').lower() -# return { -# 'PK': f"PROPERTY#{pk_details}", -# 'SK': f"{street}#{str(number)}".replace(' ', '-').lower(), -# } + from publication_manager_service import publication_evaluation_event_handler + reload(publication_evaluation_event_handler) -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_property_approved(dynamodb, mocker): -# eventbridge_event = load_event('events/property_approved.json') -# property_id = eventbridge_event['detail']['property_id'] + create_ddb_table_property_web(dynamodb) -# import publication_manager_service.publication_approved_event_handler as app -# reload(app) # Reload is required to prevent function setup reuse from another test + ret = publication_evaluation_event_handler.lambda_handler(eventbridge_event, lambda_context) + assert ret["result"] == "Successfully updated property status" -# create_ddb_table_property_web(dynamodb) + # property_id = "usa/anytown/main-street/126" => PK = PROPERTY#usa#anytown, SK = main-street#126 + country, city, street, number = property_id.split("/") + pk = f"PROPERTY#{country}#{city}" + sk = f"{street}#{number}" + ddbitem_after = dynamodb.Table(TABLE_NAME).get_item(Key={"PK": pk, "SK": sk}) + assert ddbitem_after["Item"]["status"] == "APPROVED" -# ret = app.lambda_handler(eventbridge_event, LambdaContext()) # type: ignore -# assert ret['result'] == 'Successfully updated property status' -# ddbitem_after = dynamodb.Table('table1').get_item(Key=get_property_pk_sk(property_id)) -# assert ddbitem_after['Item']['status'] == 'APPROVED' +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_property_declined(dynamodb, lambda_context): + eventbridge_event = load_event("property_approved") + # Change the evaluation result to DECLINED + eventbridge_event["detail"]["evaluation_result"] = "DECLINED" + property_id = eventbridge_event["detail"]["property_id"] + + from publication_manager_service import publication_evaluation_event_handler + + reload(publication_evaluation_event_handler) + + create_ddb_table_property_web(dynamodb) + + ret = publication_evaluation_event_handler.lambda_handler(eventbridge_event, lambda_context) + assert ret["result"] == "Successfully updated property status" + + country, city, street, number = property_id.split("/") + pk = f"PROPERTY#{country}#{city}" + sk = f"{street}#{number}" + ddbitem_after = dynamodb.Table(TABLE_NAME).get_item(Key={"PK": pk, "SK": sk}) + assert ddbitem_after["Item"]["status"] == "DECLINED" + + +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_unknown_result_no_update(dynamodb, lambda_context): + eventbridge_event = load_event("property_approved") + # Set evaluation result to an unknown value + eventbridge_event["detail"]["evaluation_result"] = "UNKNOWN_STATUS" + + from publication_manager_service import publication_evaluation_event_handler + + reload(publication_evaluation_event_handler) + + create_ddb_table_property_web(dynamodb) + + ret = publication_evaluation_event_handler.lambda_handler(eventbridge_event, lambda_context) + assert "Skipped" in ret["result"] + + +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_invalid_property_id(dynamodb, lambda_context): + eventbridge_event = load_event("property_approved") + # Set an invalid property_id that cannot be split into 4 parts + eventbridge_event["detail"]["property_id"] = "invalid" + + from publication_manager_service import publication_evaluation_event_handler + + reload(publication_evaluation_event_handler) + + create_ddb_table_property_web(dynamodb) + + with pytest.raises(ValueError): + publication_evaluation_event_handler.lambda_handler(eventbridge_event, lambda_context) diff --git a/unicorn_web/tests/unit/test_request_approval_function.py b/unicorn_web/tests/unit/test_request_approval_function.py index d9365bf..bdbb490 100644 --- a/unicorn_web/tests/unit/test_request_approval_function.py +++ b/unicorn_web/tests/unit/test_request_approval_function.py @@ -1,13 +1,12 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os - -# import json +import json from importlib import reload -# import pytest +import pytest from unittest import mock -# from botocore.exceptions import ClientError +from botocore.exceptions import ClientError from .event_generator import sqs_event from .helper import TABLE_NAME @@ -53,85 +52,87 @@ def test_valid_event(dynamodb, eventbridge, sqs, lambda_context): assert res["Item"]["street"] == "Main Street" -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_broken_input_event(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/request_approval_bad_input.json') - -# # Loading function here so that mocking works correctly. -# import publication_manager_service.request_approval_function as app - -# # Reload is required to prevent function setup reuse from another test -# reload(app) +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_invalid_property_id_format(dynamodb, eventbridge, sqs, lambda_context): + # Property ID with uppercase letter does not match the regex + payload = {"property_id": "usa/anytown/Main-street/122"} + event = sqs_event([{"body": payload, "attributes": {"HttpMethod": "POST"}}]) -# create_ddb_table_property_web(dynamodb) + from publication_manager_service import request_approval_function -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) + reload(request_approval_function) -# assert ret['statusCode'] == 400 -# assert 'message' in data.keys() -# assert 'unable' in data['message'].lower() + create_ddb_table_property_web(dynamodb) + create_test_eventbridge_bus(eventbridge) + create_test_sqs_ingestion_queue(sqs) + # get_keys_for_property returns ("", "") for invalid property_id, + # then get_property queries DynamoDB with empty PK which raises ClientError (ValidationException) + with pytest.raises((KeyError, ClientError)): + request_approval_function.lambda_handler(event, lambda_context) -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_invalid_property_id(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/request_invalid_property_id.json') -# # Loading function here so that mocking works correctly. -# import publication_manager_service.request_approval_function as app +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_already_approved_skips_eventbridge(dynamodb, eventbridge, sqs, lambda_context): + # property at main-street/124 has status APPROVED in the test table + payload = {"property_id": "usa/anytown/main-street/124"} + event = sqs_event([{"body": payload, "attributes": {"HttpMethod": "POST"}}]) -# # Reload is required to prevent function setup reuse from another test -# reload(app) + from publication_manager_service import request_approval_function -# create_ddb_table_property_web(dynamodb) + reload(request_approval_function) -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) + table = create_ddb_table_property_web(dynamodb) + create_test_eventbridge_bus(eventbridge) + create_test_sqs_ingestion_queue(sqs) -# assert ret['statusCode'] == 400 -# assert 'message' in data.keys() -# assert 'invalid' in data['message'].lower() + # Update the item to APPROVED status so the function skips + table.update_item( + Key={"PK": "PROPERTY#usa#anytown", "SK": "main-street#124"}, + AttributeUpdates={"status": {"Value": "APPROVED", "Action": "PUT"}}, + ) + # Should complete without error and without publishing to EventBridge + request_approval_function.lambda_handler(event, lambda_context) -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_already_approved(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/request_already_approved.json') + # Verify the status in DDB is still APPROVED (not changed to PENDING) + res = dynamodb.Table(TABLE_NAME).get_item( + Key={"PK": "PROPERTY#usa#anytown", "SK": "main-street#124"} + ) + assert res["Item"]["status"] == "APPROVED" -# # Loading function here so that mocking works correctly. -# import publication_manager_service.request_approval_function as app -# # Reload is required to prevent function setup reuse from another test -# reload(app) +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_property_not_found(dynamodb, eventbridge, sqs, lambda_context): + # Property usa/anytown/main-street/999 does not exist in the test data + payload = {"property_id": "usa/anytown/main-street/999"} + event = sqs_event([{"body": payload, "attributes": {"HttpMethod": "POST"}}]) -# create_ddb_table_property_web(dynamodb) + from publication_manager_service import request_approval_function -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) + reload(request_approval_function) -# assert ret['statusCode'] == 200 -# assert 'result' in data.keys() -# assert 'already' in data['result'].lower() + create_ddb_table_property_web(dynamodb) + create_test_eventbridge_bus(eventbridge) + create_test_sqs_ingestion_queue(sqs) + # get_property returns empty dict, then item.pop("status") raises KeyError + with pytest.raises(KeyError): + request_approval_function.lambda_handler(event, lambda_context) -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_property_does_not_exist(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/request_non_existent_property.json') -# # Loading function here so that mocking works correctly. -# import publication_manager_service.request_approval_function as app +@mock.patch.dict(os.environ, return_env_vars_dict({"EVENT_BUS": "nonexistent_bus"}), clear=True) +def test_eventbridge_failure(dynamodb, eventbridge, sqs, lambda_context): + payload = load_event("request_approval_event") + event = sqs_event([{"body": payload, "attributes": {"HttpMethod": "POST"}}]) -# # Reload is required to prevent function setup reuse from another test -# reload(app) + from publication_manager_service import request_approval_function -# create_ddb_table_property_web(dynamodb) + reload(request_approval_function) -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) + create_ddb_table_property_web(dynamodb) + # Intentionally do NOT create the event bus so put_events fails + create_test_sqs_ingestion_queue(sqs) -# assert ret['statusCode'] == 404 -# assert 'message' in data.keys() -# assert 'no property found' in data['message'].lower() + with pytest.raises(Exception, match="Unable to send event to Event Bus|Error sending requests to Event Bus"): + request_approval_function.lambda_handler(event, lambda_context) diff --git a/unicorn_web/tests/unit/test_search_function.py b/unicorn_web/tests/unit/test_search_function.py index daceddd..784e2c1 100644 --- a/unicorn_web/tests/unit/test_search_function.py +++ b/unicorn_web/tests/unit/test_search_function.py @@ -1,143 +1,121 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 +import os +import json +from importlib import reload -# import os -# import json +from unittest import mock -# from unittest import mock -# from importlib import reload +from .helper import load_event, return_env_vars_dict, create_ddb_table_property_web -# from .lambda_context import LambdaContext -# from .helper import load_event, return_env_vars_dict, create_ddb_table_property_web +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_search_by_city(dynamodb, lambda_context): + apigw_event = load_event("search_by_city") -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_search_by_street(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/search_by_street_event.json') + from search_service import property_search_function -# # Loading function here so that mocking works correctly. -# import search_service.property_search_function as app + reload(property_search_function) -# # Reload is required to prevent function setup reuse from another test -# reload(app) + create_ddb_table_property_web(dynamodb) -# create_ddb_table_property_web(dynamodb) + ret = property_search_function.lambda_handler(apigw_event, lambda_context) + data = json.loads(ret["body"]) -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) + assert ret["statusCode"] == 200 + assert type(data) == list + # Only APPROVED items are returned; test data has 1 APPROVED item (main-street#124) + assert len(data) == 1 + item = data[0] + assert item["city"] == "Anytown" + assert item["number"] == "124" -# assert ret['statusCode'] == 200 -# assert type(data) == list -# assert len(data) == 1 -# item = data[0] -# assert item['city'] == 'Anytown' -# assert item['number'] == '124' +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_search_by_city_and_street(dynamodb, lambda_context): + apigw_event = load_event("search_by_street_event") -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_search_by_city(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/search_by_city.json') + from search_service import property_search_function -# # Loading function here so that mocking works correctly. -# import search_service.property_search_function as app + reload(property_search_function) -# # Reload is required to prevent function setup reuse from another test -# reload(app) + create_ddb_table_property_web(dynamodb) -# create_ddb_table_property_web(dynamodb) + ret = property_search_function.lambda_handler(apigw_event, lambda_context) + data = json.loads(ret["body"]) -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) + assert ret["statusCode"] == 200 + assert type(data) == list + # Only APPROVED items with SK beginning with "main-street#" are returned + assert len(data) == 1 + item = data[0] + assert item["city"] == "Anytown" + assert item["number"] == "124" -# assert ret['statusCode'] == 200 -# assert type(data) == list -# assert len(data) == 1 -# item = data[0] -# assert item['city'] == 'Anytown' -# assert item['number'] == '124' +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_property_details_happy_path(dynamodb, lambda_context): + apigw_event = load_event("search_by_full_address") -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_search_full_address(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/search_by_full_address.json') + from search_service import property_search_function -# # Loading function here so that mocking works correctly. -# import search_service.property_search_function as app + reload(property_search_function) -# # Reload is required to prevent function setup reuse from another test -# reload(app) + create_ddb_table_property_web(dynamodb) -# create_ddb_table_property_web(dynamodb) + ret = property_search_function.lambda_handler(apigw_event, lambda_context) + data = json.loads(ret["body"]) -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) + assert ret["statusCode"] == 200 + assert data["city"] == "Anytown" + assert data["number"] == "124" -# assert ret['statusCode'] == 200 -# assert data['city'] == 'Anytown' -# assert data['number'] == '124' +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_property_not_found_returns_404(dynamodb, lambda_context): + apigw_event = load_event("search_by_full_address_not_found") -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_search_full_address_declined(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/search_by_full_address_declined.json') + from search_service import property_search_function -# # Loading function here so that mocking works correctly. -# import search_service.property_search_function as app + reload(property_search_function) -# # Reload is required to prevent function setup reuse from another test -# reload(app) + create_ddb_table_property_web(dynamodb) -# create_ddb_table_property_web(dynamodb) + ret = property_search_function.lambda_handler(apigw_event, lambda_context) -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) + assert ret["statusCode"] == 404 -# assert ret['statusCode'] == 404 -# assert 'message' in data -# assert 'declined' in data['message'].lower() +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_property_not_approved_returns_404(dynamodb, lambda_context): + # main-street/125 has status DECLINED in test data + apigw_event = load_event("search_by_full_address_declined") -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_search_full_address_new(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/search_by_full_address_new.json') + from search_service import property_search_function -# # Loading function here so that mocking works correctly. -# import search_service.property_search_function as app + reload(property_search_function) -# # Reload is required to prevent function setup reuse from another test -# reload(app) + create_ddb_table_property_web(dynamodb) -# create_ddb_table_property_web(dynamodb) + ret = property_search_function.lambda_handler(apigw_event, lambda_context) -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) + assert ret["statusCode"] == 404 -# assert ret['statusCode'] == 404 -# assert 'message' in data -# assert 'new' in data['message'].lower() +@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) +def test_non_get_method_returns_error(dynamodb, lambda_context): + apigw_event = load_event("search_by_city") + # Change method to POST which is not supported by any route + apigw_event["httpMethod"] = "POST" + apigw_event["requestContext"]["httpMethod"] = "POST" -# @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) -# def test_search_full_address_not_found(dynamodb, eventbridge, mocker): -# apigw_event = load_event('events/search_by_full_address_not_found.json') + from search_service import property_search_function -# # Loading function here so that mocking works correctly. -# import search_service.property_search_function as app + reload(property_search_function) -# # Reload is required to prevent function setup reuse from another test -# reload(app) + create_ddb_table_property_web(dynamodb) -# create_ddb_table_property_web(dynamodb) + ret = property_search_function.lambda_handler(apigw_event, lambda_context) -# context = LambdaContext() -# ret = app.lambda_handler(apigw_event, context) # type: ignore -# data = json.loads(ret['body']) - -# assert ret['statusCode'] == 404 -# assert 'message' in data -# assert 'not found' in data['message'].lower() + # ApiGatewayResolver returns 404 for unmatched routes (no POST handler) + assert ret["statusCode"] in (400, 404, 405) diff --git a/unicorn_web/uv.lock b/unicorn_web/uv.lock index 65735ad..071ed3e 100644 --- a/unicorn_web/uv.lock +++ b/unicorn_web/uv.lock @@ -215,6 +215,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + [[package]] name = "crhelper" version = "2.0.12" @@ -584,6 +653,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -778,6 +861,7 @@ dev = [ { name = "importlib-metadata" }, { name = "moto", extra = ["dynamodb", "events"] }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "pyyaml" }, { name = "ruff" }, ] @@ -793,6 +877,7 @@ requires-dist = [ { name = "importlib-metadata", marker = "extra == 'dev'", specifier = ">=8.7.1" }, { name = "moto", extras = ["dynamodb", "events", "sqs"], marker = "extra == 'dev'", specifier = ">=5.1.22" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" }, { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.3" }, { name = "requests", specifier = ">=2.32.5" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.6" }, From 298c4b729ca63e09609e6ed506bed2d45bcc9087 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Thu, 26 Mar 2026 00:43:39 +0800 Subject: [PATCH 7/7] test(contracts): standardize test event payloads with JSON fixtures Align event fixture values (address number 111, seller "John Doe") to match other runtimes. Remove unused create_contract_invalid_1.json. Update helper seed data and inline test data to use consistent values. --- .../tests/unit/events/create_contract_invalid_1.json | 5 ----- .../tests/unit/events/create_contract_valid_1.json | 6 +++--- .../tests/unit/events/update_contract_valid_1.json | 2 +- unicorn_contracts/tests/unit/helper.py | 6 +++--- .../tests/unit/test_contract_event_handler.py | 8 ++++---- 5 files changed, 11 insertions(+), 16 deletions(-) delete mode 100644 unicorn_contracts/tests/unit/events/create_contract_invalid_1.json diff --git a/unicorn_contracts/tests/unit/events/create_contract_invalid_1.json b/unicorn_contracts/tests/unit/events/create_contract_invalid_1.json deleted file mode 100644 index 1177ce8..0000000 --- a/unicorn_contracts/tests/unit/events/create_contract_invalid_1.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "add": "St.1 , Building 10", - "seller": "John Smith", - "property": "4781231c-bc30-4f30-8b30-7145f4dd1adb" -} diff --git a/unicorn_contracts/tests/unit/events/create_contract_valid_1.json b/unicorn_contracts/tests/unit/events/create_contract_valid_1.json index 2b6245b..0d8ca64 100644 --- a/unicorn_contracts/tests/unit/events/create_contract_valid_1.json +++ b/unicorn_contracts/tests/unit/events/create_contract_valid_1.json @@ -3,8 +3,8 @@ "country": "USA", "city": "Anytown", "street": "Main Street", - "number": 123 + "number": 111 }, - "seller_name": "John Smith", - "property_id": "usa/anytown/main-street/123" + "seller_name": "John Doe", + "property_id": "usa/anytown/main-street/111" } diff --git a/unicorn_contracts/tests/unit/events/update_contract_valid_1.json b/unicorn_contracts/tests/unit/events/update_contract_valid_1.json index b960967..6d71d6a 100644 --- a/unicorn_contracts/tests/unit/events/update_contract_valid_1.json +++ b/unicorn_contracts/tests/unit/events/update_contract_valid_1.json @@ -1,3 +1,3 @@ { - "property_id": "usa/anytown/main-street/123" + "property_id": "usa/anytown/main-street/111" } \ No newline at end of file diff --git a/unicorn_contracts/tests/unit/helper.py b/unicorn_contracts/tests/unit/helper.py index aad7bf7..7f65b07 100644 --- a/unicorn_contracts/tests/unit/helper.py +++ b/unicorn_contracts/tests/unit/helper.py @@ -61,12 +61,12 @@ def create_ddb_table_contracts_with_entry(dynamodb): ) table.meta.client.get_waiter("table_exists").wait(TableName=TABLE_NAME) contract = { - "property_id": "usa/anytown/main-street/123", # PK + "property_id": "usa/anytown/main-street/111", # PK "contract_created": "01/08/2022 20:36:30", "contract_last_modified_on": "01/08/2022 20:36:30", "contract_id": "11111111", - "address": {"country": "USA", "city": "Anytown", "street": "Main Street", "number": 123}, - "seller_name": "John Smith", + "address": {"country": "USA", "city": "Anytown", "street": "Main Street", "number": 111}, + "seller_name": "John Doe", "contract_status": "DRAFT", } table.put_item(Item=contract) diff --git a/unicorn_contracts/tests/unit/test_contract_event_handler.py b/unicorn_contracts/tests/unit/test_contract_event_handler.py index 044f81b..46b612f 100644 --- a/unicorn_contracts/tests/unit/test_contract_event_handler.py +++ b/unicorn_contracts/tests/unit/test_contract_event_handler.py @@ -74,7 +74,7 @@ def test_create_contract_already_exists(dynamodb, sqs, lambda_context): create_ddb_table_contracts_with_entry(dynamodb) create_test_sqs_ingestion_queue(sqs) - # Contract for property_id "usa/anytown/main-street/123" already exists in DRAFT status; + # Contract for property_id "usa/anytown/main-street/111" already exists in DRAFT status; # ConditionalCheckFailedException is caught and logged, not raised. contract_event_handler.lambda_handler(event, lambda_context) @@ -98,12 +98,12 @@ def test_update_contract_not_in_draft(dynamodb, sqs, lambda_context): create_test_sqs_ingestion_queue(sqs) table.put_item( Item={ - "property_id": "usa/anytown/main-street/123", + "property_id": "usa/anytown/main-street/111", "contract_created": "01/08/2022 20:36:30", "contract_last_modified_on": "01/08/2022 20:36:30", "contract_id": "11111111", - "address": {"country": "USA", "city": "Anytown", "street": "Main Street", "number": 123}, - "seller_name": "John Smith", + "address": {"country": "USA", "city": "Anytown", "street": "Main Street", "number": 111}, + "seller_name": "John Doe", "contract_status": "APPROVED", } )