Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate db migrations #144

Closed
wants to merge 49 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c0621f7
Remove code not currently in use on feature branch
steventux Jan 6, 2025
b12ece2
Add Flask dependencies
steventux Jan 6, 2025
df390a2
Add basic Flask app
steventux Jan 6, 2025
51b3f02
Add function app which delegates requests to Flask
steventux Jan 6, 2025
99bc84d
Test Flask app in CI
steventux Jan 6, 2025
62883dc
Revise Dockerfile/compose.yml for one function app with db
steventux Jan 7, 2025
52f7f9e
alembic init
dnimmo Jan 9, 2025
6403908
Add Flask dependencies
steventux Jan 6, 2025
4757e84
Add basic Flask app
steventux Jan 6, 2025
b40ad35
Add function app which delegates requests to Flask
steventux Jan 6, 2025
557764d
Test Flask app in CI
steventux Jan 6, 2025
6f47c02
Revise Dockerfile/compose.yml for one function app with db
steventux Jan 7, 2025
8e618ff
added alembic for migrations
dnimmo Jan 14, 2025
7148b32
alembic init
dnimmo Jan 9, 2025
3af3ccf
added alembic for migrations
dnimmo Jan 14, 2025
b87b43e
updated lockfile
dnimmo Jan 14, 2025
c53cc53
Add pytest-sugar
steventux Jan 14, 2025
b8d65b1
Add request verifier
steventux Jan 14, 2025
f02ddb9
Access headers with lowercase keys
steventux Jan 14, 2025
5e96744
Add POST status create endpoint
steventux Jan 14, 2025
fe893c3
Add jsonschema validation library
steventux Jan 14, 2025
8364991
Validate request body data against NHS Notify schema
steventux Jan 15, 2025
803059a
Move status endpoints to route handlers
steventux Jan 15, 2025
d6209cd
Update name of healthcheck route function to match path
steventux Jan 15, 2025
ea4fbdd
Merge pull request #140 from NHSDigital/add-status-endpoint-to-flask
steventux Jan 15, 2025
2bef605
Ensure local test runs use test db
steventux Jan 16, 2025
5a8f46f
Add status recorder service
steventux Jan 16, 2025
e1126d1
Use psycopg2 sql.Identifier to safely interpolate the table name
steventux Jan 16, 2025
9722944
Add FLASK_DEBUG env vars
steventux Jan 16, 2025
acf0110
Extract hmac signature generation
steventux Jan 16, 2025
24ffac4
Read the post body as json and ensure key sorting is consistent
steventux Jan 16, 2025
372eb62
Truncate all tables after each test
steventux Jan 16, 2025
4263a47
Return useful error descriptions in the status/create response
steventux Jan 16, 2025
ec5d825
Add postgres to CI test jobs
steventux Jan 16, 2025
a427e07
PR comments
dnimmo Jan 17, 2025
8a8f8e3
Merge branch 'feature-flask-and-function-apps' of https://github.com/…
dnimmo Jan 20, 2025
385c076
file formatting fixes for auto-generated alembic init
dnimmo Jan 20, 2025
9ef879a
removed schema.hcl
dnimmo Jan 20, 2025
8d415f5
re-add schema.hcl
dnimmo Jan 20, 2025
df1c138
DTOSS-6505 update Pipfile
dnimmo Jan 20, 2025
e0ef3d4
replace print with logger
dnimmo Jan 20, 2025
97472a4
Refactor persistence tests
steventux Jan 20, 2025
afa461f
Merge pull request #142 from NHSDigital/persist-status-callback-requests
steventux Jan 21, 2025
46476d6
Merge pull request #139 from NHSDigital/DTOSS-6505
dnimmo Jan 21, 2025
a2a5763
Add sqlalchemy_utils dependency
steventux Jan 21, 2025
c2ae0cb
Add alembic_postgresql_enum dependency
steventux Jan 21, 2025
a6f87ff
Refactor schema for batch message endpoint
steventux Jan 21, 2025
4ae3227
Remove schema initialisation from database module
steventux Jan 22, 2025
86dff02
Run alembic upgrade head on test CI dbs
steventux Jan 22, 2025
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
Prev Previous commit
Next Next commit
Return useful error descriptions in the status/create response
steventux committed Jan 16, 2025

Verified

This commit was signed with the committer’s verified signature.
steventux Steve Laing
commit 4263a471cd90e0706f7a1f3dcd25a9cb05a40050
29 changes: 15 additions & 14 deletions src/notify/app/route_handlers/status.py
Original file line number Diff line number Diff line change
@@ -4,18 +4,19 @@


def create():
if request_validator.verify_headers(dict(request.headers)) is False:
status_code = 401
body = {"status": "error"}
elif request_validator.verify_signature(dict(request.headers), request.json) is False:
status_code = 403
body = {"status": "error"}
elif request_validator.verify_body(request.json)[0] is False:
status_code = 422
body = {"status": "error"}
else:
status_recorder.save_statuses(request.json)
status_code = 200
body = {"status": "success"}
json_data = request.json or {}
valid_headers, error_message = request_validator.verify_headers(dict(request.headers))

