mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-23 15:56:45 -04:00
5
.coveragerc
Normal file
5
.coveragerc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
pragma: no cover
|
||||||
|
^\s*logger\.
|
||||||
|
^\s*current_app\.logger\.
|
||||||
4
Makefile
4
Makefile
@@ -104,8 +104,8 @@ test: ## Run tests and create coverage report
|
|||||||
poetry run isort ./app ./tests
|
poetry run isort ./app ./tests
|
||||||
poetry run coverage run --omit=*/migrations/*,*/tests/* -m pytest --maxfail=10
|
poetry run coverage run --omit=*/migrations/*,*/tests/* -m pytest --maxfail=10
|
||||||
|
|
||||||
## TODO set this back to 95 asap
|
|
||||||
poetry run coverage report -m --fail-under=94
|
poetry run coverage report -m --fail-under=95
|
||||||
poetry run coverage html -d .coverage_cache
|
poetry run coverage html -d .coverage_cache
|
||||||
|
|
||||||
.PHONY: test-debug
|
.PHONY: test-debug
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class AwsPinpointClient(Client):
|
|||||||
# TODO right now this will only print with AWS simulated numbers,
|
# TODO right now this will only print with AWS simulated numbers,
|
||||||
# but remove this when that changes
|
# but remove this when that changes
|
||||||
current_app.logger.info(hilite(response))
|
current_app.logger.info(hilite(response))
|
||||||
|
return response
|
||||||
except ClientError:
|
except ClientError:
|
||||||
current_app.logger.exception(
|
current_app.logger.exception(
|
||||||
"#notify-debug-validate-phone-number Could not validate with pinpoint"
|
"#notify-debug-validate-phone-number Could not validate with pinpoint"
|
||||||
|
|||||||
@@ -752,12 +752,13 @@ def create_admin_jwt():
|
|||||||
@notify_command(name="create-user-jwt")
|
@notify_command(name="create-user-jwt")
|
||||||
@click.option("-t", "--token", required=True, prompt=False)
|
@click.option("-t", "--token", required=True, prompt=False)
|
||||||
def create_user_jwt(token):
|
def create_user_jwt(token):
|
||||||
if getenv("NOTIFY_ENVIRONMENT", "") != "development":
|
if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]:
|
||||||
current_app.logger.error("Can only be run in development")
|
current_app.logger.error("Can only be run in development")
|
||||||
return
|
return
|
||||||
service_id = token[-73:-37]
|
service_id = token[-73:-37]
|
||||||
api_key = token[-36:]
|
api_key = token[-36:]
|
||||||
current_app.logger.info(create_jwt_token(api_key, service_id))
|
token = create_jwt_token(api_key, service_id)
|
||||||
|
current_app.logger.info(token)
|
||||||
|
|
||||||
|
|
||||||
def _update_template(id, name, template_type, content, subject):
|
def _update_template(id, name, template_type, content, subject):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from unittest.mock import MagicMock, Mock, call
|
from unittest.mock import MagicMock, Mock, call, patch
|
||||||
|
|
||||||
import botocore
|
import botocore
|
||||||
import pytest
|
import pytest
|
||||||
@@ -13,6 +13,7 @@ from app.aws import s3
|
|||||||
from app.aws.s3 import (
|
from app.aws.s3 import (
|
||||||
cleanup_old_s3_objects,
|
cleanup_old_s3_objects,
|
||||||
download_from_s3,
|
download_from_s3,
|
||||||
|
extract_phones,
|
||||||
file_exists,
|
file_exists,
|
||||||
get_job_and_metadata_from_s3,
|
get_job_and_metadata_from_s3,
|
||||||
get_job_from_s3,
|
get_job_from_s3,
|
||||||
@@ -650,3 +651,33 @@ def test_read_s3_file_populates_cache(monkeypatch):
|
|||||||
assert job_cache.get("66")[0].startswith("Phone number")
|
assert job_cache.get("66")[0].startswith("Phone number")
|
||||||
assert job_cache.get("66_phones")[0] == {"0", "15551234"}
|
assert job_cache.get("66_phones")[0] == {"0", "15551234"}
|
||||||
assert job_cache.get("66_personalisation")[0] == {0: {"Name": "Alice"}}
|
assert job_cache.get("66_personalisation")[0] == {0: {"Name": "Alice"}}
|
||||||
|
|
||||||
|
|
||||||
|
@patch("app.aws.s3.current_app")
|
||||||
|
def test_valid_csv(mock_app):
|
||||||
|
csv_data = "Name,Phone Number\nAlice,+1 (555) 555-5555\nBob,555.555.1111"
|
||||||
|
result = extract_phones(csv_data, "service1", "job1")
|
||||||
|
expected = {0: "15555555555", 1: "5555551111"}
|
||||||
|
assert result == expected
|
||||||
|
mock_app.logger.error.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("app.aws.s3.current_app")
|
||||||
|
def test_missing_phone_column(mock_app):
|
||||||
|
|
||||||
|
csv_data = "Name,Phone Number\nAlice,\nBob"
|
||||||
|
result = extract_phones(csv_data, "service1", "job1")
|
||||||
|
assert result == {0: "", 1: "Unavailable"}
|
||||||
|
mock_app.logger.error.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("app.aws.s3.current_app")
|
||||||
|
def test_test_with_bom_header(mock_app):
|
||||||
|
csv_data = "\ufeffName,Phone Number\nAlice,1-555-555-5555"
|
||||||
|
result = extract_phones(csv_data, "service2", "job2")
|
||||||
|
expected = {0: "15555555555"}
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_valid_csv()
|
||||||
|
|||||||
37
tests/app/clients/test_aws_pinpoint.py
Normal file
37
tests/app/clients/test_aws_pinpoint.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from aiohttp import ClientError
|
||||||
|
|
||||||
|
from app.clients.pinpoint.aws_pinpoint import AwsPinpointClient
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_phone_number_success():
|
||||||
|
phone = "+1234567890"
|
||||||
|
mock_response = {
|
||||||
|
"NumberValidateResponse": {
|
||||||
|
"PhoneType": "MOBILE",
|
||||||
|
"CleansedPhoneNumberE164": phone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client_instance = AwsPinpointClient()
|
||||||
|
client_instance._client = MagicMock()
|
||||||
|
client_instance._client.phone_number_validate.return_value = mock_response
|
||||||
|
|
||||||
|
result = client_instance.validate_phone_number("US", phone)
|
||||||
|
assert result is not None
|
||||||
|
client_instance._client.phone_number_validate.assert_called_once_with(
|
||||||
|
NumberValidateRequest={"IsoCountryCode": "US", "PhoneNumber": phone}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_phone_number_client_error():
|
||||||
|
client_instance = AwsPinpointClient()
|
||||||
|
client_instance._client = MagicMock()
|
||||||
|
client_instance._client.phone_number_validate.side_effect = ClientError(
|
||||||
|
{"Error": {"Code": "BadRequest1", "MEssage": "Invalid phone"}},
|
||||||
|
"phone number validate",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ClientError):
|
||||||
|
client_instance.validate_phone_number("US", "bad-number")
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import MagicMock, mock_open
|
from unittest.mock import MagicMock, mock_open, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
@@ -11,13 +12,16 @@ from app.commands import (
|
|||||||
_update_template,
|
_update_template,
|
||||||
associate_services_to_organizations,
|
associate_services_to_organizations,
|
||||||
bulk_invite_user_to_service,
|
bulk_invite_user_to_service,
|
||||||
|
clear_redis_list,
|
||||||
create_new_service,
|
create_new_service,
|
||||||
create_test_user,
|
create_test_user,
|
||||||
|
create_user_jwt,
|
||||||
download_csv_file_by_name,
|
download_csv_file_by_name,
|
||||||
dump_sms_senders,
|
dump_sms_senders,
|
||||||
dump_user_info,
|
dump_user_info,
|
||||||
fix_billable_units,
|
fix_billable_units,
|
||||||
generate_salt,
|
generate_salt,
|
||||||
|
get_service_sender_phones,
|
||||||
insert_inbound_numbers_from_file,
|
insert_inbound_numbers_from_file,
|
||||||
populate_annual_billing_with_defaults,
|
populate_annual_billing_with_defaults,
|
||||||
populate_annual_billing_with_the_previous_years_allowance,
|
populate_annual_billing_with_the_previous_years_allowance,
|
||||||
@@ -26,8 +30,10 @@ from app.commands import (
|
|||||||
populate_organizations_from_file,
|
populate_organizations_from_file,
|
||||||
process_row_from_job,
|
process_row_from_job,
|
||||||
promote_user_to_platform_admin,
|
promote_user_to_platform_admin,
|
||||||
|
purge_csv_bucket,
|
||||||
purge_functional_test_data,
|
purge_functional_test_data,
|
||||||
update_jobs_archived_flag,
|
update_jobs_archived_flag,
|
||||||
|
update_templates,
|
||||||
)
|
)
|
||||||
from app.dao.inbound_numbers_dao import dao_get_available_inbound_numbers
|
from app.dao.inbound_numbers_dao import dao_get_available_inbound_numbers
|
||||||
from app.dao.users_dao import get_user_by_email
|
from app.dao.users_dao import get_user_by_email
|
||||||
@@ -732,3 +738,113 @@ def test_clear_templates_from_cache(mocker):
|
|||||||
[mocker.call(p) for p in expected_patterns], any_order=True
|
[mocker.call(p) for p in expected_patterns], any_order=True
|
||||||
)
|
)
|
||||||
mock_logger.info.assert_called_once_with("Number of templates deleted from cache 9")
|
mock_logger.info.assert_called_once_with("Number of templates deleted from cache 9")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("app.commands.s3.purge_bucket")
|
||||||
|
@patch("app.commands.current_app")
|
||||||
|
def test_purge_csv_bucket(mock_current_app, mock_purge_bucket, notify_api):
|
||||||
|
mock_current_app.config = {
|
||||||
|
"CSV_UPLOAD_BUCKET": {
|
||||||
|
"bucket": "test-bucket",
|
||||||
|
"access_key_id": "FAKE_ACCESS_KEY",
|
||||||
|
"secret_access_key": "FAKE_SECRET_KEY", # pragma: allowlist secret
|
||||||
|
"region": "us-north-1",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runner = notify_api.test_cli_runner()
|
||||||
|
runner.invoke(purge_csv_bucket)
|
||||||
|
|
||||||
|
mock_purge_bucket.assert_called_once_with(
|
||||||
|
"test-bucket", "FAKE_ACCESS_KEY", "FAKE_SECRET_KEY", "us-north-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("app.commands.db.session.execute")
|
||||||
|
def test_get_service_sender_phones(mock_execute, notify_api):
|
||||||
|
runner = notify_api.test_cli_runner()
|
||||||
|
result = runner.invoke(get_service_sender_phones, ["-s", "service-id"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_execute.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("app.commands.current_app.logger.info")
|
||||||
|
def test_create_user_jwt(mock_logger, notify_api, sample_api_key, sample_service):
|
||||||
|
token = f"{sample_service.id}{sample_api_key}"
|
||||||
|
runner = notify_api.test_cli_runner()
|
||||||
|
result = runner.invoke(create_user_jwt, ["-t", token])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_logger.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_templates_calls_update_and_clear(monkeypatch, notify_api):
|
||||||
|
templates_data = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Template1",
|
||||||
|
"type": "email",
|
||||||
|
"content": "Hello",
|
||||||
|
"subject": "Subject1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Template2",
|
||||||
|
"type": "sms",
|
||||||
|
"content": "Hi",
|
||||||
|
"subject": "Subject2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
m = mock_open(read_data=json.dumps(templates_data))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.commands.current_app",
|
||||||
|
type("obj", (), {"config": {"CONFIG_FILES": "/fake/path"}}),
|
||||||
|
)
|
||||||
|
with patch("app.commands.open", m):
|
||||||
|
with patch("app.commands._update_template") as mock_update_template, patch(
|
||||||
|
"app.commands._clear_templates_from_cache"
|
||||||
|
) as mock_clear_cache:
|
||||||
|
|
||||||
|
runner = notify_api.test_cli_runner()
|
||||||
|
result = runner.invoke(update_templates)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
expected_calls = [
|
||||||
|
(1, "Template1", "email", "Hello", "Subject1"),
|
||||||
|
(2, "Template2", "sms", "Hi", "Subject2"),
|
||||||
|
]
|
||||||
|
actual_calls = [call.args for call in mock_update_template.call_args_list]
|
||||||
|
assert actual_calls == expected_calls
|
||||||
|
mock_clear_cache.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_redis_list(monkeypatch, notify_api):
|
||||||
|
mock_redis_store = type("RedisStoreMock", (), {})()
|
||||||
|
monkeypatch.setattr("app.commands.redis_store", mock_redis_store)
|
||||||
|
mock_redis_store.llen = lambda list_name: {"before_list": 5, "after_list": 0}[
|
||||||
|
list_name
|
||||||
|
]
|
||||||
|
ltrim_calls = []
|
||||||
|
|
||||||
|
def fake_ltrim(name, start, end):
|
||||||
|
ltrim_calls.append((name, start, end))
|
||||||
|
|
||||||
|
mock_redis_store.ltrim = fake_ltrim
|
||||||
|
logger_info_calls = []
|
||||||
|
mock_logger = type(
|
||||||
|
"LoggerMock", (), {"info": lambda self, msg: logger_info_calls.append(msg)}
|
||||||
|
)()
|
||||||
|
mock_app = type("FlaskAppMock", (), {"logger": mock_logger})
|
||||||
|
monkeypatch.setattr("app.commands.current_app", mock_app)
|
||||||
|
call_count = {"count": 0}
|
||||||
|
|
||||||
|
def llen_side_effect(name):
|
||||||
|
if call_count["count"] == 0:
|
||||||
|
call_count["count"] += 1
|
||||||
|
return 5
|
||||||
|
return 0
|
||||||
|
|
||||||
|
mock_redis_store.llen = llen_side_effect
|
||||||
|
runner = notify_api.test_cli_runner()
|
||||||
|
result = runner.invoke(clear_redis_list, ["-n", "test_list"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert ltrim_calls == [("test_list", 1, 0)]
|
||||||
|
assert logger_info_calls == ["Cleared redis list test_list. Before: 5 after 0"]
|
||||||
|
|||||||
80
tests/app/test_history_meta.py
Normal file
80
tests/app/test_history_meta.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from app.history_meta import (
|
||||||
|
_get_bases_for_versioned_class,
|
||||||
|
_handle_single_table_inheritance,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_bases_with_super_history_and_table():
|
||||||
|
mock_super_mapper = MagicMock()
|
||||||
|
mock_super_mapper.class_ = object
|
||||||
|
mock_super_mapper.attrs.changed.columns = ["col1", "col2"]
|
||||||
|
mock_table = MagicMock()
|
||||||
|
mock_table.c.changed = "changed_col"
|
||||||
|
|
||||||
|
properties = {}
|
||||||
|
bases = _get_bases_for_versioned_class(
|
||||||
|
super_history_mapper=mock_super_mapper,
|
||||||
|
table=mock_table,
|
||||||
|
properties=properties,
|
||||||
|
local_mapper=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert bases == (object,)
|
||||||
|
assert properties["changed"] == ("changed_col", "col1", "col2")
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_bases_with_super_history_and_no_table():
|
||||||
|
mock_super_mapper = MagicMock()
|
||||||
|
mock_super_mapper.class_ = int
|
||||||
|
properties = {}
|
||||||
|
bases = _get_bases_for_versioned_class(
|
||||||
|
super_history_mapper=mock_super_mapper,
|
||||||
|
table=None,
|
||||||
|
properties=properties,
|
||||||
|
local_mapper=None,
|
||||||
|
)
|
||||||
|
assert bases == (int,)
|
||||||
|
assert "changed" not in properties
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_bases_without_super_history():
|
||||||
|
class Base:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class_ = type("Dummy", (Base,), {})
|
||||||
|
local_mapper = MagicMock()
|
||||||
|
local_mapper.base_mapper.class_ = class_
|
||||||
|
|
||||||
|
properties = {}
|
||||||
|
|
||||||
|
bases = _get_bases_for_versioned_class(
|
||||||
|
super_history_mapper=None,
|
||||||
|
table=None,
|
||||||
|
properties=properties,
|
||||||
|
local_mapper=local_mapper,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert bases == (Base,)
|
||||||
|
assert "changed" not in properties
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_single_table_inheritance():
|
||||||
|
col1 = MagicMock()
|
||||||
|
col1.key = "id"
|
||||||
|
col1.name = "name_col1"
|
||||||
|
col2 = MagicMock()
|
||||||
|
col2.key = "new_column"
|
||||||
|
col2.name = "name_col2"
|
||||||
|
|
||||||
|
local_mapper = MagicMock()
|
||||||
|
local_mapper.local_table.c = [col1, col2]
|
||||||
|
|
||||||
|
super_history_mapper = MagicMock()
|
||||||
|
super_history_mapper.local_table.c = {"id": col1}
|
||||||
|
super_history_mapper.local_table.append_column = col2
|
||||||
|
|
||||||
|
_handle_single_table_inheritance(local_mapper, super_history_mapper)
|
||||||
|
|
||||||
|
super_history_mapper.local_table.append_column.assert_called_once()
|
||||||
Reference in New Issue
Block a user