Skip to content

Commit 7555020

Browse files
firehose and audit bucket fixture
1 parent bfa9629 commit 7555020

11 files changed

Lines changed: 102 additions & 21 deletions

File tree

src/eligibility_signposting_api/config/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
def config() -> dict[str, Any]:
2222
person_table_name = TableName(os.getenv("PERSON_TABLE_NAME", "test_eligibility_datastore"))
2323
rules_bucket_name = BucketName(os.getenv("RULES_BUCKET_NAME", "test-rules-bucket"))
24+
audit_bucket_name = BucketName(os.getenv("AUDIT_BUCKET_NAME", "test-audit-bucket"))
2425
aws_default_region = AwsRegion(os.getenv("AWS_DEFAULT_REGION", "eu-west-1"))
2526
log_level = LOG_LEVEL
2627

@@ -33,6 +34,8 @@ def config() -> dict[str, Any]:
3334
"person_table_name": person_table_name,
3435
"s3_endpoint": None,
3536
"rules_bucket_name": rules_bucket_name,
37+
"audit_bucket_name": audit_bucket_name,
38+
"firehose_endpoint": None,
3639
"log_level": log_level,
3740
}
3841

@@ -44,6 +47,8 @@ def config() -> dict[str, Any]:
4447
"person_table_name": person_table_name,
4548
"s3_endpoint": URL(os.getenv("S3_ENDPOINT", "http://localhost:4566")),
4649
"rules_bucket_name": rules_bucket_name,
50+
"audit_bucket_name": audit_bucket_name,
51+
"firehose_endpoint": URL(os.getenv("FIREHOSE_ENDPOINT", "http://localhost:4566")),
4752
"log_level": log_level,
4853
}
4954

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
MAGIC_COHORT_LABEL = "elid_all_people"
22
RULE_STOP_DEFAULT = False
3-
NHS_NUMBER_HEADER_NAME = "nhs-login-nhs-number"
3+
NHS_NUMBER_HEADER = "nhs-login-nhs-number"
4+
5+
# Infra
6+
ELIGIBILITY_SIGNPOSTING_AUDIT_STREAM = "eligibility-signposting-audit-stream"

