import uuid import pytest from flask import json from freezegun import freeze_time from jsonschema import ValidationError from app.models import EMAIL_TYPE, NOTIFICATION_CREATED from app.schema_validation import validate from app.v2.notifications.notification_schemas import get_notifications_request from app.v2.notifications.notification_schemas import ( post_email_request as post_email_request_schema, ) from app.v2.notifications.notification_schemas import ( post_sms_request as post_sms_request_schema, ) valid_get_json = {} valid_get_with_optionals_json = { "reference": "test reference", "status": [NOTIFICATION_CREATED], "template_type": [EMAIL_TYPE], "include_jobs": "true", "older_than": "a5149c32-f03b-4711-af49-ad6993797d45", } @pytest.mark.parametrize("input", [valid_get_json, valid_get_with_optionals_json]) def test_get_notifications_valid_json(input): assert validate(input, get_notifications_request) == input @pytest.mark.parametrize( "invalid_statuses, valid_statuses", [ # one invalid status (["elephant"], []), # multiple invalid statuses (["elephant", "giraffe", "cheetah"], []), # one bad status and one good status (["elephant"], ["created"]), ], ) def test_get_notifications_request_invalid_statuses(invalid_statuses, valid_statuses): partial_error_status = ( "is not one of " "[cancelled, created, sending, sent, delivered, pending, failed, " "technical-failure, temporary-failure, permanent-failure, pending-virus-check, " "validation-failed, virus-scan-failed]" ) with pytest.raises(ValidationError) as e: validate( {"status": invalid_statuses + valid_statuses}, get_notifications_request ) errors = json.loads(str(e.value)).get("errors") assert len(errors) == len(invalid_statuses) for index, value in enumerate(invalid_statuses): assert errors[index]["message"] == "status {} {}".format( value, partial_error_status ) @pytest.mark.parametrize( "invalid_template_types, valid_template_types", [ # one invalid template_type (["orange"], []), # multiple invalid template_types (["orange", "avocado", "banana"], []), # one bad template_type and one good template_type (["orange"], ["sms"]), ], ) def test_get_notifications_request_invalid_template_types( invalid_template_types, valid_template_types ): partial_error_template_type = "is not one of [sms, email]" with pytest.raises(ValidationError) as e: validate( {"template_type": invalid_template_types + valid_template_types}, get_notifications_request, ) errors = json.loads(str(e.value)).get("errors") assert len(errors) == len(invalid_template_types) for index, value in enumerate(invalid_template_types): assert errors[index]["message"] == "template_type {} {}".format( value, partial_error_template_type ) def test_get_notifications_request_invalid_statuses_and_template_types(): with pytest.raises(ValidationError) as e: validate( { "status": ["created", "elephant", "giraffe"], "template_type": ["sms", "orange", "avocado"], }, get_notifications_request, ) errors = json.loads(str(e.value)).get("errors") assert len(errors) == 4 error_messages = [error["message"] for error in errors] for invalid_status in ["elephant", "giraffe"]: assert ( "status {} is not one of [cancelled, created, sending, sent, delivered, " "pending, failed, technical-failure, temporary-failure, permanent-failure, " "pending-virus-check, validation-failed, virus-scan-failed]".format( invalid_status ) in error_messages ) for invalid_template_type in ["orange", "avocado"]: assert ( "template_type {} is not one of [sms, email]".format(invalid_template_type) in error_messages ) valid_json = {"phone_number": "2028675309", "template_id": str(uuid.uuid4())} valid_json_with_optionals = { "phone_number": "2028675309", "template_id": str(uuid.uuid4()), "reference": "reference from caller", "personalisation": {"key": "value"}, } @pytest.mark.parametrize("input", [valid_json, valid_json_with_optionals]) def test_post_sms_schema_is_valid(input): assert validate(input, post_sms_request_schema) == input @pytest.mark.parametrize( "template_id", [ "2ebe4da8-17be-49fe-b02f-dff2760261a0" + "\n", "2ebe4da8-17be-49fe-b02f-dff2760261a0" + " ", "2ebe4da8-17be-49fe-b02f-dff2760261a0" + "\r", "\t" + "2ebe4da8-17be-49fe-b02f-dff2760261a0", "2ebe4da8-17be-49fe-b02f-dff2760261a0"[4:], "bad_uuid", ], ) def test_post_sms_json_schema_bad_uuid(template_id): j = {"template_id": template_id, "phone_number": "2028675309"} with pytest.raises(ValidationError) as e: validate(j, post_sms_request_schema) error = json.loads(str(e.value)) assert len(error.keys()) == 2 assert error.get("status_code") == 400 assert len(error.get("errors")) == 1 assert { "error": "ValidationError", "message": "template_id is not a valid UUID", } in error["errors"] def test_post_sms_json_schema_bad_uuid_and_missing_phone_number(): j = {"template_id": "notUUID"} with pytest.raises(ValidationError) as e: validate(j, post_sms_request_schema) error = json.loads(str(e.value)) assert len(error.keys()) == 2 assert error.get("status_code") == 400 assert len(error.get("errors")) == 2 assert { "error": "ValidationError", "message": "phone_number is a required property", } in error["errors"] assert { "error": "ValidationError", "message": "template_id is not a valid UUID", } in error["errors"] def test_post_sms_schema_with_personalisation_that_is_not_a_dict(): j = { "phone_number": "2028675309", "template_id": str(uuid.uuid4()), "reference": "reference from caller", "personalisation": "not_a_dict", } with pytest.raises(ValidationError) as e: validate(j, post_sms_request_schema) error = json.loads(str(e.value)) assert len(error.get("errors")) == 1 assert error["errors"] == [ { "error": "ValidationError", "message": "personalisation not_a_dict is not of type object", } ] assert error.get("status_code") == 400 assert len(error.keys()) == 2 @pytest.mark.parametrize( "invalid_phone_number, err_msg", [ ("08515111111", "phone_number Phone number is not possible"), ("07515111*11", "phone_number Not enough digits"), ( "notaphoneumber", "phone_number The string supplied did not seem to be a phone number.", ), (7700900001, "phone_number 7700900001 is not of type string"), (None, "phone_number None is not of type string"), ([], "phone_number [] is not of type string"), ({}, "phone_number {} is not of type string"), ], ) def test_post_sms_request_schema_invalid_phone_number(invalid_phone_number, err_msg): j = {"phone_number": invalid_phone_number, "template_id": str(uuid.uuid4())} with pytest.raises(ValidationError) as e: validate(j, post_sms_request_schema) errors = json.loads(str(e.value)).get("errors") assert len(errors) == 1 assert {"error": "ValidationError", "message": err_msg} == errors[0] def test_post_sms_request_schema_invalid_phone_number_and_missing_template(): j = { "phone_number": "5558675309", } with pytest.raises(ValidationError) as e: validate(j, post_sms_request_schema) errors = json.loads(str(e.value)).get("errors") assert len(errors) == 2 assert { "error": "ValidationError", "message": "phone_number Phone number range is not in use", } in errors assert { "error": "ValidationError", "message": "template_id is a required property", } in errors valid_post_email_json = { "email_address": "test@example.gov.uk", "template_id": str(uuid.uuid4()), } valid_post_email_json_with_optionals = { "email_address": "test@example.gov.uk", "template_id": str(uuid.uuid4()), "reference": "reference from caller", "personalisation": {"key": "value"}, } @pytest.mark.parametrize( "input", [valid_post_email_json, valid_post_email_json_with_optionals] ) def test_post_email_schema_is_valid(input): assert validate(input, post_email_request_schema) == input def test_post_email_schema_bad_uuid_and_missing_email_address(): j = {"template_id": "bad_template"} with pytest.raises(ValidationError): validate(j, post_email_request_schema) @pytest.mark.parametrize( "email_address, err_msg", [ ("example", "email_address Not a valid email address"), (12345, "email_address 12345 is not of type string"), ("with(brackets)@example.com", "email_address Not a valid email address"), (None, "email_address None is not of type string"), ([], "email_address [] is not of type string"), ({}, "email_address {} is not of type string"), ], ) def test_post_email_schema_invalid_email_address(email_address, err_msg): j = {"template_id": str(uuid.uuid4()), "email_address": email_address} with pytest.raises(ValidationError) as e: validate(j, post_email_request_schema) errors = json.loads(str(e.value)).get("errors") assert len(errors) == 1 assert {"error": "ValidationError", "message": err_msg} == errors[0] def valid_email_response(): return { "id": str(uuid.uuid4()), "content": { "body": "the body of the message", "subject": "subject of the message", "from_email": "service@dig.gov.uk", }, "uri": "http://notify.api/v2/notifications/id", "template": { "id": str(uuid.uuid4()), "version": 1, "uri": "http://notify.api/v2/template/id", }, "scheduled_for": "", } @pytest.mark.parametrize("schema", [post_email_request_schema, post_sms_request_schema]) @freeze_time("2017-05-12 13:00:00") def test_post_schema_valid_scheduled_for(schema): j = {"template_id": str(uuid.uuid4()), "scheduled_for": "2017-05-12 13:15"} if schema == post_email_request_schema: j.update({"email_address": "joe@gmail.com"}) else: j.update({"phone_number": "2028675309"}) assert validate(j, schema) == j @pytest.mark.parametrize( "invalid_datetime", ["13:00:00 2017-01-01", "2017-31-12 13:00:00", "01-01-2017T14:00:00.0000Z"], ) @pytest.mark.parametrize("schema", [post_email_request_schema, post_sms_request_schema]) def test_post_email_schema_invalid_scheduled_for(invalid_datetime, schema): j = {"template_id": str(uuid.uuid4()), "scheduled_for": invalid_datetime} if schema == post_email_request_schema: j.update({"email_address": "joe@gmail.com"}) else: j.update({"phone_number": "2028675309"}) with pytest.raises(ValidationError) as e: validate(j, schema) error = json.loads(str(e.value)) assert error["status_code"] == 400 assert error["errors"] == [ { "error": "ValidationError", "message": "scheduled_for datetime format is invalid. " "It must be a valid ISO8601 date time format, " "https://en.wikipedia.org/wiki/ISO_8601", } ] @freeze_time("2017-05-12 13:00:00") def test_scheduled_for_raises_validation_error_when_in_the_past(): j = { "phone_number": "2028675309", "template_id": str(uuid.uuid4()), "scheduled_for": "2017-05-12 10:00", } with pytest.raises(ValidationError) as e: validate(j, post_sms_request_schema) error = json.loads(str(e.value)) assert error["status_code"] == 400 assert error["errors"] == [ { "error": "ValidationError", "message": "scheduled_for datetime can not be in the past", } ] @freeze_time("2017-05-12 13:00:00") def test_scheduled_for_raises_validation_error_when_more_than_24_hours_in_the_future(): j = { "phone_number": "2028675309", "template_id": str(uuid.uuid4()), "scheduled_for": "2017-05-13 14:00", } with pytest.raises(ValidationError) as e: validate(j, post_sms_request_schema) error = json.loads(str(e.value)) assert error["status_code"] == 400 assert error["errors"] == [ { "error": "ValidationError", "message": "scheduled_for datetime can only be 24 hours in the future", } ]