Skip to content

Commit 19b16de

Browse files
committed
CCM-12616: mesh-poll lambda
1 parent dc0f7cf commit 19b16de

28 files changed

+3892
-1254
lines changed

Makefile

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ include scripts/init.mk
99

1010
quick-start: config clean build serve-docs # Quick start target to setup, build and serve docs @Pipeline
1111

12-
dependencies: # Install dependencies needed to build and test the project @Pipeline
13-
# TODO: Implement installation of your project dependencies
12+
dependencies:: # Install dependencies needed to build and test the project @Pipeline
13+
$(MAKE) -C docs install
14+
$(MAKE) -C src/cloudevents install
15+
$(MAKE) -C src/eventcatalogasyncapiimporter install
16+
$(MAKE) -C lambdas/mesh-poll install
17+
$(MAKE) -C lambdas/mesh-download install
18+
$(MAKE) -C utils/metric-publishers install
19+
$(MAKE) -C utils/event-publisher-py install
20+
$(MAKE) -C utils/py-mock-mesh install
21+
npm install --workspaces
1422

1523
build: # Build the project artefact @Pipeline
1624
$(MAKE) -C docs build
@@ -29,13 +37,15 @@ clean:: # Clean-up project resources (main) @Operations
2937
$(MAKE) -C src/cloudevents clean
3038
$(MAKE) -C src/eventcatalogasyncapiimporter clean
3139
$(MAKE) -C src/eventcatalogasyncapiimporter clean-output
40+
$(MAKE) -C lambdas/mesh-poll clean
41+
$(MAKE) -C lambdas/mesh-download clean
42+
$(MAKE) -C utils/metric-publishers clean
43+
$(MAKE) -C utils/event-publisher-py clean
44+
$(MAKE) -C utils/py-mock-mesh clean
3245
rm -f .version
3346
# TODO: Implement project resources clean-up step
3447

35-
config:: _install-dependencies version # Configure development environment (main) @Configuration
36-
$(MAKE) -C docs install
37-
$(MAKE) -C src/cloudevents install
38-
$(MAKE) -C src/eventcatalogasyncapiimporter install
48+
config:: _install-dependencies version dependencies # Configure development environment (main) @Configuration
3949

4050
serve-docs:
4151
$(MAKE) -C docs s

lambdas/mesh-poll/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ test-coverage:
1414
poetry run pytest --cov --cov-report xml:target/coverage/@comms/mesh-poll-lambda/cobertura-coverage.xml
1515

1616
lint:
17-
poetry run pylint src
17+
poetry run pylint mesh_poll
1818

1919
format:
2020
poetry run autopep8 -ri .

lambdas/mesh-poll/package.json

Lines changed: 0 additions & 26 deletions
This file was deleted.

lambdas/mesh-poll/poetry.lock

Lines changed: 322 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lambdas/mesh-poll/pyproject.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,20 @@ authors = ["Your Name <[email protected]>"]
66
packages = [{include = "*", from = "src"}]
77

88
[tool.poetry.dependencies]
9+
certifi = ">= 2023.07.22"
910
python = "^3.9"
11+
Mesh-Client = "^3.2.3"
1012
structlog = "^21.5.0"
13+
orjson = "^3.9.15"
14+
metric-publishers = {path = "../../utils/metric-publishers", develop = false}
15+
event-publisher-py = {path = "../../utils/event-publisher-py", develop = false}
1116
py-mock-mesh = {path = "../../utils/py-mock-mesh", develop = false}
17+
boto3 = "^1.28.62"
18+
urllib3 = "^1.26.19"
19+
idna = "^3.7"
20+
requests = "^2.32.0"
21+
pyOpenSSL = "^24.2.1"
22+
setuptools = ">=78.1.1"
1223

1324
[tool.poetry.group.dev.dependencies]
1425
autopep8 = "^2.0.2"

lambdas/mesh-poll/pytest.ini

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[pytest]
2+
testpaths = src/__tests__
3+
python_files = test_*.py
4+
python_classes = Test*
5+
python_functions = test_*
6+
addopts = -v --tb=short
7+
env =
8+
AWS_ACCESS_KEY_ID=testing
9+
AWS_SECRET_ACCESS_KEY=testing
10+
AWS_DEFAULT_REGION=eu-west-2
11+
SSM_PREFIX=/dl/test/mesh
12+
SSM_CLIENTS_PARAMETER_PATH=/dl/test/mesh/clients
13+
INBOX_WORKFLOW_ID=TESTINBOX
14+
OUTBOX_WORKFLOW_ID=TESTOUTBOX
15+
MAXIMUM_RUNTIME_MILLISECONDS=240000
16+
ENVIRONMENT=dev
17+
DEPLOYMENT=dev-1
18+
EVENT_PUBLISHER_EVENT_BUS_ARN=arn:aws:events:eu-west-2:123456789012:event-bus/test-bus
19+
EVENT_PUBLISHER_DLQ_URL=https://sqs.eu-west-2.amazonaws.com/123456789012/test-dlq
20+
CERTIFICATE_EXPIRY_METRIC_NAME=test-cert-expiry
21+
CERTIFICATE_EXPIRY_METRIC_NAMESPACE=test-namespace
22+
POLLING_METRIC_NAME=test-polling
23+
POLLING_METRIC_NAMESPACE=test-polling-namespace
24+
USE_MESH_MOCK=true