src/eligibility_signposting_api/repos/factory.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@ def dynamodb_resource_factory(
3535
def s3_service_factory(session: Session, s3_endpoint: Annotated[URL, Inject(param="s3_endpoint")]) -> BaseClient:
3636
endpoint_url = str(s3_endpoint) if s3_endpoint is not None else None
3737
return session.client("s3", endpoint_url=endpoint_url)
38+
39+
40+
@service(qualifier="firehose")
41+
def firehose_client_factory(
42+
session: Session, firehose_endpoint: Annotated[URL, Inject(param="firehose_endpoint")]
43+
) -> BaseClient:
44+
endpoint_url = str(firehose_endpoint) if firehose_endpoint is not None else None
45+
return session.client("firehose", endpoint_url=endpoint_url)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import json
2+
import logging
3+
from typing import Annotated
4+
5+
from botocore.client import BaseClient
6+
from wireup import Inject, service
7+
8+
from eligibility_signposting_api.config.contants import ELIGIBILITY_SIGNPOSTING_AUDIT_STREAM
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
@service
14+
class AuditService:
15+
def __init__(self, firehose: Annotated[BaseClient, Inject(qualifier="firehose")]) -> None:
16+
super().__init__()
17+
self.firehose = firehose
18+
self.delivery_stream_name = ELIGIBILITY_SIGNPOSTING_AUDIT_STREAM
19+
20+
def audit(self, audit_record: dict) -> None:
21+
"""
22+
Sends an audit record to the configured Firehose delivery stream.
23+
24+
Args:
25+
audit_record (dict): The audit data to send.
26+
27+
Returns:
28+
str: The Firehose record ID.
29+
"""
30+
response = self.firehose.put_record(
31+
DeliveryStreamName=self.delivery_stream_name,
32+
Record={"Data": (json.dumps(audit_record) + "\n").encode("utf-8")},
33+
)
34+
logger.info("Successfully sent to the Firehose", extra={"firehose_record_id": response["RecordId"]})

src/eligibility_signposting_api/services/eligibility_services.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from eligibility_signposting_api.model import eligibility
66
from eligibility_signposting_api.repos import CampaignRepo, NotFoundError, PersonRepo
7+
from eligibility_signposting_api.services.audit_service import AuditService
78
from eligibility_signposting_api.services.calculators import eligibility_calculator as calculator
89

910
logger = logging.getLogger(__name__)
@@ -23,11 +24,13 @@ def __init__(
2324
self,
2425
person_repo: PersonRepo,
2526
campaign_repo: CampaignRepo,
27+
audit_service: AuditService,
2628
calculator_factory: calculator.EligibilityCalculatorFactory,
2729
) -> None:
2830
super().__init__()
2931
self.person_repo = person_repo
3032
self.campaign_repo = campaign_repo
33+
self.audit_service = audit_service
3134
self.calculator_factory = calculator_factory
3235

3336
def get_eligibility_status(
@@ -51,6 +54,7 @@ def get_eligibility_status(
5154
raise UnknownPersonError from e
5255
else:
5356
calc: calculator.EligibilityCalculator = self.calculator_factory.get(person_data, campaign_configs)
57+
self.audit_service.audit({"test_audit": "check if audit works"})
5458
return calc.evaluate_eligibility(include_actions_flag=include_actions_flag)
5559

5660
raise UnknownPersonError # pragma: no cover

src/eligibility_signposting_api/wrapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from mangum.types import LambdaContext, LambdaEvent
66

7-
from eligibility_signposting_api.config.contants import NHS_NUMBER_HEADER_NAME
7+
from eligibility_signposting_api.config.contants import NHS_NUMBER_HEADER
88

99
logger = logging.getLogger(__name__)
1010

@@ -20,7 +20,7 @@ def wrapper(event: LambdaEvent, context: LambdaContext) -> dict[str, int | str]:
2020
headers = event.get("headers", {})
2121
path_params = event.get("pathParameters", {})
2222

23-
header_nhs = headers.get(NHS_NUMBER_HEADER_NAME)
23+
header_nhs = headers.get(NHS_NUMBER_HEADER)
2424
path_nhs = path_params.get("id")
2525

2626
logger.info("nhs numbers from the request", extra={"header_nhs": header_nhs, "path_nhs": path_nhs})

tests/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
# LocalStack configuration: https://docs.localstack.cloud/references/configuration/
1010
- DEBUG=${LOCALSTACK_DEBUG:-0}
1111
- DEFAULT_REGION=${AWS_DEFAULT_REGION:-eu-west-1}
12-
- LAMBDA_EXECUTOR=local
12+
- LAMBDA_EXECUTOR=docker
1313
volumes:
1414
- "${LOCALSTACK_VOLUME_DIR:-../volume}:/var/lib/localstack"
1515
- "/var/run/docker.sock:/var/run/docker.sock"

tests/integration/conftest.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ def s3_client(boto3_session: Session, localstack: URL) -> BaseClient:
9797
return boto3_session.client("s3", endpoint_url=str(localstack))
9898

9999

100+
@pytest.fixture(scope="session")
101+
def firehose_client(boto3_session: Session, localstack: URL) -> BaseClient:
102+
return boto3_session.client("firehose", endpoint_url=str(localstack))
103+
104+
105+
@pytest.fixture(autouse=True)
106+
def firehose_delivery_stream(firehose_client: BaseClient) -> dict[str, Any]:
107+
return firehose_client.create_delivery_stream(DeliveryStreamName="eligibility-signposting-audit-stream")
108+
109+
100110
@pytest.fixture(scope="session")
101111
def iam_role(iam_client: BaseClient) -> Generator[str]:
102112
role_name = "LambdaExecutionRole"
@@ -190,6 +200,7 @@ def flask_function(lambda_client: BaseClient, iam_role: str, lambda_zip: Path) -
190200
"Variables": {
191201
"DYNAMODB_ENDPOINT": os.getenv("LOCALSTACK_INTERNAL_ENDPOINT", "http://localstack:4566/"),
192202
"S3_ENDPOINT": os.getenv("LOCALSTACK_INTERNAL_ENDPOINT", "http://localstack:4566/"),
203+
"FIREHOSE_ENDPOINT": os.getenv("LOCALSTACK_INTERNAL_ENDPOINT", "http://localstack:4566/"),
193204
"AWS_REGION": AWS_REGION,
194205
"LOG_LEVEL": "DEBUG",
195206
}
@@ -372,15 +383,23 @@ def persisted_person_pc_sw19(person_table: Any, faker: Faker) -> Generator[eligi
372383

373384

374385
@pytest.fixture(scope="session")
375-
def bucket(s3_client: BaseClient) -> Generator[BucketName]:
386+
def rules_bucket(s3_client: BaseClient) -> Generator[BucketName]:
376387
bucket_name = BucketName(os.getenv("RULES_BUCKET_NAME", "test-rules-bucket"))
377388
s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": AWS_REGION})
378389
yield bucket_name
379390
s3_client.delete_bucket(Bucket=bucket_name)
380391

381392

393+
@pytest.fixture(scope="session")
394+
def audit_bucket(s3_client: BaseClient) -> Generator[BucketName]:
395+
bucket_name = BucketName(os.getenv("AUDIT_BUCKET_NAME", "test-audit-bucket"))
396+
s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration={"LocationConstraint": AWS_REGION})
397+
yield bucket_name
398+
s3_client.delete_bucket(Bucket=bucket_name)
399+
400+
382401
@pytest.fixture(scope="class")
383-
def campaign_config(s3_client: BaseClient, bucket: BucketName) -> Generator[rules.CampaignConfig]:
402+
def campaign_config(s3_client: BaseClient, rules_bucket: BucketName) -> Generator[rules.CampaignConfig]:
384403
campaign: rules.CampaignConfig = rule.CampaignConfigFactory.build(
385404
target="RSV",
386405
iterations=[
@@ -402,14 +421,16 @@ def campaign_config(s3_client: BaseClient, bucket: BucketName) -> Generator[rule
402421
)
403422
campaign_data = {"CampaignConfig": campaign.model_dump(by_alias=True)}
404423
s3_client.put_object(
405-
Bucket=bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
424+
Bucket=rules_bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
406425
)
407426
yield campaign
408-
s3_client.delete_object(Bucket=bucket, Key=f"{campaign.name}.json")
427+
s3_client.delete_object(Bucket=rules_bucket, Key=f"{campaign.name}.json")
409428

410429

411430
@pytest.fixture(scope="class")
412-
def campaign_config_with_magic_cohort(s3_client: BaseClient, bucket: BucketName) -> Generator[rules.CampaignConfig]:
431+
def campaign_config_with_magic_cohort(
432+
s3_client: BaseClient, rules_bucket: BucketName
433+
) -> Generator[rules.CampaignConfig]:
413434
campaign: rules.CampaignConfig = rule.CampaignConfigFactory.build(
414435
target="COVID",
415436
iterations=[
@@ -424,15 +445,15 @@ def campaign_config_with_magic_cohort(s3_client: BaseClient, bucket: BucketName)
424445
)
425446
campaign_data = {"CampaignConfig": campaign.model_dump(by_alias=True)}
426447
s3_client.put_object(
427-
Bucket=bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
448+
Bucket=rules_bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
428449
)
429450
yield campaign
430-
s3_client.delete_object(Bucket=bucket, Key=f"{campaign.name}.json")
451+
s3_client.delete_object(Bucket=rules_bucket, Key=f"{campaign.name}.json")
431452

432453

433454
@pytest.fixture(scope="class")
434455
def campaign_config_with_missing_descriptions_missing_rule_text(
435-
s3_client: BaseClient, bucket: BucketName
456+
s3_client: BaseClient, rules_bucket: BucketName
436457
) -> Generator[rules.CampaignConfig]:
437458
campaign: rules.CampaignConfig = rule.CampaignConfigFactory.build(
438459
target="FLU",
@@ -456,7 +477,7 @@ def campaign_config_with_missing_descriptions_missing_rule_text(
456477
)
457478
campaign_data = {"CampaignConfig": campaign.model_dump(by_alias=True)}
458479
s3_client.put_object(
459-
Bucket=bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
480+
Bucket=rules_bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
460481
)
461482
yield campaign
462-
s3_client.delete_object(Bucket=bucket, Key=f"{campaign.name}.json")
483+
s3_client.delete_object(Bucket=rules_bucket, Key=f"{campaign.name}.json")

tests/integration/lambda/test_app_running_as_lambda.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
import json
33
import logging
4+
import time
45
from http import HTTPStatus
56

67
import httpx
@@ -167,6 +168,8 @@ def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers(
167168
timeout=10,
168169
)
169170

171+
time.sleep(40)
172+
170173
# Then
171174
assert_that(
172175
response,

tests/integration/repo/test_campaign_repo.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@
1212

1313

1414
@pytest.fixture(scope="module")
15-
def campaign_config(s3_client: BaseClient, bucket: BucketName) -> Generator[CampaignConfig]:
15+
def campaign_config(s3_client: BaseClient, rules_bucket: BucketName) -> Generator[CampaignConfig]:
1616
campaign: CampaignConfig = CampaignConfigFactory.build()
1717
campaign_data = {"CampaignConfig": campaign.model_dump(by_alias=True)}
1818
s3_client.put_object(
19-
Bucket=bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
19+
Bucket=rules_bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
2020
)
2121
yield campaign
22-
s3_client.delete_object(Bucket=bucket, Key=f"{campaign.name}.json")
22+
s3_client.delete_object(Bucket=rules_bucket, Key=f"{campaign.name}.json")
2323

2424

25-
def test_get_campaign_config(s3_client: BaseClient, bucket: BucketName, campaign_config: CampaignConfig):
25+
def test_get_campaign_config(s3_client: BaseClient, rules_bucket: BucketName, campaign_config: CampaignConfig):
2626
# Given
27-
repo = CampaignRepo(s3_client, bucket)
27+
repo = CampaignRepo(s3_client, rules_bucket)
2828

2929
# When
3030
actual = list(repo.get_campaign_configs())

0 commit comments

Comments
 (0)