diff --git a/.development.env b/.development.env index f8011aa2d3..c9546f9d6a 100644 --- a/.development.env +++ b/.development.env @@ -27,3 +27,4 @@ CDN_ASSETS_PATH=/design-system ADDRESS_LOOKUP_API_URL=https://whitelodge-ai-api.census-gcp.onsdigital.uk COOKIE_SETTINGS_URL=# EQ_SUBMISSION_CONFIRMATION_BACKEND=log +SDS_API_BASE_URL=http://localhost:5003/v1/unit_data diff --git a/.functional-tests.env b/.functional-tests.env index cd5b1c31a7..3192849c18 100644 --- a/.functional-tests.env +++ b/.functional-tests.env @@ -28,3 +28,4 @@ ADDRESS_LOOKUP_API_URL=https://whitelodge-ai-api.census-gcp.onsdigital.uk COOKIE_SETTINGS_URL=# EQ_SUBMISSION_CONFIRMATION_BACKEND=log VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS=35 +SDS_API_BASE_URL=http://localhost:5003/v1/unit_data diff --git a/app/authentication/auth_payload_version.py b/app/authentication/auth_payload_versions.py similarity index 53% rename from app/authentication/auth_payload_version.py rename to app/authentication/auth_payload_versions.py index fe4d338d37..f467b7e118 100644 --- a/app/authentication/auth_payload_version.py +++ b/app/authentication/auth_payload_versions.py @@ -3,3 +3,7 @@ class AuthPayloadVersion(Enum): V2 = "v2" + + +class SupplementaryDataSchemaVersion(Enum): + V1 = "v1" diff --git a/app/data_models/metadata_proxy.py b/app/data_models/metadata_proxy.py index 28e12dee3d..a3b2414674 100644 --- a/app/data_models/metadata_proxy.py +++ b/app/data_models/metadata_proxy.py @@ -7,7 +7,7 @@ from werkzeug.datastructures import ImmutableDict -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.utilities.make_immutable import make_immutable diff --git a/app/routes/errors.py b/app/routes/errors.py index 2d8f8d2b6c..ecac804181 100644 --- a/app/routes/errors.py +++ b/app/routes/errors.py @@ -22,6 +22,7 @@ from app.globals import get_metadata from app.helpers.language_helper import handle_language from app.helpers.template_helpers import get_survey_config, render_template +from app.services.supplementary_data import SupplementaryDataRequestFailed from app.settings import ACCOUNT_SERVICE_BASE_URL_SOCIAL from app.submitter.previously_submitted_exception import PreviouslySubmittedException from app.submitter.submission_failed import SubmissionFailedException @@ -189,6 +190,14 @@ def too_many_feedback_requests( ) +@errors_blueprint.app_errorhandler(SupplementaryDataRequestFailed) +def supplementary_data_request_failed( + exception: SupplementaryDataRequestFailed, +) -> tuple[str, int]: + log_exception(exception, 500) + return _render_error_page(500, template=500) + + @errors_blueprint.app_errorhandler(SubmissionFailedException) def submission_failed( exception: SubmissionFailedException, diff --git a/app/routes/flush.py b/app/routes/flush.py index 28cb2093b2..e99cbb6b89 100644 --- a/app/routes/flush.py +++ b/app/routes/flush.py @@ -7,7 +7,7 @@ from sdc.crypto.key_store import KeyStore from structlog import contextvars, get_logger -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.authentication.user import User from app.authentication.user_id_generator import UserIDGenerator from app.data_models import QuestionnaireStore diff --git a/app/routes/session.py b/app/routes/session.py index 56c2653a69..ed05508320 100644 --- a/app/routes/session.py +++ b/app/routes/session.py @@ -11,7 +11,7 @@ from werkzeug.exceptions import Unauthorized from werkzeug.wrappers.response import Response -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.authentication.authenticator import decrypt_token, store_session from app.authentication.jti_claim_storage import JtiTokenUsed, use_jti_claim from app.data_models.metadata_proxy import MetadataProxy @@ -23,6 +23,7 @@ ) from app.questionnaire import QuestionnaireSchema from app.routes.errors import _render_error_page +from app.services.supplementary_data import get_supplementary_data from app.utilities.metadata_parser import validate_runner_claims from app.utilities.metadata_parser_v2 import ( validate_questionnaire_claims, @@ -128,6 +129,14 @@ def login() -> Response: cookie_session["language_code"] = metadata.language_code + # Type ignore: survey_id and either ru_ref or qid are required for schemas that use supplementary data + if dataset_id := metadata["sds_dataset_id"]: + get_supplementary_data( + dataset_id=dataset_id, + unit_id=metadata["ru_ref"] or metadata["qid"], # type: ignore + survey_id=metadata["survey_id"], # type: ignore + ) + return redirect(url_for("questionnaire.get_questionnaire")) diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/services/supplementary_data.py b/app/services/supplementary_data.py new file mode 100644 index 0000000000..12bcea9bdb --- /dev/null +++ b/app/services/supplementary_data.py @@ -0,0 +1,92 @@ +import json +from typing import Mapping +from urllib.parse import urlencode + +from flask import current_app +from marshmallow import ValidationError +from requests import RequestException +from structlog import get_logger + +from app.utilities.request_session import get_retryable_session +from app.utilities.supplementary_data_parser import validate_supplementary_data_v1 + +SUPPLEMENTARY_DATA_REQUEST_BACKOFF_FACTOR = 0.2 +SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES = 2 # Totals no. of request should be 3. The initial request + SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES +SUPPLEMENTARY_DATA_REQUEST_TIMEOUT = 3 +SUPPLEMENTARY_DATA_REQUEST_RETRY_STATUS_CODES = [ + 408, + 429, + 500, + 502, + 503, + 504, +] + +logger = get_logger() + + +class SupplementaryDataRequestFailed(Exception): + def __str__(self) -> str: + return "Supplementary Data request failed" + + +def get_supplementary_data(*, dataset_id: str, unit_id: str, survey_id: str) -> dict: + supplementary_data_url = current_app.config["SDS_API_BASE_URL"] + + parameters = {"dataset_id": dataset_id, "unit_id": unit_id} + + encoded_parameters = urlencode(parameters) + constructed_supplementary_data_url = ( + f"{supplementary_data_url}?{encoded_parameters}" + ) + + session = get_retryable_session( + max_retries=SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES, + retry_status_codes=SUPPLEMENTARY_DATA_REQUEST_RETRY_STATUS_CODES, + backoff_factor=SUPPLEMENTARY_DATA_REQUEST_BACKOFF_FACTOR, + ) + + try: + response = session.get( + constructed_supplementary_data_url, + timeout=SUPPLEMENTARY_DATA_REQUEST_TIMEOUT, + ) + except RequestException as exc: + logger.exception( + "Error requesting supplementary data", + supplementary_data_url=constructed_supplementary_data_url, + ) + raise SupplementaryDataRequestFailed from exc + + if response.status_code == 200: + supplementary_data_response_content = response.content.decode() + supplementary_data = json.loads(supplementary_data_response_content) + + return validate_supplementary_data( + supplementary_data=supplementary_data, + dataset_id=dataset_id, + unit_id=unit_id, + survey_id=survey_id, + ) + + logger.error( + "got a non-200 response for supplementary data request", + status_code=response.status_code, + schema_url=constructed_supplementary_data_url, + ) + + raise SupplementaryDataRequestFailed + + +def validate_supplementary_data( + supplementary_data: Mapping, dataset_id: str, unit_id: str, survey_id: str +) -> dict: + try: + return validate_supplementary_data_v1( + supplementary_data=supplementary_data, + dataset_id=dataset_id, + unit_id=unit_id, + survey_id=survey_id, + ) + except ValidationError as e: + raise ValidationError("Invalid supplementary data") from e diff --git a/app/settings.py b/app/settings.py index 675c8f1bd2..813e5637b4 100644 --- a/app/settings.py +++ b/app/settings.py @@ -144,6 +144,7 @@ def utcoffset_or_fail(date_value, key): SURVEY_TYPE = os.getenv("SURVEY_TYPE", "business") +SDS_API_BASE_URL = os.getenv("SDS_API_BASE_URL") ACCOUNT_SERVICE_BASE_URL = os.getenv( "ACCOUNT_SERVICE_BASE_URL", "https://surveys.ons.gov.uk" diff --git a/app/submitter/converter_v2.py b/app/submitter/converter_v2.py index c2e88745b2..e393890b61 100644 --- a/app/submitter/converter_v2.py +++ b/app/submitter/converter_v2.py @@ -3,7 +3,7 @@ from structlog import get_logger -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models import AnswerStore, ListStore, ProgressStore, QuestionnaireStore from app.data_models.metadata_proxy import MetadataProxy, NoMetadataException from app.questionnaire.questionnaire_schema import ( diff --git a/app/utilities/metadata_parser_v2.py b/app/utilities/metadata_parser_v2.py index 51fa8eaa8d..41ff03a10c 100644 --- a/app/utilities/metadata_parser_v2.py +++ b/app/utilities/metadata_parser_v2.py @@ -14,7 +14,7 @@ ) from structlog import get_logger -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.questionnaire.rules.utils import parse_iso_8601_datetime from app.utilities.metadata_validators import DateString, RegionCode, UUIDString diff --git a/app/utilities/request_session.py b/app/utilities/request_session.py new file mode 100644 index 0000000000..4835d06a7a --- /dev/null +++ b/app/utilities/request_session.py @@ -0,0 +1,21 @@ +import requests +from requests.adapters import HTTPAdapter +from urllib3 import Retry + + +def get_retryable_session( + max_retries, retry_status_codes, backoff_factor +) -> requests.Session: + session = requests.Session() + + retries = Retry( + total=max_retries, + status_forcelist=retry_status_codes, + ) # Codes to retry according to Google Docs https://cloud.google.com/storage/docs/retry-strategy#client-libraries + + retries.backoff_factor = backoff_factor + + session.mount("http://", HTTPAdapter(max_retries=retries)) + session.mount("https://", HTTPAdapter(max_retries=retries)) + + return session diff --git a/app/utilities/schema.py b/app/utilities/schema.py index 994f700423..4845a7df33 100644 --- a/app/utilities/schema.py +++ b/app/utilities/schema.py @@ -5,9 +5,7 @@ from pathlib import Path from typing import Any -import requests from requests import RequestException -from requests.adapters import HTTPAdapter, Retry from structlog import get_logger from app.data_models.metadata_proxy import MetadataProxy @@ -16,6 +14,7 @@ QuestionnaireSchema, ) from app.utilities.json import json_load, json_loads +from app.utilities.request_session import get_retryable_session logger = get_logger() @@ -28,7 +27,7 @@ "phm_0001": [["en", "cy"]], } -SCHEMA_REQUEST_MAX_BACKOFF = 0.2 +SCHEMA_REQUEST_BACKOFF_FACTOR = 0.2 SCHEMA_REQUEST_MAX_RETRIES = 2 # Totals no. of request should be 3. The initial request + SCHEMA_REQUEST_MAX_RETRIES SCHEMA_REQUEST_TIMEOUT = 3 SCHEMA_REQUEST_RETRY_STATUS_CODES = [ @@ -210,18 +209,11 @@ def load_schema_from_url( constructed_schema_url = f"{schema_url}?language={language_code}" - session = requests.Session() - - retries = Retry( - total=SCHEMA_REQUEST_MAX_RETRIES, - status_forcelist=SCHEMA_REQUEST_RETRY_STATUS_CODES, - ) # Codes to retry according to Google Docs https://cloud.google.com/storage/docs/retry-strategy#client-libraries - - # Type ignore: MyPy does not recognise BACKOFF_MAX however it is a property, albeit deprecated - retries.BACKOFF_MAX = SCHEMA_REQUEST_MAX_BACKOFF # type: ignore - - session.mount("http://", HTTPAdapter(max_retries=retries)) - session.mount("https://", HTTPAdapter(max_retries=retries)) + session = get_retryable_session( + max_retries=SCHEMA_REQUEST_MAX_RETRIES, + retry_status_codes=SCHEMA_REQUEST_RETRY_STATUS_CODES, + backoff_factor=SCHEMA_REQUEST_BACKOFF_FACTOR, + ) try: req = session.get(constructed_schema_url, timeout=SCHEMA_REQUEST_TIMEOUT) diff --git a/app/utilities/supplementary_data_parser.py b/app/utilities/supplementary_data_parser.py new file mode 100644 index 0000000000..8ffee3e3f7 --- /dev/null +++ b/app/utilities/supplementary_data_parser.py @@ -0,0 +1,89 @@ +from typing import Mapping + +from marshmallow import ( + INCLUDE, + Schema, + ValidationError, + fields, + validate, + validates_schema, +) + +from app.authentication.auth_payload_versions import SupplementaryDataSchemaVersion +from app.utilities.metadata_parser_v2 import VALIDATORS, StripWhitespaceMixin + + +class ItemsSchema(Schema): + identifier = VALIDATORS["string"](validate=validate.Length(min=1)) + + +class ItemsData(Schema, StripWhitespaceMixin): + pass + + +class SupplementaryData(Schema, StripWhitespaceMixin): + identifier = VALIDATORS["string"](validate=validate.Length(min=1)) + schema_version = VALIDATORS["string"]( + validate=validate.OneOf([SupplementaryDataSchemaVersion.V1.value]) + ) + items = fields.Nested(ItemsData, required=False, unknown=INCLUDE) + + @validates_schema() + def validate_unit_id(self, data, **kwargs): + # pylint: disable=no-self-use, unused-argument + if data and data["identifier"] != self.context["unit_id"]: + raise ValidationError( + "Supplementary data did not return the specified Unit ID" + ) + + +class SupplementaryDataMetadataSchema(Schema, StripWhitespaceMixin): + dataset_id = VALIDATORS["uuid"]() + survey_id = VALIDATORS["string"](validate=validate.Length(min=1)) + data = fields.Nested( + SupplementaryData, + required=True, + unknown=INCLUDE, + validate=validate.Length(min=1), + ) + + @validates_schema() + def validate_dataset_and_survey_id(self, data, **kwargs): + # pylint: disable=no-self-use, unused-argument + if data: + if data["dataset_id"] != self.context["dataset_id"]: + raise ValidationError( + "Supplementary data did not return the specified Dataset ID" + ) + + if data["survey_id"] != self.context["survey_id"]: + raise ValidationError( + "Supplementary data did not return the specified Survey ID" + ) + + +def validate_supplementary_data_v1( + supplementary_data: Mapping, + dataset_id: str, + unit_id: str, + survey_id: str, +) -> dict: + """Validate claims required for supplementary data""" + supplementary_data_metadata_schema = SupplementaryDataMetadataSchema( + unknown=INCLUDE + ) + supplementary_data_metadata_schema.context = { + "dataset_id": dataset_id, + "unit_id": unit_id, + "survey_id": survey_id, + } + validated_supplementary_data = supplementary_data_metadata_schema.load( + supplementary_data + ) + + if supplementary_data_items := supplementary_data.get("data", {}).get("items"): + for key, values in supplementary_data_items.items(): + items = [ItemsSchema(unknown=INCLUDE).load(value) for value in values] + validated_supplementary_data["data"]["items"][key] = items + + return validated_supplementary_data diff --git a/app/views/handlers/feedback.py b/app/views/handlers/feedback.py index b13b163754..8f34d0bbf1 100644 --- a/app/views/handlers/feedback.py +++ b/app/views/handlers/feedback.py @@ -7,7 +7,7 @@ from sdc.crypto.encrypter import encrypt from werkzeug.datastructures import MultiDict -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models import QuestionnaireStore from app.data_models.metadata_proxy import MetadataProxy, NoMetadataException from app.data_models.session_data import SessionData diff --git a/app/views/handlers/submission.py b/app/views/handlers/submission.py index 97857f6578..8fd24d4a38 100644 --- a/app/views/handlers/submission.py +++ b/app/views/handlers/submission.py @@ -5,7 +5,7 @@ from flask import session as cookie_session from sdc.crypto.encrypter import encrypt -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models.metadata_proxy import MetadataProxy from app.globals import get_session_store from app.keys import KEY_PURPOSE_SUBMISSION diff --git a/doc/run-mock-sds-endpoint.md b/doc/run-mock-sds-endpoint.md new file mode 100644 index 0000000000..10a656d192 --- /dev/null +++ b/doc/run-mock-sds-endpoint.md @@ -0,0 +1,23 @@ +# Running the Mock SDS endpoint + +In order to test loading supplementary data, we have a development script that creates a Mock SDS endpoint in Flask that +returns mocked supplementary data. + +Ensure the following env var is set before running the script: +```bash +SDS_API_BASE_URL=http://localhost:5003/v1/unit_data +``` + +From the home directory, run using +```python +python -m scripts.mock_sds_endpoint +``` + +The following datasets are available using the mocked endpoint. To retrieve the data, one of the following dataset IDs needs to be set using the `sds_dataset_id` +field in Launcher. + +| Dataset ID | Description | +|------------------------|------------------------------------------------------------| +| `c067f6de-6d64-42b1-8b02-431a3486c178` | Basic supplementary data structure with no repeating items | +| `34a80231-c49a-44d0-91a6-8fe1fb190e64` | Supplementary data structure with repeating items | + diff --git a/schemas/test/en/test_supplementary_data.json b/schemas/test/en/test_supplementary_data.json new file mode 100644 index 0000000000..e19d73474d --- /dev/null +++ b/schemas/test/en/test_supplementary_data.json @@ -0,0 +1,65 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test Supplementary Data", + "theme": "default", + "description": "A questionnaire to demo loading Supplementary data.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "sds_dataset_id", + "type": "string" + }, + { + "name": "survey_id", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "interstitial-section", + "groups": [ + { + "blocks": [ + { + "id": "interstitial-definition", + "content": { + "title": "Supplementary Data", + "contents": [ + { + "description": "You have successfully loaded Supplementary data" + } + ] + }, + "type": "Interstitial" + } + ], + "id": "interstitial", + "title": "Interstitial Definition" + } + ] + } + ] +} diff --git a/scripts/mock_data/supplementary_data_no_repeat.json b/scripts/mock_data/supplementary_data_no_repeat.json new file mode 100644 index 0000000000..275fbdb5f8 --- /dev/null +++ b/scripts/mock_data/supplementary_data_no_repeat.json @@ -0,0 +1,8 @@ +{ + "dataset_id": "c067f6de-6d64-42b1-8b02-431a3486c178", + "survey_id": "123", + "data": { + "schema_version": "v1", + "identifier": "12346789012A" + } +} diff --git a/scripts/mock_data/supplementary_data_with_repeat.json b/scripts/mock_data/supplementary_data_with_repeat.json new file mode 100644 index 0000000000..a5bc06bb3b --- /dev/null +++ b/scripts/mock_data/supplementary_data_with_repeat.json @@ -0,0 +1,85 @@ +{ + "dataset_id": "34a80231-c49a-44d0-91a6-8fe1fb190e64", + "survey_id": "123", + "data": { + "schema_version": "v1", + "identifier": "12346789012A", + "note": { + "title": "Volume of total production", + "description": "Figures should cover the total quantity of the goods produced during the period of the return" + }, + "items": { + "products": [ + { + "identifier": "89929001", + "name": "Articles and equipment for sports or outdoor games", + "cn_codes": "2504 + 250610 + 2512 + 2519 + 2524", + "guidance_include": { + "title": "Include", + "list": [ + "for children's playgrounds", + "swimming pools and paddling pools" + ] + }, + "guidance_exclude": { + "title": "Exclude", + "list": [ + "sports holdalls, gloves, clothing of textile materials, footwear, protective eyewear, rackets, balls, skates", + "for skiing, water sports, golf, fishing', for skiing, water sports, golf, fishing, table tennis, PE, gymnastics, athletics" + ] + }, + "value_sales": { + "answer_code": "89929001", + "label": "Value of sales" + }, + "volume_sales": { + "answer_code": "89929002", + "label": "Volume of sales", + "unit_label": "Tonnes" + }, + "total_volume": { + "answer_code": "89929005", + "label": "Total volume produced", + "unit_label": "Tonnes" + } + }, + { + "identifier": "201630601", + "name": "Other Minerals", + "cn_codes": "5908 + 5910 + 591110 + 591120 + 591140", + "guidance_include": { + "title": "Include", + "list": [ + "natural graphite", + "quartz for industrial use", + "diatomite; magnesia; feldspar", + "magnesite; natural magnesium carbonate", + "talc including steatite and chlorite", + "unexpanded vermiculite and perlite" + ] + }, + "guidance_exclude": { + "title": "Exclude", + "list": [ + "natural quartz sands" + ] + }, + "value_sales": { + "answer_code": "201630601", + "label": "Value of sales" + }, + "volume_sales": { + "answer_code": "201630602", + "label": "Volume of sales", + "unit_label": "Kilogram" + }, + "total_volume": { + "answer_code": "201630605", + "label": "Total volume produced", + "unit_label": "Kilogram" + } + } + ] + } + } +} diff --git a/scripts/mock_sds_endpoint.py b/scripts/mock_sds_endpoint.py new file mode 100644 index 0000000000..176a23e79a --- /dev/null +++ b/scripts/mock_sds_endpoint.py @@ -0,0 +1,26 @@ +import json + +from flask import Flask, Response, request + +app = Flask(__name__) + + +@app.route("/v1/unit_data") +def get_sds_data(): + dataset_id = request.args.get("dataset_id") + + if dataset_id == "c067f6de-6d64-42b1-8b02-431a3486c178": + return load_mock_data("scripts/mock_data/supplementary_data_no_repeat.json") + if dataset_id == "34a80231-c49a-44d0-91a6-8fe1fb190e64": + return load_mock_data("scripts/mock_data/supplementary_data_with_repeat.json") + + return Response(status=404) + + +def load_mock_data(filename): + with open(filename, encoding="utf-8") as mock_data_file: + return json.load(mock_data_file) + + +if __name__ == "__main__": + app.run(host="localhost", port=5003) diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 6c6a2cff45..0e84312ae1 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -1,10 +1,14 @@ # pylint: disable=redefined-outer-name from datetime import datetime, timedelta, timezone +from http.client import HTTPMessage import fakeredis import pytest from mock import MagicMock +from mock.mock import Mock +from requests.adapters import ConnectTimeoutError, ReadTimeoutError +from urllib3.connectionpool import HTTPConnectionPool, HTTPResponse from app.data_models import QuestionnaireStore from app.data_models.answer_store import AnswerStore @@ -189,3 +193,33 @@ def current_location(): @pytest.fixture def mock_autoescape_context(mocker): return mocker.Mock(autoescape=True) + + +@pytest.fixture +def mocked_response_content(mocker): + decodable_content = Mock() + decodable_content.decode.return_value = b"{}" + mocker.patch("requests.models.Response.content", decodable_content) + + +@pytest.fixture +def mocked_make_request_with_timeout( + mocker, mocked_response_content # pylint: disable=unused-argument +): + connect_timeout_error = ConnectTimeoutError("connect timed out") + read_timeout_error = ReadTimeoutError( + pool=None, message="read timed out", url="test-url" + ) + + response_not_timed_out = HTTPResponse(status=200, headers={}, msg=HTTPMessage()) + response_not_timed_out.drain_conn = Mock(return_value=None) + + return mocker.patch.object( + HTTPConnectionPool, + "_make_request", + side_effect=[ + connect_timeout_error, + read_timeout_error, + response_not_timed_out, + ], + ) diff --git a/tests/app/data_model/test_metadata_proxy.py b/tests/app/data_model/test_metadata_proxy.py index 79dce605c7..93c5cd603c 100644 --- a/tests/app/data_model/test_metadata_proxy.py +++ b/tests/app/data_model/test_metadata_proxy.py @@ -1,7 +1,7 @@ import pytest from werkzeug.datastructures import ImmutableDict -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models.metadata_proxy import MetadataProxy, SurveyMetadata METADATA_V1 = { diff --git a/tests/app/parser/conftest.py b/tests/app/parser/conftest.py index ab7bbc6e26..e1c13e6e85 100644 --- a/tests/app/parser/conftest.py +++ b/tests/app/parser/conftest.py @@ -4,7 +4,7 @@ import pytest -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion def get_metadata(version): diff --git a/tests/app/parser/test_metadata_parser.py b/tests/app/parser/test_metadata_parser.py index 74121bfc2c..10b44e1350 100644 --- a/tests/app/parser/test_metadata_parser.py +++ b/tests/app/parser/test_metadata_parser.py @@ -4,7 +4,7 @@ from freezegun import freeze_time from marshmallow import ValidationError -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.utilities.metadata_parser import validate_runner_claims from app.utilities.metadata_parser_v2 import ( validate_questionnaire_claims, diff --git a/tests/app/parser/test_supplementary_data_parser.py b/tests/app/parser/test_supplementary_data_parser.py new file mode 100644 index 0000000000..1a7f67ff8c --- /dev/null +++ b/tests/app/parser/test_supplementary_data_parser.py @@ -0,0 +1,208 @@ +from copy import deepcopy + +import pytest +from marshmallow import ValidationError + +from app.services.supplementary_data import validate_supplementary_data +from app.utilities.supplementary_data_parser import validate_supplementary_data_v1 + +SUPPLEMENTARY_DATA_PAYLOAD = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "data": { + "schema_version": "v1", + "identifier": "12346789012A", + "items": { + "local_units": [ + { + "identifier": "0001", + "lu_name": "TEST NAME. 1", + "lu_address": [ + "FIRST ADDRESS 1", + "FIRST ADDRESS 2", + "TOWN", + "COUNTY", + "POST CODE", + ], + }, + { + "identifier": "0002", + "lu_name": "TEST NAME 2", + "lu_address": [ + "SECOND ADDRESS 1", + "SECOND ADDRESS 1", + "TOWN", + "COUNTY", + "POSTCODE", + ], + }, + ] + }, + }, +} + + +def test_invalid_supplementary_data_payload_raises_error(): + with pytest.raises(ValidationError) as error: + validate_supplementary_data( + supplementary_data={}, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert str(error.value) == "Invalid supplementary data" + + +def test_validate_supplementary_data_payload(): + validated_payload = validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert validated_payload == SUPPLEMENTARY_DATA_PAYLOAD + + +def test_validate_supplementary_data_payload_incorrect_dataset_id(): + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="331507ca-1039-4624-a342-7cbc3630e217", + unit_id="12346789012A", + survey_id="123", + ) + + assert ( + str(error.value) + == "{'_schema': ['Supplementary data did not return the specified Dataset ID']}" + ) + + +def test_validate_supplementary_data_payload_incorrect_survey_id(): + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="234", + ) + + assert ( + str(error.value) + == "{'_schema': ['Supplementary data did not return the specified Survey ID']}" + ) + + +def test_validate_supplementary_data_payload_incorrect_unit_id(): + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="000000000001", + survey_id="123", + ) + + assert ( + str(error.value) + == "{'data': {'_schema': ['Supplementary data did not return the specified Unit ID']}}" + ) + + +def test_supplementary_data_payload_with_no_items_is_validated(): + payload = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "data": { + "schema_version": "v1", + "identifier": "12346789012A", + }, + } + + validated_payload = validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert validated_payload == payload + + +def test_validate_supplementary_data_payload_missing_survey_id(): + payload = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "data": { + "schema_version": "v1", + "identifier": "12346789012A", + }, + } + + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert str(error.value) == "{'survey_id': ['Missing data for required field.']}" + + +def test_validate_supplementary_data_payload_with_unknown_field(): + payload = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "some_field": "value", + "data": { + "schema_version": "v1", + "identifier": "12346789012A", + }, + } + + validated_payload = validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert validated_payload == payload + + +def test_validate_supplementary_data_invalid_schema_version(): + payload = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "some_field": "value", + "data": { + "schema_version": "v2", + "identifier": "12346789012A", + }, + } + + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="001", + unit_id="12346789012A", + survey_id="123", + ) + + assert str(error.value) == "{'data': {'schema_version': ['Must be one of: v1.']}}" + + +def test_validate_supplementary_data_payload_missing_identifier_in_items(): + payload = deepcopy(SUPPLEMENTARY_DATA_PAYLOAD) + payload["data"]["items"]["local_units"][0].pop("identifier") + + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert str(error.value) == "{'identifier': ['Missing data for required field.']}" diff --git a/tests/app/questionnaire/test_value_source_resolver.py b/tests/app/questionnaire/test_value_source_resolver.py index 57957aa7dd..6852f1d044 100644 --- a/tests/app/questionnaire/test_value_source_resolver.py +++ b/tests/app/questionnaire/test_value_source_resolver.py @@ -3,7 +3,7 @@ import pytest from mock import Mock -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models import AnswerStore, ListStore, ProgressStore from app.data_models.answer import Answer, AnswerDict from app.data_models.metadata_proxy import MetadataProxy, NoMetadataException diff --git a/tests/app/services/__init__.py b/tests/app/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/app/services/test_request_supplementary_data.py b/tests/app/services/test_request_supplementary_data.py new file mode 100644 index 0000000000..dc51abfa36 --- /dev/null +++ b/tests/app/services/test_request_supplementary_data.py @@ -0,0 +1,187 @@ +import pytest +import responses +from flask import Flask, current_app +from requests import RequestException + +from app.services.supplementary_data import ( + SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES, + SupplementaryDataRequestFailed, + get_supplementary_data, +) +from tests.app.utilities.test_schema import get_mocked_make_request + +TEST_SDS_URL = "http://test.domain/v1/unit_data" + +mock_supplementary_data_payload = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "data": { + "schema_version": "v1", + "identifier": "12346789012A", + "items": { + "local_units": [ + { + "identifier": "0001", + "lu_name": "TEST NAME. 1", + "lu_address": [ + "FIRST ADDRESS 1", + "FIRST ADDRESS 2", + "TOWN", + "COUNTY", + "POST CODE", + ], + }, + { + "identifier": "0002", + "lu_name": "TEST NAME 2", + "lu_address": [ + "SECOND ADDRESS 1", + "SECOND ADDRESS 1", + "TOWN", + "COUNTY", + "POSTCODE", + ], + }, + ] + }, + }, +} + + +@responses.activate +def test_get_supplementary_data_200(app: Flask): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + + responses.add( + responses.GET, + TEST_SDS_URL, + json=mock_supplementary_data_payload, + status=200, + ) + loaded_supplementary_data = get_supplementary_data( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert loaded_supplementary_data == mock_supplementary_data_payload + + +@pytest.mark.parametrize( + "status_code", + [401, 403, 404, 501, 511], +) +@responses.activate +def test_get_supplementary_data_non_200(app: Flask, status_code): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + + responses.add( + responses.GET, + TEST_SDS_URL, + json=mock_supplementary_data_payload, + status=status_code, + ) + + with pytest.raises(SupplementaryDataRequestFailed) as exc: + get_supplementary_data( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert str(exc.value) == "Supplementary Data request failed" + + +@responses.activate +def test_get_supplementary_data_request_failed(app: Flask): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + + responses.add(responses.GET, TEST_SDS_URL, body=RequestException()) + with pytest.raises(SupplementaryDataRequestFailed) as exc: + get_supplementary_data( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert str(exc.value) == "Supplementary Data request failed" + + +def test_get_supplementary_data_retries_timeout_error( + app: Flask, mocker, mocked_make_request_with_timeout +): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + mocker.patch( + "app.services.supplementary_data.validate_supplementary_data", + return_value=mock_supplementary_data_payload, + ) + + try: + supplementary_data = get_supplementary_data( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + except SupplementaryDataRequestFailed: + return pytest.fail("Supplementary data request unexpectedly failed") + + assert supplementary_data == mock_supplementary_data_payload + + expected_call = ( + SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES + 1 + ) # Max retries + the initial request + assert mocked_make_request_with_timeout.call_count == expected_call + + +@pytest.mark.usefixtures("mocked_response_content") +def test_get_supplementary_data_retries_transient_error(app: Flask, mocker): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + mocked_make_request = get_mocked_make_request( + mocker, status_codes=[500, 500, 200] + ) + + mocker.patch( + "app.services.supplementary_data.validate_supplementary_data", + return_value=mock_supplementary_data_payload, + ) + + try: + supplementary_data = get_supplementary_data( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + except SupplementaryDataRequestFailed: + return pytest.fail("Supplementary data request unexpectedly failed") + + assert supplementary_data == mock_supplementary_data_payload + + expected_call = ( + SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES + 1 + ) # Max retries + the initial request + + assert mocked_make_request.call_count == expected_call + + +def test_get_supplementary_data_max_retries(app: Flask, mocker): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + + mocked_make_request = get_mocked_make_request( + mocker, status_codes=[500, 500, 500, 500] + ) + + with pytest.raises(SupplementaryDataRequestFailed) as exc: + get_supplementary_data( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + unit_id="12346789012A", + survey_id="123", + ) + + assert str(exc.value) == "Supplementary Data request failed" + assert mocked_make_request.call_count == 3 diff --git a/tests/app/submitter/conftest.py b/tests/app/submitter/conftest.py index c92d9da656..08116ee25e 100644 --- a/tests/app/submitter/conftest.py +++ b/tests/app/submitter/conftest.py @@ -7,7 +7,7 @@ from mock import MagicMock from requests import Response -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models import QuestionnaireStore from app.data_models.answer import Answer from app.data_models.answer_store import AnswerStore diff --git a/tests/app/submitter/test_convert_payload_0_0_1.py b/tests/app/submitter/test_convert_payload_0_0_1.py index 6c14a3a4a2..52a1315376 100644 --- a/tests/app/submitter/test_convert_payload_0_0_1.py +++ b/tests/app/submitter/test_convert_payload_0_0_1.py @@ -2,7 +2,7 @@ import pytest -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models.answer import Answer from app.data_models.answer_store import AnswerStore from app.questionnaire.questionnaire_schema import QuestionnaireSchema diff --git a/tests/app/submitter/test_convert_payload_0_0_3.py b/tests/app/submitter/test_convert_payload_0_0_3.py index aadf9d8862..bb7ac7fddc 100644 --- a/tests/app/submitter/test_convert_payload_0_0_3.py +++ b/tests/app/submitter/test_convert_payload_0_0_3.py @@ -3,7 +3,7 @@ import pytest -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models.answer import Answer from app.data_models.answer_store import AnswerStore from app.data_models.list_store import ListStore diff --git a/tests/app/submitter/test_converter.py b/tests/app/submitter/test_converter.py index 6dde299be1..d9634a4593 100644 --- a/tests/app/submitter/test_converter.py +++ b/tests/app/submitter/test_converter.py @@ -2,7 +2,7 @@ import pytest -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.submitter.converter import convert_answers from app.submitter.converter_v2 import ( diff --git a/tests/app/utilities/test_schema.py b/tests/app/utilities/test_schema.py index c93689431a..2df35289c2 100644 --- a/tests/app/utilities/test_schema.py +++ b/tests/app/utilities/test_schema.py @@ -5,7 +5,6 @@ import responses from mock import Mock, patch from requests import RequestException -from requests.adapters import ConnectTimeoutError, ReadTimeoutError from urllib3.connectionpool import HTTPConnectionPool, HTTPResponse from app.questionnaire import QuestionnaireSchema @@ -250,13 +249,6 @@ def test_load_schema_from_metadata_with_schema_url_and_override_language_code(): assert loaded_schema.language_code == language_code -@pytest.fixture(name="mocked_response_content") -def mocked_response_content_fixture(mocker): - decodable_content = Mock() - decodable_content.decode.return_value = b"{}" - mocker.patch("requests.models.Response.content", decodable_content) - - def get_mocked_make_request(mocker, status_codes): mocked_responses = [] for status_code in status_codes: @@ -279,29 +271,6 @@ def get_mocked_make_request(mocker, status_codes): return patched_make_request -@pytest.fixture(name="mocked_make_request_with_timeout") -def mocked_make_request_with_timeout_fixture( - mocker, mocked_response_content # pylint: disable=unused-argument -): - connect_timeout_error = ConnectTimeoutError("connect timed out") - read_timeout_error = ReadTimeoutError( - pool=None, message="read timed out", url="test-url" - ) - - response_not_timed_out = HTTPResponse(status=200, headers={}, msg=HTTPMessage()) - response_not_timed_out.drain_conn = Mock(return_value=None) - - return mocker.patch.object( - HTTPConnectionPool, - "_make_request", - side_effect=[ - connect_timeout_error, - read_timeout_error, - response_not_timed_out, - ], - ) - - def test_load_schema_from_url_retries_timeout_error(mocked_make_request_with_timeout): load_schema_from_url.cache_clear() diff --git a/tests/app/views/handlers/conftest.py b/tests/app/views/handlers/conftest.py index 6fde601054..8ac724a725 100644 --- a/tests/app/views/handlers/conftest.py +++ b/tests/app/views/handlers/conftest.py @@ -5,7 +5,7 @@ from freezegun import freeze_time from mock import Mock -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models import QuestionnaireStore from app.data_models.metadata_proxy import MetadataProxy from app.data_models.session_data import SessionData diff --git a/tests/app/views/handlers/test_feedback_upload.py b/tests/app/views/handlers/test_feedback_upload.py index 3f4415d265..89904b4119 100644 --- a/tests/app/views/handlers/test_feedback_upload.py +++ b/tests/app/views/handlers/test_feedback_upload.py @@ -2,7 +2,7 @@ from freezegun import freeze_time -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE from app.views.handlers.feedback import ( FeedbackMetadata, diff --git a/tests/app/views/handlers/test_submission_handler.py b/tests/app/views/handlers/test_submission_handler.py index c4466ce2a3..6ce4431ffa 100644 --- a/tests/app/views/handlers/test_submission_handler.py +++ b/tests/app/views/handlers/test_submission_handler.py @@ -3,7 +3,7 @@ import pytest from freezegun import freeze_time -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models.session_store import SessionStore from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.utilities.schema import load_schema_from_name diff --git a/tests/integration/create_token.py b/tests/integration/create_token.py index c611ad9918..9dc97e2500 100644 --- a/tests/integration/create_token.py +++ b/tests/integration/create_token.py @@ -3,7 +3,7 @@ from sdc.crypto.encrypter import encrypt -from app.authentication.auth_payload_version import AuthPayloadVersion +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.keys import KEY_PURPOSE_AUTHENTICATION from tests.app.parser.conftest import get_response_expires_at @@ -51,6 +51,31 @@ "account_service_url": ACCOUNT_SERVICE_URL, } +PAYLOAD_V2_SUPPLEMENTARY_DATA = { + "version": AuthPayloadVersion.V2.value, + "survey_metadata": { + "data": { + "user_id": "integration-test", + "period_str": "April 2016", + "period_id": "201604", + "ru_ref": "123456789012A", + "ru_name": "Integration Testing", + "ref_p_start_date": "2016-04-01", + "ref_p_end_date": "2016-04-30", + "trad_as": "Integration Tests", + "employment_date": "1983-06-02", + "display_address": "68 Abingdon Road, Goathill", + "sds_dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + } + }, + "collection_exercise_sid": "789", + "response_id": "1234567890123456", + "language_code": "en", + "roles": [], + "account_service_url": ACCOUNT_SERVICE_URL, +} + PAYLOAD_V2_SOCIAL = { "version": AuthPayloadVersion.V2.value, "survey_metadata": { @@ -113,6 +138,15 @@ def create_token_v2(self, schema_name, theme="default", **extra_payload): return self.generate_token(payload) + def create_supplementary_data_token(self, schema_name, **extra_payload): + payload = PAYLOAD_V2_SUPPLEMENTARY_DATA + + payload = self._get_payload_with_params( + schema_name=schema_name, payload=payload, **extra_payload + ) + + return self.generate_token(payload) + def create_token_invalid_version(self, schema_name, **extra_payload): payload = self._get_payload_with_params( schema_name=schema_name, payload=PAYLOAD_V2_BUSINESS, **extra_payload diff --git a/tests/integration/integration_test_case.py b/tests/integration/integration_test_case.py index 3f1b6a7aa6..70f5f1cfe8 100644 --- a/tests/integration/integration_test_case.py +++ b/tests/integration/integration_test_case.py @@ -127,6 +127,19 @@ def launchSurvey(self, schema_name="test_dates", **payload_kwargs): self.get(f"/session?token={token}") + def launchSupplementaryDataSurvey( + self, schema_name="test_supplementary_data", **payload_kwargs + ): + """ + Launch a survey as an authenticated user and follow re-directs + :param schema_name: The name of the schema to load + """ + token = self.token_generator.create_supplementary_data_token( + schema_name=schema_name, **payload_kwargs + ) + + self.get(f"/session?token={token}") + def launchSurveyV2( self, theme="default", schema_name="test_dates", **payload_kwargs ): diff --git a/tests/integration/routes/test_session.py b/tests/integration/routes/test_session.py index 64379e468c..119cd7050e 100644 --- a/tests/integration/routes/test_session.py +++ b/tests/integration/routes/test_session.py @@ -2,8 +2,10 @@ from datetime import datetime, timedelta, timezone from freezegun import freeze_time +from mock.mock import patch from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE +from app.services.supplementary_data import SupplementaryDataRequestFailed from app.settings import ACCOUNT_SERVICE_BASE_URL, ACCOUNT_SERVICE_BASE_URL_SOCIAL from app.utilities.json import json_loads from tests.integration.integration_test_case import IntegrationTestCase @@ -88,6 +90,20 @@ def test_patch_session_expiry_extends_session(self): self.assertIn("expires_at", parsed_json) self.assertEqual(parsed_json["expires_at"], expected_expires_at) + def test_supplementary_data_is_loaded_when_sds_dataset_id_in_metadata(self): + with patch("app.routes.session.get_supplementary_data", return_value={}): + self.launchSupplementaryDataSurvey() + self.assertStatusOK() + + def test_supplementary_data_raises_500_error_on_exception(self): + with patch( + "app.routes.session.get_supplementary_data", + side_effect=SupplementaryDataRequestFailed, + ): + self.launchSupplementaryDataSurvey() + self.assertStatusCode(500) + self.assertInBody("Sorry, there is a problem with this service") + class TestCensusSession(IntegrationTestCase): def setUp(self):