return body, status_code
if not valid_headers:
return {"status": error_message}, 401

if not request_validator.verify_signature(dict(request.headers), json_data):
return {"status": "Invalid signature"}, 403

valid_body, error_message = request_validator.verify_body(json_data)

if not valid_body:
return {"status": error_message}, 422

if status_recorder.save_statuses(json_data):
return {"status": "success"}, 200
4 changes: 2 additions & 2 deletions src/notify/app/services/status_recorder.py
Original file line number Diff line number Diff line change
@@ -3,14 +3,14 @@
import logging


def save_statuses(request_body: dict) -> None:
def save_statuses(request_body: dict) -> bool:
statuses: list[dict] = status_params(request_body)
status_type = request_body["data"][0]["type"]

for status in statuses:
datastore.create_status_record(status_type, status)

return None
return True


def status_params(request_body: dict) -> list[dict]:
15 changes: 8 additions & 7 deletions src/notify/app/validators/request_validator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import hashlib
import hmac
import json
import logging
import os
import app.validators.schema_validator as schema_validator
import app.utils.hmac_signature as hmac_signature
@@ -10,16 +9,18 @@
SIGNATURE_HEADER_NAME = 'x-hmac-sha256-signature'


def verify_headers(headers: dict) -> bool:
def verify_headers(headers: dict) -> tuple[bool, str]:
lc_headers = header_keys_to_lower(headers)
if (lc_headers.get(API_KEY_HEADER_NAME) is None or
lc_headers.get(API_KEY_HEADER_NAME) != os.getenv('NOTIFY_API_KEY')):
return False
if lc_headers.get(API_KEY_HEADER_NAME) is None:
return False, "Missing API key header"

if lc_headers.get(API_KEY_HEADER_NAME) != os.getenv('NOTIFY_API_KEY'):
return False, "Invalid API key"

if lc_headers.get(SIGNATURE_HEADER_NAME) is None:
return False
return False, "Missing signature header"

return True
return True, ""


def verify_signature(headers: dict, body: dict) -> bool:
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ def test_status_create_request_validation_fails(setup, client, message_status_po
response = client.post('/api/status/create', json=message_status_post_body, headers=headers)

assert response.status_code == 403
assert response.get_json() == {"status": "error"}
assert response.get_json() == {"status": "Invalid signature"}


def test_status_create_body_validation_fails(setup, client, message_status_post_body):
@@ -40,7 +40,7 @@ def test_status_create_body_validation_fails(setup, client, message_status_post_
response = client.post('/api/status/create', json=message_status_post_body, headers=headers)

assert response.status_code == 422
assert response.get_json() == {"status": "error"}
assert response.get_json() == {"status": "'invalid' is not one of ['created', 'pending_enrichment', 'enriched', 'sending', 'delivered', 'failed']"}



Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ def test_save_statuses_with_channel_status_data(mocker, channel_status_post_body
"""Test saving channel status data to datastore"""
mock_datastore = mocker.patch("app.services.status_recorder.datastore")

assert status_recorder.save_statuses(channel_status_post_body) is None
assert status_recorder.save_statuses(channel_status_post_body)

mock_datastore.create_status_record.assert_called_once_with("ChannelStatus", {
"details": json.dumps(channel_status_post_body),
@@ -21,7 +21,7 @@ def test_save_statuses_with_message_status_data(mocker, message_status_post_body
"""Test saving message status data to datastore"""
mock_datastore = mocker.patch("app.services.status_recorder.datastore")

assert status_recorder.save_statuses(message_status_post_body) is None
assert status_recorder.save_statuses(message_status_post_body)

mock_datastore.create_status_record.assert_called_once_with("MessageStatus", {
"details": json.dumps(message_status_post_body),
8 changes: 4 additions & 4 deletions tests/unit/notify/app/validators/test_request_validator.py
Original file line number Diff line number Diff line change
@@ -33,19 +33,19 @@ def test_verify_signature_valid(setup):
def test_verify_headers_missing_all(setup):
"""Test that missing all headers fails verification."""
headers = {}
assert not request_validator.verify_headers(headers)
assert request_validator.verify_headers(headers) == (False, 'Missing API key header')


def test_verify_headers_missing_api_key(setup):
"""Test that missing API key header fails verification."""
headers = {request_validator.SIGNATURE_HEADER_NAME: 'signature'}
assert not request_validator.verify_headers(headers)
assert request_validator.verify_headers(headers) == (False, 'Missing API key header')


def test_verify_headers_missing_signature(setup):
"""Test that missing signature header fails verification."""
headers = {request_validator.API_KEY_HEADER_NAME: 'api_key'}
assert not request_validator.verify_headers(headers)
assert request_validator.verify_headers(headers) == (False, 'Missing signature header')


def test_verify_headers_valid(setup):
@@ -60,4 +60,4 @@ def test_verify_headers_valid(setup):
def test_verify_headers_invalid_api_key(setup):
"""Test that an invalid API key fails verification."""
headers = {request_validator.API_KEY_HEADER_NAME: 'invalid_api_key'}
assert not request_validator.verify_headers(headers)
assert request_validator.verify_headers(headers) == (False, 'Invalid API key')