Skip to content

Commit 083d5bb

Browse files
Eli 620 read mapping file directly by name (#569)
* eli 620 - reading consumer_mapping_config.json by name * eli 620 - fix integration tests * eli 620 - few more tests for mapping repo --------- Co-authored-by: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com>
1 parent e975a76 commit 083d5bb

5 files changed

Lines changed: 61 additions & 35 deletions

File tree

src/eligibility_signposting_api/config/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
NHS_NUMBER_HEADER = "nhs-login-nhs-number"
66
CONSUMER_ID = "NHSE-Product-ID"
77
ALLOWED_CONDITIONS = Literal["COVID", "FLU", "MMR", "RSV"]
8+
CONSUMER_MAPPING_FILE_NAME = "consumer_mapping_config.json"
Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import json
2+
import logging
23
from typing import Annotated, NewType
34

45
from botocore.client import BaseClient
6+
from botocore.exceptions import ClientError
57
from wireup import Inject, service
68

9+
from eligibility_signposting_api.config.constants import CONSUMER_MAPPING_FILE_NAME
710
from eligibility_signposting_api.model.campaign_config import CampaignID
811
from eligibility_signposting_api.model.consumer_mapping import ConsumerId, ConsumerMapping
912

13+
logger = logging.getLogger(__name__)
14+
1015
BucketName = NewType("BucketName", str)
1116

1217

@@ -24,18 +29,19 @@ def __init__(
2429
self.bucket_name = bucket_name
2530

2631
def get_permitted_campaign_ids(self, consumer_id: ConsumerId) -> list[CampaignID] | None:
27-
objects = self.s3_client.list_objects(Bucket=self.bucket_name).get("Contents")
28-
29-
if not objects:
30-
return None
32+
try:
33+
response = self.s3_client.get_object(Bucket=self.bucket_name, Key=CONSUMER_MAPPING_FILE_NAME)
34+
body = response["Body"].read()
3135

32-
consumer_mappings_obj = objects[0]
33-
response = self.s3_client.get_object(Bucket=self.bucket_name, Key=consumer_mappings_obj["Key"])
34-
body = response["Body"].read()
36+
mapping_result = ConsumerMapping.model_validate(json.loads(body)).get(consumer_id)
3537

36-
mapping_result = ConsumerMapping.model_validate(json.loads(body)).get(consumer_id)
38+
if mapping_result is None:
39+
return None
3740

38-
if mapping_result is None:
39-
return None
41+
return [item.campaign_config_id for item in mapping_result]
4042

41-
return [item.campaign_config_id for item in mapping_result]
43+
except ClientError as e:
44+
if e.response["Error"]["Code"] == "NoSuchKey":
45+
return None
46+
logger.exception("Error while reading consumer mapping config file : %s", CONSUMER_MAPPING_FILE_NAME)
47+
raise

tests/integration/conftest.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,7 +1402,7 @@ def create_and_put_consumer_mapping_in_s3(
14021402
consumer_mapping_data = consumer_mapping.model_dump(by_alias=True)
14031403
s3_client.put_object(
14041404
Bucket=consumer_mapping_bucket,
1405-
Key="consumer_mapping.json",
1405+
Key="consumer_mapping_config.json",
14061406
Body=json.dumps(consumer_mapping_data),
14071407
ContentType="application/json",
14081408
)
@@ -1420,7 +1420,7 @@ def consumer_to_active_campaign_having_invalid_tokens_mapping(
14201420
campaign_config_with_invalid_tokens, consumer_id, consumer_mapping_bucket, s3_client
14211421
)
14221422
yield consumer_mapping
1423-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1423+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
14241424

14251425

14261426
@pytest.fixture(scope="class")
@@ -1434,7 +1434,7 @@ def consumer_to_active_campaign_having_tokens_mapping(
14341434
campaign_config_with_tokens, consumer_id, consumer_mapping_bucket, s3_client
14351435
)
14361436
yield consumer_mapping
1437-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1437+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
14381438

14391439

14401440
@pytest.fixture(scope="class")
@@ -1448,7 +1448,7 @@ def consumer_to_active_rsv_campaign_mapping(
14481448
rsv_campaign_config, consumer_id, consumer_mapping_bucket, s3_client
14491449
)
14501450
yield consumer_mapping
1451-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1451+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
14521452

14531453

14541454
@pytest.fixture(scope="class")
@@ -1462,7 +1462,7 @@ def consumer_to_active_campaign_having_and_rule_mapping(
14621462
campaign_config_with_and_rule, consumer_id, consumer_mapping_bucket, s3_client
14631463
)
14641464
yield consumer_mapping
1465-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1465+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
14661466

14671467

14681468
@pytest.fixture
@@ -1476,7 +1476,7 @@ def consumer_to_active_campaign_missing_descriptions_and_rule_text_mapping(
14761476
campaign_config_with_missing_descriptions_missing_rule_text, consumer_id, consumer_mapping_bucket, s3_client
14771477
)
14781478
yield consumer_mapping
1479-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1479+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
14801480

14811481

14821482
@pytest.fixture
@@ -1490,7 +1490,7 @@ def consumer_to_active_campaign_having_rules_with_rule_code_mapping(
14901490
campaign_config_with_rules_having_rule_code, consumer_id, consumer_mapping_bucket, s3_client
14911491
)
14921492
yield consumer_mapping
1493-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1493+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
14941494

14951495

14961496
@pytest.fixture
@@ -1504,7 +1504,7 @@ def consumer_to_active_campaign_having_rules_with_rule_mapper_mapping(
15041504
campaign_config_with_rules_having_rule_mapper, consumer_id, consumer_mapping_bucket, s3_client
15051505
)
15061506
yield consumer_mapping
1507-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1507+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
15081508

15091509

15101510
@pytest.fixture
@@ -1518,7 +1518,7 @@ def consumer_to_active_campaign_having_only_virtual_cohort_mapping(
15181518
campaign_config_with_virtual_cohort, consumer_id, consumer_mapping_bucket, s3_client
15191519
)
15201520
yield consumer_mapping
1521-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1521+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
15221522

15231523

15241524
@pytest.fixture
@@ -1532,7 +1532,7 @@ def consumer_to_active_campaign_config_with_derived_values_mapping(
15321532
campaign_config_with_derived_values, consumer_id, consumer_mapping_bucket, s3_client
15331533
)
15341534
yield consumer_mapping
1535-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1535+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
15361536

15371537

15381538
@pytest.fixture
@@ -1546,7 +1546,7 @@ def consumer_to_active_campaign_config_with_derived_values_formatted_mapping(
15461546
campaign_config_with_derived_values_formatted, consumer_id, consumer_mapping_bucket, s3_client
15471547
)
15481548
yield consumer_mapping
1549-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1549+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
15501550

15511551

15521552
@pytest.fixture
@@ -1560,7 +1560,7 @@ def consumer_to_active_campaign_config_with_multiple_add_days_mapping(
15601560
campaign_config_with_multiple_add_days, consumer_id, consumer_mapping_bucket, s3_client
15611561
)
15621562
yield consumer_mapping
1563-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1563+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
15641564

15651565

15661566
@pytest.fixture
@@ -1592,12 +1592,12 @@ def consumer_to_campaign_having_inactive_iteration_mapping(
15921592

15931593
s3_client.put_object(
15941594
Bucket=consumer_mapping_bucket,
1595-
Key="consumer_mapping.json",
1595+
Key="consumer_mapping_config.json",
15961596
Body=json.dumps(mapping.model_dump(by_alias=True)),
15971597
ContentType="application/json",
15981598
)
15991599
yield mapping
1600-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1600+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
16011601

16021602

16031603
@pytest.fixture(scope="class")
@@ -1615,12 +1615,12 @@ def consumer_to_multiple_campaign_configs_mapping(
16151615

16161616
s3_client.put_object(
16171617
Bucket=consumer_mapping_bucket,
1618-
Key="consumer_mapping.json",
1618+
Key="consumer_mapping_config.json",
16191619
Body=json.dumps(mapping.model_dump(by_alias=True)),
16201620
ContentType="application/json",
16211621
)
16221622
yield mapping
1623-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1623+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
16241624

16251625

16261626
@pytest.fixture
@@ -1631,12 +1631,12 @@ def consumer_mappings(
16311631
consumer_mapping_data = consumer_mapping.model_dump(by_alias=True)
16321632
s3_client.put_object(
16331633
Bucket=consumer_mapping_bucket,
1634-
Key="consumer_mapping.json",
1634+
Key="consumer_mapping_config.json",
16351635
Body=json.dumps(consumer_mapping_data),
16361636
ContentType="application/json",
16371637
)
16381638
yield consumer_mapping
1639-
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
1639+
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")
16401640

16411641

16421642
# If you put StubSecretRepo in a separate module, import it instead

tests/integration/in_process/test_eligibility_endpoint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1274,7 +1274,7 @@ def test_if_campaign_having_best_status_is_chosen_if_there_exists_multiple_campa
12741274
# Consumer Mapping Data
12751275
s3_client.put_object(
12761276
Bucket=consumer_mapping_bucket,
1277-
Key="consumer_mapping.json",
1277+
Key="consumer_mapping_config.json",
12781278
Body=json.dumps(
12791279
{
12801280
consumer_id: [

tests/unit/repos/test_consumer_mapping_repo.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest.mock import MagicMock
33

44
import pytest
5+
from botocore.exceptions import ClientError
56

67
from eligibility_signposting_api.model.consumer_mapping import ConsumerId
78
from eligibility_signposting_api.repos.consumer_mapping_repo import BucketName, ConsumerMappingRepo
@@ -31,8 +32,6 @@ def test_get_permitted_campaign_ids_success(self, repo, mock_s3_client):
3132
]
3233
}
3334

34-
mock_s3_client.list_objects.return_value = {"Contents": [{"Key": "mappings.json"}]}
35-
3635
body_json = json.dumps(mapping_data).encode("utf-8")
3736
mock_s3_client.get_object.return_value = {"Body": MagicMock(read=lambda: body_json)}
3837

@@ -41,8 +40,7 @@ def test_get_permitted_campaign_ids_success(self, repo, mock_s3_client):
4140

4241
# Then
4342
assert result == expected_campaign_ids
44-
mock_s3_client.list_objects.assert_called_once_with(Bucket="test-bucket")
45-
mock_s3_client.get_object.assert_called_once_with(Bucket="test-bucket", Key="mappings.json")
43+
mock_s3_client.get_object.assert_called_once_with(Bucket="test-bucket", Key="consumer_mapping_config.json")
4644

4745
def test_get_permitted_campaign_ids_returns_none_when_missing(self, repo, mock_s3_client):
4846
"""
@@ -51,7 +49,6 @@ def test_get_permitted_campaign_ids_returns_none_when_missing(self, repo, mock_s
5149
"""
5250
valid_schema_data = {"other-user": [{"CampaignConfigID": "camp-1", "Description": "Some description"}]}
5351

54-
mock_s3_client.list_objects.return_value = {"Contents": [{"Key": "mappings.json"}]}
5552
body_json = json.dumps(valid_schema_data).encode("utf-8")
5653
mock_s3_client.get_object.return_value = {"Body": MagicMock(read=lambda: body_json)}
5754

@@ -60,3 +57,25 @@ def test_get_permitted_campaign_ids_returns_none_when_missing(self, repo, mock_s
6057

6158
# Then
6259
assert result is None
60+
61+
def test_get_permitted_campaign_ids_returns_none_when_file_not_in_s3(self, repo, mock_s3_client):
62+
# Given: S3 returns a NoSuchKey error
63+
error_response = {"Error": {"Code": "NoSuchKey", "Message": "Not Found"}}
64+
mock_s3_client.get_object.side_effect = ClientError(error_response, "GetObject")
65+
66+
# When
67+
result = repo.get_permitted_campaign_ids(ConsumerId("any-user"))
68+
69+
# Then
70+
assert result is None
71+
72+
def test_get_permitted_campaign_ids_raises_client_error(self, repo, mock_s3_client):
73+
# Given: An S3 error that is NOT 'NoSuchKey' (e.g, Access denied error)
74+
error_response = {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}
75+
mock_s3_client.get_object.side_effect = ClientError(error_response, "GetObject")
76+
77+
# When / Then: Verify the error is re-raised
78+
with pytest.raises(ClientError) as exc_info:
79+
repo.get_permitted_campaign_ids(ConsumerId("any-user"))
80+
81+
assert exc_info.value.response["Error"]["Code"] == "AccessDenied"

0 commit comments

Comments
 (0)