lambdas/mesh-poll/src/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
"""Mesh Poll"""
1+
"""
2+
MESH Poll Lambda
3+
4+
This module handles polling MESH inbox for new messages and publishing events.
5+
"""
26

37
__version__ = '0.1.0'
48
from .config import *
59
from .handler import *
10+
from .processor import *
11+
from .client_lookup import *
12+
from .errors import *
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Test package init
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
"""
2+
Tests for ClientLookup
3+
"""
4+
import json
5+
import pytest
6+
from unittest.mock import Mock, call
7+
8+
9+
def setup_mocks():
10+
11+
ssm = Mock()
12+
13+
config = Mock()
14+
config.ssm_clients_parameter_path = "/dl/test/mesh/clients"
15+
16+
logger = Mock()
17+
18+
return ssm, config, logger
19+
20+
21+
def create_client_parameter(client_id, mailbox_id):
22+
23+
return {
24+
"Name": f"/dl/test/mesh/clients/{client_id}",
25+
"Value": json.dumps({
26+
"clientId": client_id,
27+
"meshMailboxSenderId": mailbox_id,
28+
"name": f"Test Client {client_id}"
29+
})
30+
}
31+
32+
33+
class TestClientLookup:
34+
"""Test suite for ClientLookup"""
35+
36+
def test_load_valid_senders_single_page(self):
37+
"""Test loading valid senders from SSM (single page)"""
38+
from src.client_lookup import ClientLookup
39+
40+
ssm, config, logger = setup_mocks()
41+
42+
ssm.get_parameters_by_path.return_value = {
43+
"Parameters": [
44+
create_client_parameter("client1", "MAILBOX_001"),
45+
create_client_parameter("client2", "MAILBOX_002"),
46+
create_client_parameter("client3", "MAILBOX_003"),
47+
]
48+
}
49+
50+
client_lookup = ClientLookup(ssm, config, logger)
51+
52+
ssm.get_parameters_by_path.assert_called_once_with(
53+
Path="/dl/test/mesh/clients/",
54+
WithDecryption=True
55+
)
56+
assert client_lookup.is_valid_sender("MAILBOX_001")
57+
assert client_lookup.is_valid_sender("MAILBOX_002")
58+
assert client_lookup.is_valid_sender("MAILBOX_003")
59+
assert not client_lookup.is_valid_sender("UNKNOWN_MAILBOX")
60+
61+
def test_load_valid_senders_multiple_pages(self):
62+
"""Test loading valid senders from SSM with pagination"""
63+
from src.client_lookup import ClientLookup
64+
65+
ssm, config, logger = setup_mocks()
66+
67+
# Simulate paginated response
68+
ssm.get_parameters_by_path.side_effect = [
69+
{
70+
"Parameters": [
71+
create_client_parameter("client1", "MAILBOX_001"),
72+
create_client_parameter("client2", "MAILBOX_002"),
73+
],
74+
"NextToken": "token123"
75+
},
76+
{
77+
"Parameters": [
78+
create_client_parameter("client3", "MAILBOX_003"),
79+
],
80+
}
81+
]
82+
83+
client_lookup = ClientLookup(ssm, config, logger)
84+
85+
assert ssm.get_parameters_by_path.call_count == 2
86+
ssm.get_parameters_by_path.assert_has_calls([
87+
call(Path="/dl/test/mesh/clients/", WithDecryption=True),
88+
call(Path="/dl/test/mesh/clients/",
89+
WithDecryption=True, NextToken="token123")
90+
])
91+
assert client_lookup.is_valid_sender("MAILBOX_001")
92+
assert client_lookup.is_valid_sender("MAILBOX_002")
93+
assert client_lookup.is_valid_sender("MAILBOX_003")
94+
95+
def test_is_valid_sender_case_insensitive(self):
96+
"""Test that sender validation is case-insensitive"""
97+
from src.client_lookup import ClientLookup
98+
99+
ssm, config, logger = setup_mocks()
100+
101+
ssm.get_parameters_by_path.return_value = {
102+
"Parameters": [
103+
create_client_parameter("client1", "Mailbox_MixedCase"),
104+
]
105+
}
106+
107+
client_lookup = ClientLookup(ssm, config, logger)
108+
109+
assert client_lookup.is_valid_sender("Mailbox_MixedCase")
110+
assert client_lookup.is_valid_sender("MAILBOX_MIXEDCASE")
111+
assert client_lookup.is_valid_sender("mailbox_mixedcase")
112+
assert client_lookup.is_valid_sender("mAiLbOx_MiXeDcAsE")
113+
114+
def test_is_valid_sender_returns_false_for_empty_mailbox_id(self):
115+
"""Test that empty mailbox IDs are rejected"""
116+
from src.client_lookup import ClientLookup
117+
118+
ssm, config, logger = setup_mocks()
119+
120+
ssm.get_parameters_by_path.return_value = {
121+
"Parameters": [
122+
create_client_parameter("client1", "MAILBOX_001"),
123+
]
124+
}
125+
126+
client_lookup = ClientLookup(ssm, config, logger)
127+
128+
assert not client_lookup.is_valid_sender("")
129+
assert not client_lookup.is_valid_sender(None)
130+
131+
def test_load_valid_senders_handles_malformed_json(self):
132+
"""Test that malformed JSON in parameters is handled gracefully"""
133+
from src.client_lookup import ClientLookup
134+
135+
ssm, config, logger = setup_mocks()
136+
137+
ssm.get_parameters_by_path.return_value = {
138+
"Parameters": [
139+
create_client_parameter("client1", "MAILBOX_001"),
140+
{
141+
"Name": "/dl/test/mesh/clients/bad_client",
142+
"Value": "not valid json {{"
143+
},
144+
create_client_parameter("client3", "MAILBOX_003"),
145+
]
146+
}
147+
148+
client_lookup = ClientLookup(ssm, config, logger)
149+
150+
assert client_lookup.is_valid_sender("MAILBOX_001")
151+
assert client_lookup.is_valid_sender("MAILBOX_003")
152+
logger.warn.assert_called()
153+
154+
def test_load_valid_senders_handles_missing_mailbox_id(self):
155+
"""Test that parameters without meshMailboxSenderId are skipped"""
156+
from src.client_lookup import ClientLookup
157+
158+
ssm, config, logger = setup_mocks()
159+
160+
ssm.get_parameters_by_path.return_value = {
161+
"Parameters": [
162+
create_client_parameter("client1", "MAILBOX_001"),
163+
{
164+
"Name": "/dl/test/mesh/clients/incomplete_client",
165+
"Value": json.dumps({
166+
"clientId": "incomplete",
167+
"name": "Incomplete Client"
168+
# Missing meshMailboxSenderId
169+
})
170+
},
171+
create_client_parameter("client3", "MAILBOX_003"),
172+
]
173+
}
174+
175+
client_lookup = ClientLookup(ssm, config, logger)
176+
177+
assert client_lookup.is_valid_sender("MAILBOX_001")
178+
assert client_lookup.is_valid_sender("MAILBOX_003")
179+
180+
def test_load_valid_senders_handles_empty_mailbox_id(self):
181+
"""Test that empty meshMailboxSenderId values are skipped"""
182+
from src.client_lookup import ClientLookup
183+
184+
ssm, config, logger = setup_mocks()
185+
186+
ssm.get_parameters_by_path.return_value = {
187+
"Parameters": [
188+
create_client_parameter("client1", "MAILBOX_001"),
189+
{
190+
"Name": "/dl/test/mesh/clients/empty_mailbox",
191+
"Value": json.dumps({
192+
"clientId": "empty",
193+
"meshMailboxSenderId": "", # Empty string
194+
"name": "Empty Mailbox Client"
195+
})
196+
},
197+
create_client_parameter("client3", "MAILBOX_003"),
198+
]
199+
}
200+
201+
client_lookup = ClientLookup(ssm, config, logger)
202+
203+
assert client_lookup.is_valid_sender("MAILBOX_001")
204+
assert client_lookup.is_valid_sender("MAILBOX_003")
205+
assert not client_lookup.is_valid_sender("")
206+
207+
def test_load_valid_senders_with_trailing_slash_in_path(self):
208+
"""Test that paths with trailing slashes are handled correctly"""
209+
from src.client_lookup import ClientLookup
210+
211+
ssm, config, logger = setup_mocks()
212+
config.ssm_clients_parameter_path = "/dl/test/mesh/clients/" # Trailing slash
213+
214+
ssm.get_parameters_by_path.return_value = {
215+
"Parameters": [
216+
create_client_parameter("client1", "MAILBOX_001"),
217+
]
218+
}
219+
220+
client_lookup = ClientLookup(ssm, config, logger)
221+
222+
ssm.get_parameters_by_path.assert_called_once_with(
223+
Path="/dl/test/mesh/clients/",
224+
WithDecryption=True
225+
)
226+
assert client_lookup.is_valid_sender("MAILBOX_001")
227+
228+
def test_load_valid_senders_handles_empty_response(self):
229+
"""Test that empty SSM response is handled correctly"""
230+
from src.client_lookup import ClientLookup
231+
232+
ssm, config, logger = setup_mocks()
233+
234+
ssm.get_parameters_by_path.return_value = {
235+
"Parameters": []
236+
}
237+
238+
client_lookup = ClientLookup(ssm, config, logger)
239+
240+
assert not client_lookup.is_valid_sender("ANY_MAILBOX")
241+
logger.debug.assert_called_once()
242+
call_args = logger.debug.call_args[0][0]
243+
assert "0" in call_args # Should log count of 0

0 commit comments

Comments
 (0)