Skip to content

Commit 078b014

Browse files
committed
PDS
1 parent f75cafa commit 078b014

File tree

8 files changed

+298
-44
lines changed

8 files changed

+298
-44
lines changed
Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,41 @@
11
{
22
"folders": [
3-
{
4-
"path": "."
5-
},
6-
{
7-
"path": "backend"
8-
},
9-
{
10-
"path": "filenameprocessor"
11-
},
12-
{
13-
"path": "recordprocessor"
14-
},
15-
{
16-
"path": "ack_backend"
17-
},
18-
{
19-
"path": "delta_backend"
20-
},
21-
{
22-
"path": "mesh_processor"
23-
},
24-
{
25-
"path": "e2e"
26-
},
27-
{
28-
"path": "e2e_batch"
29-
},
30-
{
31-
"path": "redis_sync"
32-
},
33-
{
34-
"path": "lambdas/id_sync"
35-
},
36-
{
37-
"path": "lambdas/shared"
38-
}
39-
],
40-
"settings": {},
3+
{
4+
"path": "."
5+
},
6+
{
7+
"path": "backend"
8+
},
9+
{
10+
"path": "filenameprocessor"
11+
},
12+
{
13+
"path": "recordprocessor"
14+
},
15+
{
16+
"path": "ack_backend"
17+
},
18+
{
19+
"path": "delta_backend"
20+
},
21+
{
22+
"path": "mesh_processor"
23+
},
24+
{
25+
"path": "e2e"
26+
},
27+
{
28+
"path": "e2e_batch"
29+
},
30+
{
31+
"path": "redis_sync"
32+
},
33+
{
34+
"path": "lambdas/id_sync"
35+
},
36+
{
37+
"path": "lambdas/shared"
38+
}
39+
],
40+
"settings": {}
4141
}

lambdas/id_sync/src/clients.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import os
2+
3+
4+
pds_env: str = os.getenv("PDS_ENV", "int")

lambdas/id_sync/src/pds_details.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'''
2+
record Processor
3+
'''
4+
from common.clients import logger, secrets_manager_client
5+
from clients import pds_env
6+
from cache import Cache
7+
from common.pds_service import PdsService
8+
from common.authentication import AppRestrictedAuth, Service
9+
10+
11+
def get_pds_patient_details(id: str) -> dict:
12+
try:
13+
logger.info(f"Get PDS patient details for {id}")
14+
15+
cache = Cache(directory="/tmp")
16+
authenticator = AppRestrictedAuth(
17+
service=Service.PDS,
18+
secret_manager_client=secrets_manager_client,
19+
environment=pds_env,
20+
cache=cache,
21+
)
22+
pds_service = PdsService(authenticator, pds_env)
23+
24+
details = pds_service.get_patient_details(id)
25+
26+
return details.get("id")
27+
except Exception:
28+
logger.exception(f"Error getting PDS patient details for {id}")
29+
return None

lambdas/id_sync/src/record_processor.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ def check_records_exist(id: str) -> bool:
3939
def get_pds_patient_details(id: str) -> dict:
4040
# TODO: Implement logic to retrieve patient details from PDS
4141
logger.info(f"TODO Get patient details for {id}")
42+
43+
authenticator = AppRestrictedAuth(
44+
service=Service.PDS,
45+
secret_manager_client=boto3.client("secretsmanager", config=boto_config),
46+
environment=pds_env,
47+
cache=cache,
48+
)
49+
pds_service = PdsService(authenticator, pds_env)
50+
51+
4252
return {"id": id, "name": "Mr Man"}
4353

