Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/eligibility_signposting_api/config/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
NHS_NUMBER_HEADER = "nhs-login-nhs-number"
CONSUMER_ID = "nhsd-application-id" # "Nhsd-Application-Id"
ALLOWED_CONDITIONS = Literal["COVID", "FLU", "MMR", "RSV"]
CONSUMER_MAPPING_FILE_NAME = "consumer_mapping_config.json"
28 changes: 17 additions & 11 deletions src/eligibility_signposting_api/repos/consumer_mapping_repo.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import json
import logging
from typing import Annotated, NewType

from botocore.client import BaseClient
from botocore.exceptions import ClientError
from wireup import Inject, service

from eligibility_signposting_api.config.constants import CONSUMER_MAPPING_FILE_NAME
from eligibility_signposting_api.model.campaign_config import CampaignID
from eligibility_signposting_api.model.consumer_mapping import ConsumerId, ConsumerMapping

logger = logging.getLogger(__name__)

BucketName = NewType("BucketName", str)


Expand All @@ -24,18 +29,19 @@ def __init__(
self.bucket_name = bucket_name

def get_permitted_campaign_ids(self, consumer_id: ConsumerId) -> list[CampaignID] | None:
objects = self.s3_client.list_objects(Bucket=self.bucket_name).get("Contents")

if not objects:
return None
try:
response = self.s3_client.get_object(Bucket=self.bucket_name, Key=CONSUMER_MAPPING_FILE_NAME)
body = response["Body"].read()

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

mapping_result = ConsumerMapping.model_validate(json.loads(body)).get(consumer_id)
if mapping_result is None:
return None

if mapping_result is None:
return None
return [item.campaign_config_id for item in mapping_result]

return [item.campaign_config_id for item in mapping_result]
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
return None
logger.exception("Error while reading consumer mapping config file : %s", CONSUMER_MAPPING_FILE_NAME)
raise
36 changes: 18 additions & 18 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1402,7 +1402,7 @@ def create_and_put_consumer_mapping_in_s3(
consumer_mapping_data = consumer_mapping.model_dump(by_alias=True)
s3_client.put_object(
Bucket=consumer_mapping_bucket,
Key="consumer_mapping.json",
Key="consumer_mapping_config.json",
Body=json.dumps(consumer_mapping_data),
ContentType="application/json",
)
Expand All @@ -1420,7 +1420,7 @@ def consumer_to_active_campaign_having_invalid_tokens_mapping(
campaign_config_with_invalid_tokens, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture(scope="class")
Expand All @@ -1434,7 +1434,7 @@ def consumer_to_active_campaign_having_tokens_mapping(
campaign_config_with_tokens, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture(scope="class")
Expand All @@ -1448,7 +1448,7 @@ def consumer_to_active_rsv_campaign_mapping(
rsv_campaign_config, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture(scope="class")
Expand All @@ -1462,7 +1462,7 @@ def consumer_to_active_campaign_having_and_rule_mapping(
campaign_config_with_and_rule, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture
Expand All @@ -1476,7 +1476,7 @@ def consumer_to_active_campaign_missing_descriptions_and_rule_text_mapping(
campaign_config_with_missing_descriptions_missing_rule_text, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture
Expand All @@ -1490,7 +1490,7 @@ def consumer_to_active_campaign_having_rules_with_rule_code_mapping(
campaign_config_with_rules_having_rule_code, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture
Expand All @@ -1504,7 +1504,7 @@ def consumer_to_active_campaign_having_rules_with_rule_mapper_mapping(
campaign_config_with_rules_having_rule_mapper, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture
Expand All @@ -1518,7 +1518,7 @@ def consumer_to_active_campaign_having_only_virtual_cohort_mapping(
campaign_config_with_virtual_cohort, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture
Expand All @@ -1532,7 +1532,7 @@ def consumer_to_active_campaign_config_with_derived_values_mapping(
campaign_config_with_derived_values, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture
Expand All @@ -1546,7 +1546,7 @@ def consumer_to_active_campaign_config_with_derived_values_formatted_mapping(
campaign_config_with_derived_values_formatted, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture
Expand All @@ -1560,7 +1560,7 @@ def consumer_to_active_campaign_config_with_multiple_add_days_mapping(
campaign_config_with_multiple_add_days, consumer_id, consumer_mapping_bucket, s3_client
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture
Expand Down Expand Up @@ -1592,12 +1592,12 @@ def consumer_to_campaign_having_inactive_iteration_mapping(

s3_client.put_object(
Bucket=consumer_mapping_bucket,
Key="consumer_mapping.json",
Key="consumer_mapping_config.json",
Body=json.dumps(mapping.model_dump(by_alias=True)),
ContentType="application/json",
)
yield mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


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

s3_client.put_object(
Bucket=consumer_mapping_bucket,
Key="consumer_mapping.json",
Key="consumer_mapping_config.json",
Body=json.dumps(mapping.model_dump(by_alias=True)),
ContentType="application/json",
)
yield mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


@pytest.fixture
Expand All @@ -1631,12 +1631,12 @@ def consumer_mappings(
consumer_mapping_data = consumer_mapping.model_dump(by_alias=True)
s3_client.put_object(
Bucket=consumer_mapping_bucket,
Key="consumer_mapping.json",
Key="consumer_mapping_config.json",
Body=json.dumps(consumer_mapping_data),
ContentType="application/json",
)
yield consumer_mapping
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping.json")
s3_client.delete_object(Bucket=consumer_mapping_bucket, Key="consumer_mapping_config.json")


# If you put StubSecretRepo in a separate module, import it instead
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/in_process/test_eligibility_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1274,7 +1274,7 @@ def test_if_campaign_having_best_status_is_chosen_if_there_exists_multiple_campa
# Consumer Mapping Data
s3_client.put_object(
Bucket=consumer_mapping_bucket,
Key="consumer_mapping.json",
Key="consumer_mapping_config.json",
Body=json.dumps(
{
consumer_id: [
Expand Down
29 changes: 24 additions & 5 deletions tests/unit/repos/test_consumer_mapping_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from unittest.mock import MagicMock

import pytest
from botocore.exceptions import ClientError

from eligibility_signposting_api.model.consumer_mapping import ConsumerId
from eligibility_signposting_api.repos.consumer_mapping_repo import BucketName, ConsumerMappingRepo
Expand Down Expand Up @@ -31,8 +32,6 @@ def test_get_permitted_campaign_ids_success(self, repo, mock_s3_client):
]
}

mock_s3_client.list_objects.return_value = {"Contents": [{"Key": "mappings.json"}]}

body_json = json.dumps(mapping_data).encode("utf-8")
mock_s3_client.get_object.return_value = {"Body": MagicMock(read=lambda: body_json)}

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

# Then
assert result == expected_campaign_ids
mock_s3_client.list_objects.assert_called_once_with(Bucket="test-bucket")
mock_s3_client.get_object.assert_called_once_with(Bucket="test-bucket", Key="mappings.json")
mock_s3_client.get_object.assert_called_once_with(Bucket="test-bucket", Key="consumer_mapping_config.json")

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

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

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

# Then
assert result is None

def test_get_permitted_campaign_ids_returns_none_when_file_not_in_s3(self, repo, mock_s3_client):
# Given: S3 returns a NoSuchKey error
error_response = {"Error": {"Code": "NoSuchKey", "Message": "Not Found"}}
mock_s3_client.get_object.side_effect = ClientError(error_response, "GetObject")

# When
result = repo.get_permitted_campaign_ids(ConsumerId("any-user"))

# Then
assert result is None

def test_get_permitted_campaign_ids_raises_client_error(self, repo, mock_s3_client):
# Given: An S3 error that is NOT 'NoSuchKey' (e.g, Access denied error)
error_response = {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}
mock_s3_client.get_object.side_effect = ClientError(error_response, "GetObject")

# When / Then: Verify the error is re-raised
with pytest.raises(ClientError) as exc_info:
repo.get_permitted_campaign_ids(ConsumerId("any-user"))

assert exc_info.value.response["Error"]["Code"] == "AccessDenied"
Loading