mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-07-31 03:39:19 -04:00
Localize notification_utils to the admin
This changeset pulls in all of the notification_utils code directly into the admin and removes it as an external dependency. We are doing this to cut down on operational maintenance of the project and will begin removing parts of it no longer needed for the admin. Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import io
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from notifications_utils.clients.antivirus.antivirus_client import (
|
||||
AntivirusClient,
|
||||
AntivirusError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def antivirus(app, mocker):
|
||||
client = AntivirusClient()
|
||||
app.config["ANTIVIRUS_API_HOST"] = "https://antivirus"
|
||||
app.config["ANTIVIRUS_API_KEY"] = "test-antivirus-key"
|
||||
client.init_app(app)
|
||||
return client
|
||||
|
||||
|
||||
def test_scan_document(antivirus, rmock):
|
||||
document = io.BytesIO(b"filecontents")
|
||||
rmock.request(
|
||||
"POST",
|
||||
"https://antivirus/scan",
|
||||
json={"ok": True},
|
||||
request_headers={
|
||||
"Authorization": "Bearer test-antivirus-key",
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
resp = antivirus.scan(document)
|
||||
|
||||
assert resp
|
||||
assert "filecontents" in rmock.last_request.text
|
||||
assert document.tell() == 0
|
||||
|
||||
|
||||
def test_should_raise_for_status(antivirus, rmock):
|
||||
with pytest.raises(AntivirusError) as excinfo:
|
||||
rmock.request(
|
||||
"POST",
|
||||
"https://antivirus/scan",
|
||||
json={"error": "Antivirus error"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
antivirus.scan(io.BytesIO(b"document"))
|
||||
|
||||
assert excinfo.value.message == "Antivirus error"
|
||||
assert excinfo.value.status_code == 400
|
||||
|
||||
|
||||
def test_should_raise_for_connection_errors(antivirus, rmock):
|
||||
with pytest.raises(AntivirusError) as excinfo:
|
||||
rmock.request(
|
||||
"POST", "https://antivirus/scan", exc=requests.exceptions.ConnectTimeout
|
||||
)
|
||||
antivirus.scan(io.BytesIO(b"document"))
|
||||
|
||||
assert excinfo.value.message == "connection error"
|
||||
assert excinfo.value.status_code == 503
|
||||
@@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
|
||||
from notifications_utils.clients.encryption.encryption_client import (
|
||||
Encryption,
|
||||
EncryptionError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def encryption_client(app):
|
||||
client = Encryption()
|
||||
|
||||
app.config["SECRET_KEY"] = "test-notify-secret-key"
|
||||
app.config["DANGEROUS_SALT"] = "test-notify-salt"
|
||||
|
||||
client.init_app(app)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def test_should_ensure_shared_salt_security(app):
|
||||
client = Encryption()
|
||||
app.config["SECRET_KEY"] = "test-notify-secret-key"
|
||||
app.config["DANGEROUS_SALT"] = "too-short"
|
||||
with pytest.raises(EncryptionError):
|
||||
client.init_app(app)
|
||||
|
||||
|
||||
def test_should_ensure_custom_salt_security(encryption_client):
|
||||
with pytest.raises(EncryptionError):
|
||||
encryption_client.encrypt("this", salt="too-short")
|
||||
|
||||
|
||||
def test_should_encrypt_strings(encryption_client):
|
||||
encrypted = encryption_client.encrypt("this")
|
||||
assert encrypted != "this"
|
||||
assert isinstance(encrypted, str)
|
||||
|
||||
|
||||
def test_should_encrypt_dicts(encryption_client):
|
||||
to_encrypt = {"hello": "world"}
|
||||
encrypted = encryption_client.encrypt(to_encrypt)
|
||||
assert encrypted != to_encrypt
|
||||
assert encryption_client.decrypt(encrypted) == to_encrypt
|
||||
|
||||
|
||||
def test_encryption_is_nondeterministic(encryption_client):
|
||||
first_run = encryption_client.encrypt("this")
|
||||
second_run = encryption_client.encrypt("this")
|
||||
assert first_run != second_run
|
||||
|
||||
|
||||
def test_should_decrypt_content(encryption_client):
|
||||
encrypted = encryption_client.encrypt("this")
|
||||
assert encryption_client.decrypt(encrypted) == "this"
|
||||
|
||||
|
||||
def test_should_decrypt_content_with_custom_salt(encryption_client):
|
||||
salt = "different-salt-value"
|
||||
encrypted = encryption_client.encrypt("this", salt=salt)
|
||||
assert encryption_client.decrypt(encrypted, salt=salt) == "this"
|
||||
|
||||
|
||||
def test_should_verify_decryption(encryption_client):
|
||||
encrypted = encryption_client.encrypt("this")
|
||||
with pytest.raises(EncryptionError):
|
||||
encryption_client.decrypt(encrypted, salt="different-salt-value")
|
||||
|
||||
|
||||
def test_should_sign_and_serialize_string(encryption_client):
|
||||
signed = encryption_client.sign("this")
|
||||
assert signed != "this"
|
||||
|
||||
|
||||
def test_should_verify_signature_and_deserialize_string(encryption_client):
|
||||
signed = encryption_client.sign("this")
|
||||
assert encryption_client.verify_signature(signed) == "this"
|
||||
|
||||
|
||||
def test_should_raise_encryption_error_on_bad_salt(encryption_client):
|
||||
signed = encryption_client.sign("this")
|
||||
with pytest.raises(EncryptionError):
|
||||
encryption_client.verify_signature(signed, salt="different-salt-value")
|
||||
|
||||
|
||||
def test_should_sign_and_serialize_json(encryption_client):
|
||||
signed = encryption_client.sign({"this": "that"})
|
||||
assert encryption_client.verify_signature(signed) == {"this": "that"}
|
||||
221
tests/notifications_utils/clients/redis/test_redis_client.py
Normal file
221
tests/notifications_utils/clients/redis/test_redis_client.py
Normal file
@@ -0,0 +1,221 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock, call
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
|
||||
from notifications_utils.clients.redis.redis_client import RedisClient, prepare_value
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_redis_pipeline():
|
||||
return Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def delete_mock():
|
||||
return Mock(return_value=4)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_redis_client(app, mocked_redis_pipeline, delete_mock, mocker):
|
||||
app.config["REDIS_ENABLED"] = True
|
||||
|
||||
redis_client = RedisClient()
|
||||
redis_client.init_app(app)
|
||||
|
||||
mocker.patch.object(redis_client.redis_store, "get", return_value=100)
|
||||
mocker.patch.object(redis_client.redis_store, "set")
|
||||
mocker.patch.object(redis_client.redis_store, "incr")
|
||||
mocker.patch.object(redis_client.redis_store, "delete")
|
||||
mocker.patch.object(
|
||||
redis_client.redis_store, "pipeline", return_value=mocked_redis_pipeline
|
||||
)
|
||||
|
||||
mocker.patch.object(
|
||||
redis_client, "scripts", {"delete-keys-by-pattern": delete_mock}
|
||||
)
|
||||
|
||||
mocker.patch.object(
|
||||
redis_client.redis_store,
|
||||
"hgetall",
|
||||
return_value={b"template-1111": b"8", b"template-2222": b"8"},
|
||||
)
|
||||
|
||||
return redis_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def failing_redis_client(mocked_redis_client, delete_mock):
|
||||
mocked_redis_client.redis_store.get.side_effect = Exception("get failed")
|
||||
mocked_redis_client.redis_store.set.side_effect = Exception("set failed")
|
||||
mocked_redis_client.redis_store.incr.side_effect = Exception("incr failed")
|
||||
mocked_redis_client.redis_store.pipeline.side_effect = Exception("pipeline failed")
|
||||
mocked_redis_client.redis_store.delete.side_effect = Exception("delete failed")
|
||||
delete_mock.side_effect = Exception("delete by pattern failed")
|
||||
return mocked_redis_client
|
||||
|
||||
|
||||
def test_should_not_raise_exception_if_raise_set_to_false(
|
||||
app, caplog, failing_redis_client, mocker
|
||||
):
|
||||
mock_logger = mocker.patch("flask.Flask.logger")
|
||||
|
||||
assert failing_redis_client.get("get_key") is None
|
||||
assert failing_redis_client.set("set_key", "set_value") is None
|
||||
assert failing_redis_client.incr("incr_key") is None
|
||||
assert failing_redis_client.exceeded_rate_limit("rate_limit_key", 100, 100) is False
|
||||
assert failing_redis_client.delete("delete_key") is None
|
||||
assert failing_redis_client.delete("a", "b", "c") is None
|
||||
assert failing_redis_client.delete_by_pattern("pattern") == 0
|
||||
|
||||
assert mock_logger.mock_calls == [
|
||||
call.exception("Redis error performing get on get_key"),
|
||||
call.exception("Redis error performing set on set_key"),
|
||||
call.exception("Redis error performing incr on incr_key"),
|
||||
call.exception("Redis error performing rate-limit-pipeline on rate_limit_key"),
|
||||
call.exception("Redis error performing delete on delete_key"),
|
||||
call.exception("Redis error performing delete on a, b, c"),
|
||||
call.exception("Redis error performing delete-by-pattern on pattern"),
|
||||
]
|
||||
|
||||
|
||||
def test_should_raise_exception_if_raise_set_to_true(
|
||||
app,
|
||||
failing_redis_client,
|
||||
):
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.get("test", raise_exception=True)
|
||||
assert str(e.value) == "get failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.set("test", "test", raise_exception=True)
|
||||
assert str(e.value) == "set failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.incr("test", raise_exception=True)
|
||||
assert str(e.value) == "incr failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.exceeded_rate_limit("test", 100, 200, raise_exception=True)
|
||||
assert str(e.value) == "pipeline failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.delete("test", raise_exception=True)
|
||||
assert str(e.value) == "delete failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.delete_by_pattern("pattern", raise_exception=True)
|
||||
assert str(e.value) == "delete by pattern failed"
|
||||
|
||||
|
||||
def test_should_not_call_if_not_enabled(mocked_redis_client, delete_mock):
|
||||
mocked_redis_client.active = False
|
||||
|
||||
assert mocked_redis_client.get("get_key") is None
|
||||
assert mocked_redis_client.set("set_key", "set_value") is None
|
||||
assert mocked_redis_client.incr("incr_key") is None
|
||||
assert mocked_redis_client.exceeded_rate_limit("rate_limit_key", 100, 100) is False
|
||||
assert mocked_redis_client.delete("delete_key") is None
|
||||
assert mocked_redis_client.delete_by_pattern("pattern") == 0
|
||||
|
||||
mocked_redis_client.redis_store.get.assert_not_called()
|
||||
mocked_redis_client.redis_store.set.assert_not_called()
|
||||
mocked_redis_client.redis_store.incr.assert_not_called()
|
||||
mocked_redis_client.redis_store.delete.assert_not_called()
|
||||
mocked_redis_client.redis_store.pipeline.assert_not_called()
|
||||
delete_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_should_call_set_if_enabled(mocked_redis_client):
|
||||
mocked_redis_client.set("key", "value")
|
||||
mocked_redis_client.redis_store.set.assert_called_with(
|
||||
"key", "value", None, None, False, False
|
||||
)
|
||||
|
||||
|
||||
def test_should_call_get_if_enabled(mocked_redis_client):
|
||||
assert mocked_redis_client.get("key") == 100
|
||||
mocked_redis_client.redis_store.get.assert_called_with("key")
|
||||
|
||||
|
||||
@freeze_time("2001-01-01 12:00:00.000000")
|
||||
def test_exceeded_rate_limit_should_add_correct_calls_to_the_pipe(
|
||||
mocked_redis_client, mocked_redis_pipeline
|
||||
):
|
||||
mocked_redis_client.exceeded_rate_limit("key", 100, 100)
|
||||
assert mocked_redis_client.redis_store.pipeline.called
|
||||
mocked_redis_pipeline.zadd.assert_called_with("key", {978350400.0: 978350400.0})
|
||||
mocked_redis_pipeline.zremrangebyscore.assert_called_with(
|
||||
"key", "-inf", 978350300.0
|
||||
)
|
||||
mocked_redis_pipeline.zcard.assert_called_with("key")
|
||||
mocked_redis_pipeline.expire.assert_called_with("key", 100)
|
||||
assert mocked_redis_pipeline.execute.called
|
||||
|
||||
|
||||
@freeze_time("2001-01-01 12:00:00.000000")
|
||||
def test_exceeded_rate_limit_should_fail_request_if_over_limit(
|
||||
mocked_redis_client, mocked_redis_pipeline
|
||||
):
|
||||
mocked_redis_pipeline.execute.return_value = [True, True, 100, True]
|
||||
assert mocked_redis_client.exceeded_rate_limit("key", 99, 100)
|
||||
|
||||
|
||||
@freeze_time("2001-01-01 12:00:00.000000")
|
||||
def test_exceeded_rate_limit_should_allow_request_if_not_over_limit(
|
||||
mocked_redis_client, mocked_redis_pipeline
|
||||
):
|
||||
mocked_redis_pipeline.execute.return_value = [True, True, 100, True]
|
||||
assert not mocked_redis_client.exceeded_rate_limit("key", 101, 100)
|
||||
|
||||
|
||||
@freeze_time("2001-01-01 12:00:00.000000")
|
||||
def test_exceeded_rate_limit_not_exceeded(mocked_redis_client, mocked_redis_pipeline):
|
||||
mocked_redis_pipeline.execute.return_value = [True, True, 80, True]
|
||||
assert not mocked_redis_client.exceeded_rate_limit("key", 90, 100)
|
||||
|
||||
|
||||
def test_exceeded_rate_limit_should_not_call_if_not_enabled(
|
||||
mocked_redis_client, mocked_redis_pipeline
|
||||
):
|
||||
mocked_redis_client.active = False
|
||||
|
||||
assert not mocked_redis_client.exceeded_rate_limit("key", 100, 100)
|
||||
assert not mocked_redis_client.redis_store.pipeline.called
|
||||
|
||||
|
||||
def test_delete(mocked_redis_client):
|
||||
key = "hash-key"
|
||||
mocked_redis_client.delete(key)
|
||||
mocked_redis_client.redis_store.delete.assert_called_with(key)
|
||||
|
||||
|
||||
def test_delete_multi(mocked_redis_client):
|
||||
mocked_redis_client.delete("a", "b", "c")
|
||||
mocked_redis_client.redis_store.delete.assert_called_with("a", "b", "c")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input,output",
|
||||
[
|
||||
(b"asdf", b"asdf"),
|
||||
("asdf", "asdf"),
|
||||
(0, 0),
|
||||
(1.2, 1.2),
|
||||
(uuid.UUID(int=0), "00000000-0000-0000-0000-000000000000"),
|
||||
pytest.param({"a": 1}, None, marks=pytest.mark.xfail(raises=ValueError)),
|
||||
pytest.param(
|
||||
datetime.utcnow(), None, marks=pytest.mark.xfail(raises=ValueError)
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_prepare_value(input, output):
|
||||
assert prepare_value(input) == output
|
||||
|
||||
|
||||
def test_delete_by_pattern(mocked_redis_client, delete_mock):
|
||||
ret = mocked_redis_client.delete_by_pattern("foo")
|
||||
assert ret == 4
|
||||
delete_mock.assert_called_once_with(args=["foo"])
|
||||
190
tests/notifications_utils/clients/redis/test_request_cache.py
Normal file
190
tests/notifications_utils/clients/redis/test_request_cache.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import pytest
|
||||
|
||||
from notifications_utils.clients.redis import RequestCache
|
||||
from notifications_utils.clients.redis.redis_client import RedisClient
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_redis_client(app):
|
||||
app.config["REDIS_ENABLED"] = True
|
||||
redis_client = RedisClient()
|
||||
redis_client.init_app(app)
|
||||
return redis_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cache(mocked_redis_client):
|
||||
return RequestCache(mocked_redis_client)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args, kwargs, expected_cache_key",
|
||||
(
|
||||
([1, 2, 3], {}, "1-2-3-None-None-None"),
|
||||
([1, 2, 3, 4, 5, 6], {}, "1-2-3-4-5-6"),
|
||||
([1, 2, 3], {"x": 4, "y": 5, "z": 6}, "1-2-3-4-5-6"),
|
||||
([1, 2, 3, 4], {"y": 5}, "1-2-3-4-5-None"),
|
||||
),
|
||||
)
|
||||
def test_set(
|
||||
mocker,
|
||||
mocked_redis_client,
|
||||
cache,
|
||||
args,
|
||||
kwargs,
|
||||
expected_cache_key,
|
||||
):
|
||||
mock_redis_set = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"set",
|
||||
)
|
||||
mock_redis_get = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"get",
|
||||
return_value=None,
|
||||
)
|
||||
|
||||
@cache.set("{a}-{b}-{c}-{x}-{y}-{z}")
|
||||
def foo(a, b, c, x=None, y=None, z=None):
|
||||
return "bar"
|
||||
|
||||
assert foo(*args, **kwargs) == "bar"
|
||||
|
||||
mock_redis_get.assert_called_once_with(expected_cache_key)
|
||||
|
||||
mock_redis_set.assert_called_once_with(
|
||||
expected_cache_key,
|
||||
'"bar"',
|
||||
ex=604_800,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cache_set_call, expected_redis_client_ttl",
|
||||
(
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(1.111, 1),
|
||||
("2000", 2_000),
|
||||
),
|
||||
)
|
||||
def test_set_with_custom_ttl(
|
||||
mocker,
|
||||
mocked_redis_client,
|
||||
cache,
|
||||
cache_set_call,
|
||||
expected_redis_client_ttl,
|
||||
):
|
||||
mock_redis_set = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"set",
|
||||
)
|
||||
mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"get",
|
||||
return_value=None,
|
||||
)
|
||||
|
||||
@cache.set("foo", ttl_in_seconds=cache_set_call)
|
||||
def foo():
|
||||
return "bar"
|
||||
|
||||
foo()
|
||||
|
||||
mock_redis_set.assert_called_once_with(
|
||||
"foo",
|
||||
'"bar"',
|
||||
ex=expected_redis_client_ttl,
|
||||
)
|
||||
|
||||
|
||||
def test_raises_if_key_doesnt_match_arguments(cache):
|
||||
@cache.set("{baz}")
|
||||
def foo(bar):
|
||||
pass
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
foo(1)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
foo()
|
||||
|
||||
|
||||
def test_get(mocker, mocked_redis_client, cache):
|
||||
mock_redis_get = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"get",
|
||||
return_value=b'"bar"',
|
||||
)
|
||||
|
||||
@cache.set("{a}-{b}-{c}")
|
||||
def foo(a, b, c):
|
||||
# This function should not be called because the cache has
|
||||
# returned a value
|
||||
raise RuntimeError
|
||||
|
||||
assert foo(1, 2, 3) == "bar"
|
||||
|
||||
mock_redis_get.assert_called_once_with("1-2-3")
|
||||
|
||||
|
||||
def test_delete(mocker, mocked_redis_client, cache):
|
||||
mock_redis_delete = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"delete",
|
||||
)
|
||||
|
||||
@cache.delete("{a}-{b}-{c}")
|
||||
def foo(a, b, c):
|
||||
return "bar"
|
||||
|
||||
assert foo(1, 2, 3) == "bar"
|
||||
|
||||
mock_redis_delete.assert_called_once_with("1-2-3")
|
||||
|
||||
|
||||
def test_delete_even_if_call_raises(mocker, mocked_redis_client, cache):
|
||||
mock_redis_delete = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"delete",
|
||||
)
|
||||
|
||||
@cache.delete("bar")
|
||||
def foo():
|
||||
raise RuntimeError
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
foo()
|
||||
|
||||
mock_redis_delete.assert_called_once_with("bar")
|
||||
|
||||
|
||||
def test_delete_by_pattern(mocker, mocked_redis_client, cache):
|
||||
mock_redis_delete = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"delete_by_pattern",
|
||||
)
|
||||
|
||||
@cache.delete_by_pattern("{a}-{b}-{c}-???")
|
||||
def foo(a, b, c):
|
||||
return "bar"
|
||||
|
||||
assert foo(1, 2, 3) == "bar"
|
||||
|
||||
mock_redis_delete.assert_called_once_with("1-2-3-???")
|
||||
|
||||
|
||||
def test_delete_by_pattern_even_if_call_raises(mocker, mocked_redis_client, cache):
|
||||
mock_redis_delete = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"delete_by_pattern",
|
||||
)
|
||||
|
||||
@cache.delete_by_pattern("bar-???")
|
||||
def foo():
|
||||
raise RuntimeError
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
foo()
|
||||
|
||||
mock_redis_delete.assert_called_once_with("bar-???")
|
||||
7
tests/notifications_utils/clients/test_redis.py
Normal file
7
tests/notifications_utils/clients/test_redis.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from notifications_utils.clients.redis import rate_limit_cache_key
|
||||
|
||||
|
||||
def test_rate_limit_cache_key(sample_service):
|
||||
assert rate_limit_cache_key(sample_service.id, "TEST") == "{}-TEST".format(
|
||||
sample_service.id
|
||||
)
|
||||
220
tests/notifications_utils/clients/zendesk/test_zendesk_client.py
Normal file
220
tests/notifications_utils/clients/zendesk/test_zendesk_client.py
Normal file
@@ -0,0 +1,220 @@
|
||||
from base64 import b64decode
|
||||
|
||||
import pytest
|
||||
|
||||
from notifications_utils.clients.zendesk.zendesk_client import (
|
||||
NotifySupportTicket,
|
||||
ZendeskClient,
|
||||
ZendeskError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def zendesk_client(app):
|
||||
client = ZendeskClient()
|
||||
|
||||
app.config["ZENDESK_API_KEY"] = "testkey"
|
||||
|
||||
client.init_app(app)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def test_zendesk_client_send_ticket_to_zendesk(zendesk_client, app, mocker, rmock):
|
||||
rmock.request(
|
||||
"POST",
|
||||
ZendeskClient.ZENDESK_TICKET_URL,
|
||||
status_code=201,
|
||||
json={
|
||||
"ticket": {
|
||||
"id": 12345,
|
||||
"subject": "Something is wrong",
|
||||
}
|
||||
},
|
||||
)
|
||||
mock_logger = mocker.patch.object(app.logger, "info")
|
||||
|
||||
ticket = NotifySupportTicket("subject", "message", "incident")
|
||||
zendesk_client.send_ticket_to_zendesk(ticket)
|
||||
|
||||
assert rmock.last_request.headers["Authorization"][:6] == "Basic "
|
||||
b64_auth = rmock.last_request.headers["Authorization"][6:]
|
||||
assert (
|
||||
b64decode(b64_auth.encode()).decode()
|
||||
== "zd-api-notify@digital.cabinet-office.gov.uk/token:testkey"
|
||||
)
|
||||
assert rmock.last_request.json() == ticket.request_data
|
||||
mock_logger.assert_called_once_with("Zendesk create ticket 12345 succeeded")
|
||||
|
||||
|
||||
def test_zendesk_client_send_ticket_to_zendesk_error(
|
||||
zendesk_client, app, mocker, rmock
|
||||
):
|
||||
rmock.request(
|
||||
"POST", ZendeskClient.ZENDESK_TICKET_URL, status_code=401, json={"foo": "bar"}
|
||||
)
|
||||
|
||||
mock_logger = mocker.patch.object(app.logger, "error")
|
||||
|
||||
ticket = NotifySupportTicket("subject", "message", "incident")
|
||||
|
||||
with pytest.raises(ZendeskError):
|
||||
zendesk_client.send_ticket_to_zendesk(ticket)
|
||||
|
||||
mock_logger.assert_called_with(
|
||||
"Zendesk create ticket request failed with 401 '{'foo': 'bar'}'"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"p1_arg, expected_tags, expected_priority",
|
||||
(
|
||||
(
|
||||
{},
|
||||
["govuk_notify_support"],
|
||||
"normal",
|
||||
),
|
||||
(
|
||||
{
|
||||
"p1": False,
|
||||
},
|
||||
["govuk_notify_support"],
|
||||
"normal",
|
||||
),
|
||||
(
|
||||
{
|
||||
"p1": True,
|
||||
},
|
||||
["govuk_notify_emergency"],
|
||||
"urgent",
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_notify_support_ticket_request_data(p1_arg, expected_tags, expected_priority):
|
||||
notify_ticket_form = NotifySupportTicket("subject", "message", "question", **p1_arg)
|
||||
|
||||
assert notify_ticket_form.request_data == {
|
||||
"ticket": {
|
||||
"subject": "subject",
|
||||
"comment": {
|
||||
"body": "message",
|
||||
"public": True,
|
||||
},
|
||||
"group_id": NotifySupportTicket.NOTIFY_GROUP_ID,
|
||||
"organization_id": NotifySupportTicket.NOTIFY_ORG_ID,
|
||||
"ticket_form_id": NotifySupportTicket.NOTIFY_TICKET_FORM_ID,
|
||||
"priority": expected_priority,
|
||||
"tags": expected_tags,
|
||||
"type": "question",
|
||||
"custom_fields": [
|
||||
{"id": "1900000744994", "value": "notify_ticket_type_non_technical"},
|
||||
{"id": "360022836500", "value": []},
|
||||
{"id": "360022943959", "value": None},
|
||||
{"id": "360022943979", "value": None},
|
||||
{"id": "1900000745014", "value": None},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_notify_support_ticket_request_data_with_message_hidden_from_requester():
|
||||
notify_ticket_form = NotifySupportTicket(
|
||||
"subject", "message", "problem", requester_sees_message_content=False
|
||||
)
|
||||
|
||||
assert notify_ticket_form.request_data["ticket"]["comment"]["public"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name, zendesk_name", [("Name", "Name"), (None, "(no name supplied)")]
|
||||
)
|
||||
def test_notify_support_ticket_request_data_with_user_name_and_email(
|
||||
name, zendesk_name
|
||||
):
|
||||
notify_ticket_form = NotifySupportTicket(
|
||||
"subject", "message", "question", user_name=name, user_email="user@example.com"
|
||||
)
|
||||
|
||||
assert (
|
||||
notify_ticket_form.request_data["ticket"]["requester"]["email"]
|
||||
== "user@example.com"
|
||||
)
|
||||
assert (
|
||||
notify_ticket_form.request_data["ticket"]["requester"]["name"] == zendesk_name
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"custom_fields, tech_ticket_tag, categories, org_id, org_type, service_id",
|
||||
[
|
||||
(
|
||||
{"technical_ticket": True},
|
||||
"notify_ticket_type_technical",
|
||||
[],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
{"technical_ticket": False},
|
||||
"notify_ticket_type_non_technical",
|
||||
[],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
{"ticket_categories": ["notify_billing", "notify_bug"]},
|
||||
"notify_ticket_type_non_technical",
|
||||
["notify_billing", "notify_bug"],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
{"org_id": "1234", "org_type": "local"},
|
||||
"notify_ticket_type_non_technical",
|
||||
[],
|
||||
"1234",
|
||||
"notify_org_type_local",
|
||||
None,
|
||||
),
|
||||
(
|
||||
{"service_id": "abcd", "org_type": "nhs"},
|
||||
"notify_ticket_type_non_technical",
|
||||
[],
|
||||
None,
|
||||
"notify_org_type_nhs",
|
||||
"abcd",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_notify_support_ticket_request_data_custom_fields(
|
||||
custom_fields,
|
||||
tech_ticket_tag,
|
||||
categories,
|
||||
org_id,
|
||||
org_type,
|
||||
service_id,
|
||||
):
|
||||
notify_ticket_form = NotifySupportTicket(
|
||||
"subject", "message", "question", **custom_fields
|
||||
)
|
||||
|
||||
assert notify_ticket_form.request_data["ticket"]["custom_fields"] == [
|
||||
{"id": "1900000744994", "value": tech_ticket_tag},
|
||||
{"id": "360022836500", "value": categories},
|
||||
{"id": "360022943959", "value": org_id},
|
||||
{"id": "360022943979", "value": org_type},
|
||||
{"id": "1900000745014", "value": service_id},
|
||||
]
|
||||
|
||||
|
||||
def test_notify_support_ticket_request_data_email_ccs():
|
||||
notify_ticket_form = NotifySupportTicket(
|
||||
"subject", "message", "question", email_ccs=["someone@example.com"]
|
||||
)
|
||||
|
||||
assert notify_ticket_form.request_data["ticket"]["email_ccs"] == [
|
||||
{"user_email": "someone@example.com", "action": "put"},
|
||||
]
|
||||
Reference in New Issue
Block a user