4454

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock
3+
from pds_details import get_pds_patient_details
4+
5+
6+
class TestGetPdsPatientDetails(unittest.TestCase):
7+
8+
def setUp(self):
9+
"""Set up test fixtures and mocks"""
10+
# Mock the dependencies
11+
self.mock_boto3_client = MagicMock()
12+
self.test_patient_id = "9912003888"
13+
14+
# Patch all external dependencies
15+
self.logger_patcher = patch('pds_details.logger')
16+
self.mock_logger = self.logger_patcher.start()
17+
18+
self.secrets_manager_patcher = patch('pds_details.secrets_manager_client')
19+
self.mock_secrets_manager = self.secrets_manager_patcher.start()
20+
21+
self.pds_env_patcher = patch('pds_details.pds_env')
22+
self.mock_pds_env = self.pds_env_patcher.start()
23+
24+
self.cache_patcher = patch('pds_details.Cache')
25+
self.mock_cache_class = self.cache_patcher.start()
26+
self.mock_cache_instance = MagicMock()
27+
self.mock_cache_class.return_value = self.mock_cache_instance
28+
29+
self.auth_patcher = patch('pds_details.AppRestrictedAuth')
30+
self.mock_auth_class = self.auth_patcher.start()
31+
self.mock_auth_instance = MagicMock()
32+
self.mock_auth_class.return_value = self.mock_auth_instance
33+
34+
self.pds_service_patcher = patch('pds_details.PdsService')
35+
self.mock_pds_service_class = self.pds_service_patcher.start()
36+
self.mock_pds_service_instance = MagicMock()
37+
self.mock_pds_service_class.return_value = self.mock_pds_service_instance
38+
39+
def tearDown(self):
40+
"""Clean up patches"""
41+
patch.stopall()
42+
43+
def test_get_pds_patient_details_success(self):
44+
"""Test successful retrieval of patient details"""
45+
# Arrange
46+
expected_patient_data = {
47+
"id": "9912003888",
48+
"name": "John Doe",
49+
"birthDate": "1990-01-01",
50+
"gender": "male"
51+
}
52+
self.mock_pds_service_instance.get_patient_details.return_value = expected_patient_data
53+
54+
# Act
55+
result = get_pds_patient_details(self.test_patient_id, self.mock_boto3_client)
56+
57+
# Assert
58+
self.assertEqual(result, "9912003888")
59+
60+
# Verify Cache was initialized correctly
61+
self.mock_cache_class.assert_called_once_with(directory="/tmp")
62+
63+
# Verify AppRestrictedAuth was initialized correctly
64+
from common.authentication import Service
65+
self.mock_auth_class.assert_called_once_with(
66+
service=Service.PDS,
67+
secret_manager_client=self.mock_secrets_manager,
68+
environment=self.mock_pds_env,
69+
cache=self.mock_cache_instance
70+
)
71+
72+
# Verify PdsService was initialized correctly
73+
self.mock_pds_service_class.assert_called_once_with(
74+
self.mock_auth_instance,
75+
self.mock_pds_env
76+
)
77+
78+
# Verify get_patient_details was called
79+
self.mock_pds_service_instance.get_patient_details.assert_called_once_with(self.test_patient_id)
80+
81+
def test_get_pds_patient_details_missing_id_in_response(self):
82+
"""Test when PDS response doesn't contain 'id' field"""
83+
# Arrange
84+
patient_data_without_id = {
85+
"name": "John Doe",
86+
"birthDate": "1990-01-01",
87+
"gender": "male"
88+
# Missing 'id' field
89+
}
90+
self.mock_pds_service_instance.get_patient_details.return_value = patient_data_without_id
91+
92+
# Act
93+
result = get_pds_patient_details(self.test_patient_id, self.mock_boto3_client)
94+
95+
# Assert
96+
self.assertIsNone(result)
97+
self.mock_pds_service_instance.get_patient_details.assert_called_once_with(self.test_patient_id)
98+
99+
def test_get_pds_patient_details_empty_response(self):
100+
"""Test when PDS returns empty response"""
101+
# Arrange
102+
self.mock_pds_service_instance.get_patient_details.return_value = {}
103+
104+
# Act
105+
result = get_pds_patient_details(self.test_patient_id, self.mock_boto3_client)
106+
107+
# Assert
108+
self.assertIsNone(result)
109+
self.mock_pds_service_instance.get_patient_details.assert_called_once_with(self.test_patient_id)
110+
111+
def test_get_pds_patient_details_none_response(self):
112+
"""Test when PDS returns None"""
113+
# Arrange
114+
self.mock_pds_service_instance.get_patient_details.return_value = None
115+
116+
# Act
117+
with self.assertRaises(AttributeError):
118+
get_pds_patient_details(self.test_patient_id, self.mock_boto3_client)
119+
120+
# Assert
121+
self.mock_pds_service_instance.get_patient_details.assert_called_once_with(self.test_patient_id)
122+
123+
def test_get_pds_patient_details_pds_service_exception(self):
124+
"""Test when PdsService.get_patient_details raises an exception"""
125+
# Arrange
126+
self.mock_pds_service_instance.get_patient_details.side_effect = Exception("PDS API error")
127+
128+
# Act & Assert
129+
with self.assertRaises(Exception) as context:
130+
get_pds_patient_details(self.test_patient_id, self.mock_boto3_client)
131+
132+
self.assertEqual(str(context.exception), "PDS API error")
133+
self.mock_pds_service_instance.get_patient_details.assert_called_once_with(self.test_patient_id)
134+
135+
def test_get_pds_patient_details_cache_initialization_error(self):
136+
"""Test when Cache initialization fails"""
137+
# Arrange
138+
self.mock_cache_class.side_effect = OSError("Cannot write to /tmp")
139+
140+
# Act & Assert
141+
with self.assertRaises(OSError) as context:
142+
get_pds_patient_details(self.test_patient_id, self.mock_boto3_client)
143+
144+
self.assertEqual(str(context.exception), "Cannot write to /tmp")
145+
self.mock_cache_class.assert_called_once_with(directory="/tmp")
146+
147+
def test_get_pds_patient_details_auth_initialization_error(self):
148+
"""Test when AppRestrictedAuth initialization fails"""
149+
# Arrange
150+
self.mock_auth_class.side_effect = ValueError("Invalid authentication parameters")
151+
152+
# Act & Assert
153+
with self.assertRaises(ValueError) as context:
154+
get_pds_patient_details(self.test_patient_id, self.mock_boto3_client)
155+
156+
self.assertEqual(str(context.exception), "Invalid authentication parameters")
157+
158+
def test_get_pds_patient_details_exception(self):
159+
"""Test when an exception occurs"""
160+
# Arrange
161+
self.mock_logger.info.side_effect = Exception("Logging system failure")
162+
163+
# Act
164+
result = get_pds_patient_details(self.test_patient_id, self.mock_boto3_client)
165+
166+
# Assert
167+
self.assertIsNone(result)
168+
169+
# Verify logger.exception was called due to the caught exception
170+
self.mock_logger.exception.assert_called_once_with(
171+
f"Error getting PDS patient details for {self.test_patient_id}")

