diff --git a/pyproject.toml b/pyproject.toml index 31e88965..220e721f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ pythonVersion = "3.13" [tool.pytest.ini_options] log_cli = true -log_cli_level = "INFO" +log_cli_level = "DEBUG" log_format = "%(asctime)s %(levelname)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" diff --git a/src/eligibility_signposting_api/app.py b/src/eligibility_signposting_api/app.py index ad639ac0..0bfcbf84 100644 --- a/src/eligibility_signposting_api/app.py +++ b/src/eligibility_signposting_api/app.py @@ -10,6 +10,7 @@ from eligibility_signposting_api import repos, services from eligibility_signposting_api.config import LOG_LEVEL, config, init_logging from eligibility_signposting_api.error_handler import handle_exception +from eligibility_signposting_api.views.eligibility import eligibility from eligibility_signposting_api.views.hello import hello @@ -32,7 +33,8 @@ def create_app() -> Flask: app.logger.info("app created") # Register views & error handler - app.register_blueprint(hello) + app.register_blueprint(eligibility, url_prefix="/eligibility") + app.register_blueprint(hello, url_prefix="/hello") app.register_error_handler(Exception, handle_exception) # Set up dependency injection using wireup diff --git a/src/eligibility_signposting_api/config.py b/src/eligibility_signposting_api/config.py index c2a8856c..78a43693 100644 --- a/src/eligibility_signposting_api/config.py +++ b/src/eligibility_signposting_api/config.py @@ -36,5 +36,8 @@ def init_logging() -> None: "wsgi": {"class": "logging.StreamHandler", "stream": "ext://sys.stdout", "formatter": "default"} }, "root": {"level": level, "handlers": ["wsgi"]}, + "loggers": { + "eligibility_signposting_api.app": {"level": level, "handlers": ["wsgi"], "propagate": False}, + }, } ) diff --git a/src/eligibility_signposting_api/error_handler.py b/src/eligibility_signposting_api/error_handler.py index 732d4d94..d3a82b99 100644 --- a/src/eligibility_signposting_api/error_handler.py +++ b/src/eligibility_signposting_api/error_handler.py @@ -4,14 +4,20 @@ from flask import make_response from flask.typing import ResponseReturnValue +from werkzeug.exceptions import HTTPException from eligibility_signposting_api.views.response_models import Problem logger = logging.getLogger(__name__) -def handle_exception(e: BaseException) -> ResponseReturnValue: +def handle_exception(e: Exception) -> ResponseReturnValue | HTTPException: logger.exception("Unexpected Exception", exc_info=e) + + # Let Flask handle its own exceptions for now. + if isinstance(e, HTTPException): + return e + problem = Problem( title="Unexpected Exception", type=str(type(e)), diff --git a/src/eligibility_signposting_api/model/eligibility.py b/src/eligibility_signposting_api/model/eligibility.py new file mode 100644 index 00000000..83ea6ffa --- /dev/null +++ b/src/eligibility_signposting_api/model/eligibility.py @@ -0,0 +1,3 @@ +from typing import NewType + +NHSNumber = NewType("NHSNumber", str) diff --git a/src/eligibility_signposting_api/views/eligibility.py b/src/eligibility_signposting_api/views/eligibility.py new file mode 100644 index 00000000..7f591534 --- /dev/null +++ b/src/eligibility_signposting_api/views/eligibility.py @@ -0,0 +1,17 @@ +import logging +from http import HTTPStatus + +from flask import Blueprint, make_response +from flask.typing import ResponseReturnValue + +from eligibility_signposting_api.model.eligibility import NHSNumber + +logger = logging.getLogger(__name__) + +eligibility = Blueprint("eligibility", __name__) + + +@eligibility.get("/") +def check_eligibility(nhs_number: NHSNumber) -> ResponseReturnValue: + logger.info("nhs_number: %s", nhs_number) + return make_response({}, HTTPStatus.OK) diff --git a/tests/integration/in_process/test_eligibility_endpoint.py b/tests/integration/in_process/test_eligibility_endpoint.py new file mode 100644 index 00000000..71aadb7c --- /dev/null +++ b/tests/integration/in_process/test_eligibility_endpoint.py @@ -0,0 +1,26 @@ +from http import HTTPStatus + +from brunns.matchers.data import json_matching as is_json_that +from brunns.matchers.werkzeug import is_werkzeug_response as is_response +from flask.testing import FlaskClient +from hamcrest import assert_that, empty, is_ + + +def test_nhs_number_given(client: FlaskClient): + # Given + + # When + response = client.get("/eligibility/12345") + + # Then + assert_that(response, is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(is_(empty())))) + + +def test_no_nhs_number_given(client: FlaskClient): + # Given + + # When + response = client.get("/eligibility/") + + # Then + assert_that(response, is_response().with_status_code(HTTPStatus.NOT_FOUND)) diff --git a/tests/integration/test_flask_app.py b/tests/integration/in_process/test_hello_world_endpoint.py similarity index 93% rename from tests/integration/test_flask_app.py rename to tests/integration/in_process/test_hello_world_endpoint.py index d2bfdf25..d8ea5c3c 100644 --- a/tests/integration/test_flask_app.py +++ b/tests/integration/in_process/test_hello_world_endpoint.py @@ -25,7 +25,7 @@ def test_no_name_given(client: FlaskClient): # Given # When - response = client.get("/") + response = client.get("/hello/") # Then assert_that( @@ -40,7 +40,7 @@ def test_app_for_name_with_nickname(client: FlaskClient): # Given # When - response = client.get("/simon") + response = client.get("/hello/simon") # Then assert_that( @@ -55,7 +55,7 @@ def test_app_for_nonexistent_name(client: FlaskClient): # Given # When - response = client.get("/fred") + response = client.get("/hello/fred") # Then assert_that( diff --git a/tests/integration/test_app_running_as_lambda.py b/tests/integration/lambda/test_app_running_as_lambda.py similarity index 89% rename from tests/integration/test_app_running_as_lambda.py rename to tests/integration/lambda/test_app_running_as_lambda.py index 30b5f1a5..a58538c6 100644 --- a/tests/integration/test_app_running_as_lambda.py +++ b/tests/integration/lambda/test_app_running_as_lambda.py @@ -38,7 +38,9 @@ def test_install_and_call_lambda_flask(lambda_client: BaseClient, flask_function "rawPath": "/", "rawQueryString": "", "headers": {"accept": "application/json", "content-type": "application/json"}, - "requestContext": {"http": {"sourceIp": "192.0.0.1", "method": "GET", "path": "/", "protocol": "HTTP/1.1"}}, + "requestContext": { + "http": {"sourceIp": "192.0.0.1", "method": "GET", "path": "/hello/", "protocol": "HTTP/1.1"} + }, "body": None, "isBase64Encoded": False, } @@ -60,7 +62,7 @@ def test_install_and_call_flask_lambda_over_http(flask_function_url: URL): # Given # When - response = httpx.get(str(flask_function_url)) + response = httpx.get(str(flask_function_url / "hello" / "")) # Then assert_that( @@ -76,7 +78,7 @@ def test_install_and_call_flask_lambda_with_nickname_over_http(flask_function_ur # Given # When - response = httpx.get(str(flask_function_url / "ayesh"), timeout=30) + response = httpx.get(str(flask_function_url / "hello" / "ayesh"), timeout=30) # Then assert_that( diff --git a/tests/unit/views/test_eligibility.py b/tests/unit/views/test_eligibility.py new file mode 100644 index 00000000..62d347c5 --- /dev/null +++ b/tests/unit/views/test_eligibility.py @@ -0,0 +1,29 @@ +import logging +from http import HTTPStatus + +from brunns.matchers.data import json_matching as is_json_that +from brunns.matchers.werkzeug import is_werkzeug_response as is_response +from flask.testing import FlaskClient +from hamcrest import assert_that, empty, is_ + +logger = logging.getLogger(__name__) + + +def test_nhs_number_given(client: FlaskClient): + # Given + + # When + response = client.get("/eligibility/12345") + + # Then + assert_that(response, is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(is_(empty())))) + + +def test_no_nhs_number_given(client: FlaskClient): + # Given + + # When + response = client.get("/eligibility/") + + # Then + assert_that(response, is_response().with_status_code(HTTPStatus.NOT_FOUND)) diff --git a/tests/unit/views/test_hello.py b/tests/unit/views/test_hello.py index 9f332944..52157461 100644 --- a/tests/unit/views/test_hello.py +++ b/tests/unit/views/test_hello.py @@ -39,23 +39,23 @@ def get_nickname(self, _: str | None = None) -> str: def test_name_given(app: Flask, client: FlaskClient): with get_container(app).override.service(PersonService, new=FakePersonService()): - response = client.get("/simon") + response = client.get("/hello/simon") assert_that(response, is_response().with_status_code(HTTPStatus.OK).and_text(contains_string("SIMON"))) def test_default_name(app: Flask, client: FlaskClient): with get_container(app).override.service(PersonService, new=FakePersonService()): - response = client.get("/") + response = client.get("/hello/") assert_that(response, is_response().with_status_code(HTTPStatus.OK).and_text(contains_string("Default"))) def test_unknown_name(app: Flask, client: FlaskClient): with get_container(app).override.service(PersonService, new=FakeUnknownPersonService()): - response = client.get("/fred") + response = client.get("/hello/fred") assert_that(response, is_response().with_status_code(HTTPStatus.NOT_FOUND)) def test_unexpected_error(app: Flask, client: FlaskClient): with get_container(app).override.service(PersonService, new=FakeUnexpectedErrorPersonService()): - response = client.get("/fred") + response = client.get("/hello/fred") assert_that(response, is_response().with_status_code(HTTPStatus.INTERNAL_SERVER_ERROR))