Skip to content

Commit c561265

Browse files
author
Byron Himes
authored
Include Correlation ID in Idempotence Check (GSI-930) (#15)
* Include correlation ID in notification hash and sort model fields * Bump version from 1.1.2 -> 1.1.3 Update support files * Satisfy mypy
1 parent fe65bd4 commit c561265

8 files changed

Lines changed: 338 additions & 302 deletions

File tree

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ repos:
4848
- id: no-commit-to-branch
4949
args: [--branch, dev, --branch, int, --branch, main]
5050
- repo: https://github.com/astral-sh/ruff-pre-commit
51-
rev: v0.5.2
51+
rev: v0.5.5
5252
hooks:
5353
- id: ruff
5454
args: [--fix, --exit-non-zero-on-fix]
5555
- id: ruff-format
5656
- repo: https://github.com/pre-commit/mirrors-mypy
57-
rev: v1.10.1
57+
rev: v1.11.0
5858
hooks:
5959
- id: mypy
6060
args: [--no-warn-unused-ignores]

.pyproject_generation/pyproject_custom.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "ns"
3-
version = "1.1.2"
3+
version = "1.1.3"
44
description = "The Notification Service (NS) handles notification kafka events."
55
dependencies = [
66
"typer>=0.12",

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,21 @@ We recommend using the provided Docker container.
3333

3434
A pre-build version is available at [docker hub](https://hub.docker.com/repository/docker/ghga/notification-service):
3535
```bash
36-
docker pull ghga/notification-service:1.1.2
36+
docker pull ghga/notification-service:1.1.3
3737
```
3838

3939
Or you can build the container yourself from the [`./Dockerfile`](./Dockerfile):
4040
```bash
4141
# Execute in the repo's root dir:
42-
docker build -t ghga/notification-service:1.1.2 .
42+
docker build -t ghga/notification-service:1.1.3 .
4343
```
4444

4545
For production-ready deployment, we recommend using Kubernetes, however,
4646
for simple use cases, you could execute the service using docker
4747
on a single server:
4848
```bash
4949
# The entrypoint is preconfigured:
50-
docker run -p 8080:8080 ghga/notification-service:1.1.2 --help
50+
docker run -p 8080:8080 ghga/notification-service:1.1.3 --help
5151
```
5252

5353
If you prefer not to use containers, you may install the service from source:

lock/requirements-dev.txt

Lines changed: 188 additions & 184 deletions
Large diffs are not rendered by default.

lock/requirements.txt

Lines changed: 107 additions & 103 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ classifiers = [
2121
"Intended Audience :: Developers",
2222
]
2323
name = "ns"
24-
version = "1.1.2"
24+
version = "1.1.3"
2525
description = "The Notification Service (NS) handles notification kafka events."
2626
dependencies = [
2727
"typer>=0.12",

src/ns/core/notifier.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""Contains the concrete implementation of a NotifierPort"""
1717

1818
import html
19+
import json
1920
import logging
2021
from contextlib import suppress
2122
from email.message import EmailMessage
@@ -24,6 +25,7 @@
2425
from string import Template
2526

2627
from ghga_event_schemas import pydantic_ as event_schemas
28+
from hexkit.correlation import get_correlation_id
2729
from pydantic import EmailStr, Field
2830
from pydantic_settings import BaseSettings
2931

@@ -72,8 +74,11 @@ def __init__(
7274
def _create_notification_record(
7375
self, *, notification: event_schemas.Notification
7476
) -> models.NotificationRecord:
75-
"""Creates a notification record from a notification event"""
76-
hash_sum = sha256(notification.model_dump_json().encode("utf-8")).hexdigest()
77+
"""Creates a notification record from a notification event and correlation ID."""
78+
correlation_id = get_correlation_id()
79+
notification_str = json.dumps(notification.model_dump(), sort_keys=True)
80+
concatenated = correlation_id + notification_str
81+
hash_sum = sha256(concatenated.encode("utf-8")).hexdigest()
7782
return models.NotificationRecord(hash_sum=hash_sum, sent=False)
7883

7984
async def _has_been_sent(self, *, hash_sum: str) -> bool:

tests/test_basic.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
"""Test basic event consumption"""
1717

1818
import html
19+
import json
1920
from hashlib import sha256
2021
from typing import cast
2122

2223
import pytest
24+
from hexkit.correlation import correlation_id_var
2325

2426
from ns.adapters.outbound.smtp_client import SmtpClient
2527
from ns.core.models import NotificationRecord
@@ -30,6 +32,8 @@
3032

3133
pytestmark = pytest.mark.asyncio()
3234

35+
TEST_CORRELATION_ID = "6914c8cd-1f18-43da-ac6c-c43cca3f36cc"
36+
3337
sample_notification = {
3438
"recipient_email": "test@example.com",
3539
"email_cc": ["test2@test.com", "test3@test.com"],
@@ -40,6 +44,16 @@
4044
}
4145

4246

47+
@pytest.fixture(autouse=True)
48+
def correlation_id_fixture():
49+
"""Provides a new correlation ID for each test case."""
50+
# we cannot use an async fixture with set_correlation_id(),
51+
# because it would run in a different context from the test
52+
token = correlation_id_var.set(TEST_CORRELATION_ID)
53+
yield
54+
correlation_id_var.reset(token)
55+
56+
4357
@pytest.mark.parametrize(
4458
"notification_details",
4559
[sample_notification],
@@ -61,7 +75,7 @@ async def test_email_construction(
6175
plaintext_body = msg.get_body(preferencelist="plain")
6276
assert plaintext_body is not None
6377

64-
plaintext_content = plaintext_body.get_content() # type: ignore
78+
plaintext_content = plaintext_body.get_content()
6579
expected_plaintext = (
6680
"Dear Yolanda Martinez,\n\nWhere are you, where are you, Yolanda?\n"
6781
+ "\nWarm regards,\n\nThe GHGA Team"
@@ -71,7 +85,7 @@ async def test_email_construction(
7185
html_body = msg.get_body(preferencelist="html")
7286
assert html_body is not None
7387

74-
html_content = html_body.get_content() # type: ignore
88+
html_content = html_body.get_content()
7589
assert html_content is not None
7690

7791
expected_html = (
@@ -149,9 +163,11 @@ async def test_helper_functions(joint_fixture: JointFixture):
149163
notification = make_notification(sample_notification)
150164

151165
# manually create a NotificationRecord for the notification
166+
concatenated = TEST_CORRELATION_ID + json.dumps(
167+
notification.model_dump(), sort_keys=True
168+
)
152169
expected_record = NotificationRecord(
153-
hash_sum=sha256(notification.model_dump_json().encode("utf-8")).hexdigest(),
154-
sent=False,
170+
hash_sum=sha256(concatenated.encode("utf-8")).hexdigest(), sent=False
155171
)
156172

157173
# Now create the record using the notifier's function and compare
@@ -198,6 +214,7 @@ async def test_idempotence_and_transmission(joint_fixture: JointFixture):
198214
joint_fixture.notifier = cast(Notifier, joint_fixture.notifier)
199215

200216
notification_event = make_notification(sample_notification)
217+
201218
await joint_fixture.kafka.publish_event(
202219
payload=notification_event.model_dump(),
203220
type_=joint_fixture.config.notification_event_type,
@@ -209,6 +226,12 @@ async def test_idempotence_and_transmission(joint_fixture: JointFixture):
209226
notification=notification_event
210227
)
211228

229+
# verify the hash is generated with the keys sorted and correlation ID prepended
230+
concatenated = TEST_CORRELATION_ID + json.dumps(
231+
notification_event.model_dump(), sort_keys=True
232+
)
233+
assert record.hash_sum == sha256(concatenated.encode("utf-8")).hexdigest()
234+
212235
# the record hasn't been sent, so this should return False
213236
assert not await joint_fixture.notifier._has_been_sent(hash_sum=record.hash_sum)
214237

@@ -267,7 +290,7 @@ async def test_html_escaping(joint_fixture: JointFixture):
267290
plaintext_body = msg.get_body(preferencelist="plain")
268291
assert plaintext_body is not None
269292

270-
plaintext_content = plaintext_body.get_content() # type: ignore
293+
plaintext_content = plaintext_body.get_content()
271294
expected_plaintext = (
272295
f"Dear {original_name},\n\n{original_body}\n"
273296
+ "\nWarm regards,\n\nThe GHGA Team"
@@ -277,7 +300,7 @@ async def test_html_escaping(joint_fixture: JointFixture):
277300
html_body = msg.get_body(preferencelist="html")
278301
assert html_body is not None
279302

280-
html_content = html_body.get_content() # type: ignore
303+
html_content = html_body.get_content()
281304
assert html_content is not None
282305

283306
expected_html = (

0 commit comments

Comments
 (0)