lambdas/shared/src/common/cache.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import json
2+
from typing import Optional
3+
4+
5+
class Cache:
6+
"""Key-value file cache"""
7+
8+
def __init__(self, directory):
9+
filename = f"{directory}/cache.json"
10+
with open(filename, "a+") as self.cache_file:
11+
self.cache_file.seek(0)
12+
content = self.cache_file.read()
13+
if len(content) == 0:
14+
self.cache = {}
15+
else:
16+
self.cache = json.loads(content)
17+
18+
def put(self, key: str, value: dict):
19+
self.cache[key] = value
20+
self._overwrite()
21+
22+
def get(self, key: str) -> Optional[dict]:
23+
return self.cache.get(key, None)
24+
25+
def delete(self, key: str):
26+
if key not in self.cache:
27+
return
28+
del self.cache[key]
29+
30+
def _overwrite(self):
31+
with open(self.cache_file.name, "w") as self.cache_file:
32+
self.cache_file.seek(0)
33+
self.cache_file.write(json.dumps(self.cache))
34+
self.cache_file.truncate()

lambdas/shared/src/common/clients.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import logging
33
from boto3 import client as boto3_client
4+
# from botocore.config import Config
45

56

67
logging.basicConfig(level="INFO")
@@ -14,3 +15,7 @@
1415

1516
s3_client = boto3_client("s3", region_name=REGION_NAME)
1617
firehose_client = boto3_client("firehose", region_name=REGION_NAME)
18+
19+
# boto_config = Config(region_name=REGION_NAME)
20+
# secretsmanager_client = boto3_client("secretsmanager", config=boto_config)
21+
secrets_manager_client = boto3_client("secretsmanager", region_name=REGION_NAME)

terraform/id_sync_lambda.tf

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ locals {
33
lambdas_dir = abspath("${path.root}/../lambdas")
44
shared_dir = abspath("${path.root}/../lambdas/shared")
55
id_sync_lambda_dir = abspath("${path.root}/../lambdas/id_sync")
6-
6+
77
# Get files from both directories
88
shared_files = fileset(local.shared_dir, "**")
99
id_sync_lambda_files = fileset(local.id_sync_lambda_dir, "**")
10-
10+
1111
# Calculate SHA for both directories
1212
shared_dir_sha = sha1(join("", [for f in local.shared_files : filesha1("${local.shared_dir}/${f}")]))
1313
id_sync_lambda_dir_sha = sha1(join("", [for f in local.id_sync_lambda_files : filesha1("${local.id_sync_lambda_dir}/${f}")]))
14-
14+
1515
# Combined SHA to trigger rebuild when either directory changes
1616
combined_sha = sha1("${local.shared_dir_sha}${local.id_sync_lambda_dir_sha}")
1717
}
@@ -303,6 +303,7 @@ resource "aws_lambda_function" "id_sync_lambda" {
303303
variables = {
304304
ID_SYNC_PROC_LAMBDA_NAME = "imms-${local.env}-id_sync_lambda"
305305
SPLUNK_FIREHOSE_NAME = module.splunk.firehose_stream_name
306+
PDS_ENV = local.environment == "prod" ? "prod" : local.environment == "ref" ? "ref" : "int"
306307
}
307308
}
308309
kms_key_arn = data.aws_kms_key.existing_lambda_encryption_key.arn
@@ -322,11 +323,11 @@ resource "aws_cloudwatch_log_group" "id_sync_log_group" {
322323
resource "aws_lambda_event_source_mapping" "id_sync_sqs_trigger" {
323324
event_source_arn = data.aws_sqs_queue.existing_sqs_queue.arn
324325
function_name = aws_lambda_function.id_sync_lambda.arn
325-
326+
326327
# Optional: Configure batch size and other settings
327328
batch_size = 10
328329
maximum_batching_window_in_seconds = 5
329-
330+
330331
# Optional: Configure error handling
331332
function_response_types = ["ReportBatchItemFailures"]
332-
}
333+
}

0 commit comments

Comments
 (0)