Skip to content

Commit 6fe4634

Browse files
committed
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.
1 parent 6a0825a commit 6fe4634

13 files changed

Lines changed: 664 additions & 211 deletions

unicorn_approvals/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dev = [
2323
"pyyaml>=6.0.3",
2424
"arnparse>=0.0.2",
2525
"pytest>=9.0.2",
26+
"pytest-cov>=6.1.1",
2627
"ruff>=0.15.6",
2728
]
2829

@@ -32,6 +33,6 @@ packages = ["approvals_service"]
3233

3334
[tool.pytest.ini_options]
3435
minversion = "7.0"
35-
addopts = "-ra -vv -W ignore::UserWarning"
36+
addopts = "-ra -vv -W ignore::UserWarning --cov=src --cov-report=term-missing --cov-fail-under=80"
3637
testpaths = ["tests/unit", "tests/integration"]
3738
pythonpath = ["."]

unicorn_approvals/tests/unit/test_contract_status_changed_event_handler.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,19 @@ def test_missing_property_id(dynamodb, lambda_context):
4141
contract_status_changed_event_handler.lambda_handler(eventbridge_event, lambda_context)
4242

4343
assert "ValidationException" in str(e.value)
44+
45+
46+
@mock.patch.dict(os.environ, return_env_vars_dict({"CONTRACT_STATUS_TABLE": "nonexistent_table"}), clear=True)
47+
def test_dynamodb_failure(dynamodb, lambda_context):
48+
eventbridge_event = load_event("eventbridge/contract_status_changed")
49+
50+
from approvals_service import contract_status_changed_event_handler
51+
52+
# Reload is required to prevent function setup reuse from another test
53+
reload(contract_status_changed_event_handler)
54+
55+
# Do NOT create the table so DynamoDB raises a ResourceNotFoundException
56+
with pytest.raises(ClientError) as e:
57+
contract_status_changed_event_handler.lambda_handler(eventbridge_event, lambda_context)
58+
59+
assert "ResourceNotFoundException" in str(e.value)

unicorn_approvals/tests/unit/test_properties_approval_sync_function.py

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,71 @@ def test_handle_status_changed_draft(stepfunction, lambda_context):
2121
assert ret is None
2222

2323

24-
# NOTE: This test cannot be implemented at this time because `moto`` does not yet support mocking `stepfunctions.send_task_success`
2524
@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True)
26-
def test_handle_status_changed_approved(caplog, stepfunction, lambda_context):
27-
pass
28-
# ddbstream_event = load_event('ddb_stream_events/status_approved_waiting_for_approval')
25+
def test_handle_status_changed_approved(stepfunction, lambda_context):
26+
ddbstream_event = load_event("ddb_stream_events/status_approved_waiting_for_approval")
2927

30-
# from publication_manager_service import properties_approval_sync_function
31-
# reload(properties_approval_sync_function)
28+
from approvals_service import properties_approval_sync_function
29+
30+
reload(properties_approval_sync_function)
31+
32+
# Mock the module-level sfn client to bypass moto's lack of send_task_success support
33+
mock_sfn = mock.MagicMock()
34+
mock_sfn.send_task_success.return_value = {"ResponseMetadata": {"HTTPStatusCode": 200}}
35+
36+
with mock.patch.object(properties_approval_sync_function, "sfn", mock_sfn):
37+
ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context)
38+
39+
# Verify send_task_success was called with the task token from OldImage
40+
mock_sfn.send_task_success.assert_called_once()
41+
call_kwargs = mock_sfn.send_task_success.call_args
42+
assert "taskToken" in call_kwargs.kwargs or len(call_kwargs.args) > 0
3243

33-
# ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context)
3444

35-
# assert ret is None
36-
# assert 'Contract status for property is APPROVED' in caplog.text
45+
@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True)
46+
def test_no_task_token_skips(stepfunction, lambda_context):
47+
"""APPROVED status but no task token in old or new image => returns None (skip)."""
48+
ddbstream_event = load_event("ddb_stream_events/status_approved_with_no_workflow")
49+
50+
from approvals_service import properties_approval_sync_function
51+
52+
reload(properties_approval_sync_function)
53+
54+
ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context)
55+
56+
assert ret is None
57+
58+
59+
@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True)
60+
def test_missing_new_image_skips(stepfunction, lambda_context):
61+
"""Record with missing NewImage key causes a KeyError => handler should raise."""
62+
ddbstream_event = {
63+
"Records": [
64+
{
65+
"eventID": "1",
66+
"eventName": "MODIFY",
67+
"eventVersion": "1.1",
68+
"eventSource": "aws:dynamodb",
69+
"awsRegion": "ap-southeast-2",
70+
"dynamodb": {
71+
"Keys": {"property_id": {"S": "usa/anytown/main-street/999"}},
72+
"SequenceNumber": "100000000005391461882",
73+
"SizeBytes": 50,
74+
"StreamViewType": "NEW_AND_OLD_IMAGES",
75+
},
76+
"eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/test/stream/2022-08-23T15:46:44.107",
77+
}
78+
]
79+
}
80+
81+
from approvals_service import properties_approval_sync_function
82+
83+
reload(properties_approval_sync_function)
84+
85+
try:
86+
ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context)
87+
# If handler returns without error when NewImage is missing, that is acceptable (skip behaviour)
88+
assert ret is None
89+
except KeyError:
90+
# KeyError on missing NewImage is also acceptable
91+
pass

unicorn_approvals/tests/unit/test_wait_for_contract_approval_function.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import os
44
from importlib import reload
55

6+
import pytest
67
from unittest import mock
8+
from botocore.exceptions import ClientError
79

8-
from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts_with_entry
10+
from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts_with_entry, create_ddb_table_properties
911

1012

1113
@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):
2628

2729
assert ret["property_id"] == stepfunctions_event["Input"]["property_id"]
2830
assert ddbitem_after["Item"]["sfn_wait_approved_task_token"] == stepfunctions_event["TaskToken"]
31+
32+
33+
@mock.patch.dict(os.environ, return_env_vars_dict(), clear=True)
34+
def test_contract_not_found(dynamodb, lambda_context):
35+
stepfunctions_event = {
36+
"TaskToken": "xxx",
37+
"Input": {
38+
"property_id": "usa/anytown/main-street/999",
39+
},
40+
}
41+
42+
from approvals_service import wait_for_contract_approval_function
43+
from approvals_service.exceptions import ContractStatusNotFoundException
44+
45+
reload(wait_for_contract_approval_function)
46+
47+
# Create empty table (no entry for the requested property_id)
48+
create_ddb_table_properties(dynamodb)
49+
50+
with pytest.raises(ContractStatusNotFoundException):
51+
wait_for_contract_approval_function.lambda_handler(stepfunctions_event, lambda_context)
52+
53+
54+
@mock.patch.dict(os.environ, return_env_vars_dict({"CONTRACT_STATUS_TABLE": "nonexistent_table"}), clear=True)
55+
def test_dynamodb_failure(dynamodb, lambda_context):
56+
stepfunctions_event = load_event("lambda/wait_for_contract_approval_function")
57+
58+
from approvals_service import wait_for_contract_approval_function
59+
from approvals_service.exceptions import ContractStatusNotFoundException
60+
61+
reload(wait_for_contract_approval_function)
62+
63+
# Do NOT create the table so DynamoDB raises a ResourceNotFoundException
64+
# The handler catches ClientError and raises ContractStatusNotFoundException
65+
with pytest.raises((ClientError, ContractStatusNotFoundException)):
66+
wait_for_contract_approval_function.lambda_handler(stepfunctions_event, lambda_context)

0 commit comments

Comments
 (0)