mirror of
https://github.com/GSA/notifications-api.git
synced 2026-02-03 09:51:11 -05:00
Merge pull request #3003 from alphagov/staging-preview-cbc
Non-production environments invoke CBC Proxy during broadcast event creation
This commit is contained in:
@@ -23,6 +23,7 @@ from werkzeug.local import LocalProxy
|
|||||||
|
|
||||||
from app.celery.celery import NotifyCelery
|
from app.celery.celery import NotifyCelery
|
||||||
from app.clients import Clients
|
from app.clients import Clients
|
||||||
|
from app.clients.cbc_proxy import CBCProxyClient, CBCProxyNoopClient
|
||||||
from app.clients.document_download import DocumentDownloadClient
|
from app.clients.document_download import DocumentDownloadClient
|
||||||
from app.clients.email.aws_ses import AwsSesClient
|
from app.clients.email.aws_ses import AwsSesClient
|
||||||
from app.clients.email.aws_ses_stub import AwsSesStubClient
|
from app.clients.email.aws_ses_stub import AwsSesStubClient
|
||||||
@@ -60,6 +61,7 @@ zendesk_client = ZendeskClient()
|
|||||||
statsd_client = StatsdClient()
|
statsd_client = StatsdClient()
|
||||||
redis_store = RedisClient()
|
redis_store = RedisClient()
|
||||||
performance_platform_client = PerformancePlatformClient()
|
performance_platform_client = PerformancePlatformClient()
|
||||||
|
cbc_proxy_client = CBCProxyNoopClient()
|
||||||
document_download_client = DocumentDownloadClient()
|
document_download_client = DocumentDownloadClient()
|
||||||
metrics = GDSMetrics()
|
metrics = GDSMetrics()
|
||||||
|
|
||||||
@@ -112,6 +114,11 @@ def create_app(application):
|
|||||||
performance_platform_client.init_app(application)
|
performance_platform_client.init_app(application)
|
||||||
document_download_client.init_app(application)
|
document_download_client.init_app(application)
|
||||||
|
|
||||||
|
global cbc_proxy_client
|
||||||
|
if application.config['CBC_PROXY_AWS_ACCESS_KEY_ID']:
|
||||||
|
cbc_proxy_client = CBCProxyClient()
|
||||||
|
cbc_proxy_client.init_app(application)
|
||||||
|
|
||||||
register_blueprint(application)
|
register_blueprint(application)
|
||||||
register_v2_blueprints(application)
|
register_v2_blueprints(application)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import requests
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
from notifications_utils.statsd_decorators import statsd
|
from notifications_utils.statsd_decorators import statsd
|
||||||
|
|
||||||
from app import notify_celery
|
from app import cbc_proxy_client, notify_celery
|
||||||
|
|
||||||
|
from app.models import BroadcastEventMessageType
|
||||||
from app.dao.broadcast_message_dao import dao_get_broadcast_event_by_id
|
from app.dao.broadcast_message_dao import dao_get_broadcast_event_by_id
|
||||||
|
|
||||||
|
|
||||||
@@ -12,6 +13,19 @@ from app.dao.broadcast_message_dao import dao_get_broadcast_event_by_id
|
|||||||
def send_broadcast_event(broadcast_event_id, provider='stub-1'):
|
def send_broadcast_event(broadcast_event_id, provider='stub-1'):
|
||||||
broadcast_event = dao_get_broadcast_event_by_id(broadcast_event_id)
|
broadcast_event = dao_get_broadcast_event_by_id(broadcast_event_id)
|
||||||
|
|
||||||
|
if broadcast_event.message_type == BroadcastEventMessageType.ALERT:
|
||||||
|
current_app.logger.info(
|
||||||
|
f'invoking cbc proxy to send '
|
||||||
|
f'broadcast_event {broadcast_event.reference} '
|
||||||
|
f'msgType {broadcast_event.message_type} to {provider}'
|
||||||
|
)
|
||||||
|
|
||||||
|
cbc_proxy_client.create_and_send_broadcast(
|
||||||
|
identifier=str(broadcast_event.id),
|
||||||
|
headline="GOV.UK Notify Broadcast",
|
||||||
|
description=broadcast_event.transmitted_content['body'],
|
||||||
|
)
|
||||||
|
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f'sending broadcast_event {broadcast_event.reference} '
|
f'sending broadcast_event {broadcast_event.reference} '
|
||||||
f'msgType {broadcast_event.message_type} to {provider}'
|
f'msgType {broadcast_event.message_type} to {provider}'
|
||||||
|
|||||||
90
app/clients/cbc_proxy.py
Normal file
90
app/clients/cbc_proxy.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
# The variable names in this file have specific meaning in a CAP message
|
||||||
|
#
|
||||||
|
# identifier is a unique field for each CAP message
|
||||||
|
#
|
||||||
|
# headline is a field which we are not sure if we will use
|
||||||
|
#
|
||||||
|
# description is the body of the message
|
||||||
|
#
|
||||||
|
# references is a whitespace separated list of message identifiers
|
||||||
|
# where each identifier is a previous sent message
|
||||||
|
# ie a Cancel message would have a unique identifier but have the identifier of
|
||||||
|
# the preceeding Alert message in the references field
|
||||||
|
|
||||||
|
|
||||||
|
# Noop = no operation
|
||||||
|
class CBCProxyNoopClient:
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_and_send_broadcast(
|
||||||
|
self,
|
||||||
|
identifier, headline, description,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# We have not implementated updating a broadcast
|
||||||
|
def update_and_send_broadcast(
|
||||||
|
self,
|
||||||
|
identifier, references, headline, description,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# We have not implemented cancelling a broadcast
|
||||||
|
def cancel_broadcast(
|
||||||
|
self,
|
||||||
|
identifier, references, headline, description,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CBCProxyClient:
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
self._lambda_client = boto3.client(
|
||||||
|
'lambda',
|
||||||
|
region_name='eu-west-2',
|
||||||
|
aws_access_key_id=app.config['CBC_PROXY_AWS_ACCESS_KEY_ID'],
|
||||||
|
aws_secret_access_key=app.config['CBC_PROXY_AWS_SECRET_ACCESS_KEY'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_and_send_broadcast(
|
||||||
|
self,
|
||||||
|
identifier, headline, description,
|
||||||
|
):
|
||||||
|
payload_bytes = bytes(json.dumps({
|
||||||
|
'identifier': identifier,
|
||||||
|
'headline': headline,
|
||||||
|
'description': description,
|
||||||
|
}), encoding='utf8')
|
||||||
|
|
||||||
|
result = self._lambda_client.invoke(
|
||||||
|
FunctionName='bt-ee-1-proxy',
|
||||||
|
InvocationType='RequestResponse',
|
||||||
|
Payload=payload_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result['StatusCode'] > 299:
|
||||||
|
raise Exception('Could not invoke lambda')
|
||||||
|
|
||||||
|
if 'FunctionError' in result:
|
||||||
|
raise Exception('Function exited with unhandled exception')
|
||||||
|
|
||||||
|
# We have not implementated updating a broadcast
|
||||||
|
def update_and_send_broadcast(
|
||||||
|
self,
|
||||||
|
identifier, references, headline, description,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# We have not implemented cancelling a broadcast
|
||||||
|
def cancel_broadcast(
|
||||||
|
self,
|
||||||
|
identifier, references, headline, description,
|
||||||
|
):
|
||||||
|
pass
|
||||||
@@ -353,6 +353,11 @@ class Config(object):
|
|||||||
|
|
||||||
AWS_REGION = 'eu-west-1'
|
AWS_REGION = 'eu-west-1'
|
||||||
|
|
||||||
|
# CBC Proxy
|
||||||
|
# if the access keys are empty then noop client is used
|
||||||
|
CBC_PROXY_AWS_ACCESS_KEY_ID = os.environ.get('CBC_PROXY_AWS_ACCESS_KEY_ID', '')
|
||||||
|
CBC_PROXY_AWS_SECRET_ACCESS_KEY = os.environ.get('CBC_PROXY_AWS_SECRET_ACCESS_KEY', '')
|
||||||
|
|
||||||
|
|
||||||
######################
|
######################
|
||||||
# Config overrides ###
|
# Config overrides ###
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ applications:
|
|||||||
AWS_ACCESS_KEY_ID: '{{ AWS_ACCESS_KEY_ID }}'
|
AWS_ACCESS_KEY_ID: '{{ AWS_ACCESS_KEY_ID }}'
|
||||||
AWS_SECRET_ACCESS_KEY: '{{ AWS_SECRET_ACCESS_KEY }}'
|
AWS_SECRET_ACCESS_KEY: '{{ AWS_SECRET_ACCESS_KEY }}'
|
||||||
|
|
||||||
|
{% if CBC_PROXY_AWS_ACCESS_KEY_ID is defined %}
|
||||||
|
CBC_PROXY_AWS_ACCESS_KEY_ID: '{{ CBC_PROXY_AWS_ACCESS_KEY_ID }}'
|
||||||
|
CBC_PROXY_AWS_SECRET_ACCESS_KEY: '{{ CBC_PROXY_AWS_SECRET_ACCESS_KEY }}'
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
STATSD_HOST: "notify-statsd-exporter-{{ environment }}.apps.internal"
|
STATSD_HOST: "notify-statsd-exporter-{{ environment }}.apps.internal"
|
||||||
|
|
||||||
ZENDESK_API_KEY: '{{ ZENDESK_API_KEY }}'
|
ZENDESK_API_KEY: '{{ ZENDESK_API_KEY }}'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from tests.app.db import create_template, create_broadcast_message, create_broad
|
|||||||
|
|
||||||
|
|
||||||
@freeze_time('2020-08-01 12:00')
|
@freeze_time('2020-08-01 12:00')
|
||||||
def test_send_broadcast_event_sends_data_correctly(sample_service):
|
def test_send_broadcast_event_sends_data_correctly(mocker, sample_service):
|
||||||
template = create_template(sample_service, BROADCAST_TYPE)
|
template = create_template(sample_service, BROADCAST_TYPE)
|
||||||
broadcast_message = create_broadcast_message(
|
broadcast_message = create_broadcast_message(
|
||||||
template,
|
template,
|
||||||
@@ -18,10 +18,20 @@ def test_send_broadcast_event_sends_data_correctly(sample_service):
|
|||||||
)
|
)
|
||||||
event = create_broadcast_event(broadcast_message)
|
event = create_broadcast_event(broadcast_message)
|
||||||
|
|
||||||
|
mock_create_broadcast = mocker.patch(
|
||||||
|
'app.cbc_proxy_client.create_and_send_broadcast',
|
||||||
|
)
|
||||||
|
|
||||||
with requests_mock.Mocker() as request_mock:
|
with requests_mock.Mocker() as request_mock:
|
||||||
request_mock.post("http://test-cbc-proxy/broadcasts/events/stub-1", json={'valid': 'true'}, status_code=200)
|
request_mock.post("http://test-cbc-proxy/broadcasts/events/stub-1", json={'valid': 'true'}, status_code=200)
|
||||||
send_broadcast_event(broadcast_event_id=str(event.id))
|
send_broadcast_event(broadcast_event_id=str(event.id))
|
||||||
|
|
||||||
|
mock_create_broadcast.assert_called_once_with(
|
||||||
|
identifier=str(event.id),
|
||||||
|
headline="GOV.UK Notify Broadcast",
|
||||||
|
description='this is an emergency broadcast message',
|
||||||
|
)
|
||||||
|
|
||||||
assert request_mock.call_count == 1
|
assert request_mock.call_count == 1
|
||||||
assert request_mock.request_history[0].method == 'POST'
|
assert request_mock.request_history[0].method == 'POST'
|
||||||
assert request_mock.request_history[0].headers["Content-type"] == "application/json"
|
assert request_mock.request_history[0].headers["Content-type"] == "application/json"
|
||||||
@@ -36,16 +46,22 @@ def test_send_broadcast_event_sends_data_correctly(sample_service):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_send_broadcast_event_sends_references(sample_service):
|
def test_send_broadcast_event_sends_references(mocker, sample_service):
|
||||||
template = create_template(sample_service, BROADCAST_TYPE, content='content')
|
template = create_template(sample_service, BROADCAST_TYPE, content='content')
|
||||||
broadcast_message = create_broadcast_message(template, areas=['london'], status=BroadcastStatusType.BROADCASTING)
|
broadcast_message = create_broadcast_message(template, areas=['london'], status=BroadcastStatusType.BROADCASTING)
|
||||||
alert_event = create_broadcast_event(broadcast_message, message_type=BroadcastEventMessageType.ALERT)
|
alert_event = create_broadcast_event(broadcast_message, message_type=BroadcastEventMessageType.ALERT)
|
||||||
cancel_event = create_broadcast_event(broadcast_message, message_type=BroadcastEventMessageType.CANCEL)
|
cancel_event = create_broadcast_event(broadcast_message, message_type=BroadcastEventMessageType.CANCEL)
|
||||||
|
|
||||||
|
mock_create_broadcast = mocker.patch(
|
||||||
|
'app.cbc_proxy_client.create_and_send_broadcast',
|
||||||
|
)
|
||||||
|
|
||||||
with requests_mock.Mocker() as request_mock:
|
with requests_mock.Mocker() as request_mock:
|
||||||
request_mock.post("http://test-cbc-proxy/broadcasts/events/stub-1", json={'valid': 'true'}, status_code=200)
|
request_mock.post("http://test-cbc-proxy/broadcasts/events/stub-1", json={'valid': 'true'}, status_code=200)
|
||||||
send_broadcast_event(broadcast_event_id=str(cancel_event.id))
|
send_broadcast_event(broadcast_event_id=str(cancel_event.id))
|
||||||
|
|
||||||
|
assert not mock_create_broadcast.called
|
||||||
|
|
||||||
assert request_mock.call_count == 1
|
assert request_mock.call_count == 1
|
||||||
assert request_mock.request_history[0].method == 'POST'
|
assert request_mock.request_history[0].method == 'POST'
|
||||||
assert request_mock.request_history[0].headers["Content-type"] == "application/json"
|
assert request_mock.request_history[0].headers["Content-type"] == "application/json"
|
||||||
@@ -56,11 +72,15 @@ def test_send_broadcast_event_sends_references(sample_service):
|
|||||||
assert cbc_json['previous_event_references'] == [alert_event.reference]
|
assert cbc_json['previous_event_references'] == [alert_event.reference]
|
||||||
|
|
||||||
|
|
||||||
def test_send_broadcast_event_errors(sample_service):
|
def test_send_broadcast_event_errors(mocker, sample_service):
|
||||||
template = create_template(sample_service, BROADCAST_TYPE)
|
template = create_template(sample_service, BROADCAST_TYPE)
|
||||||
broadcast_message = create_broadcast_message(template, status=BroadcastStatusType.BROADCASTING)
|
broadcast_message = create_broadcast_message(template, status=BroadcastStatusType.BROADCASTING)
|
||||||
event = create_broadcast_event(broadcast_message)
|
event = create_broadcast_event(broadcast_message)
|
||||||
|
|
||||||
|
mock_create_broadcast = mocker.patch(
|
||||||
|
'app.cbc_proxy_client.create_and_send_broadcast',
|
||||||
|
)
|
||||||
|
|
||||||
with requests_mock.Mocker() as request_mock:
|
with requests_mock.Mocker() as request_mock:
|
||||||
request_mock.post("http://test-cbc-proxy/broadcasts/events/stub-1", text='503 bad gateway', status_code=503)
|
request_mock.post("http://test-cbc-proxy/broadcasts/events/stub-1", text='503 bad gateway', status_code=503)
|
||||||
# we're not retrying or anything for the moment - but this'll ensure any exception gets logged
|
# we're not retrying or anything for the moment - but this'll ensure any exception gets logged
|
||||||
@@ -68,3 +88,9 @@ def test_send_broadcast_event_errors(sample_service):
|
|||||||
send_broadcast_event(broadcast_event_id=str(event.id))
|
send_broadcast_event(broadcast_event_id=str(event.id))
|
||||||
|
|
||||||
assert ex.value.response.status_code == 503
|
assert ex.value.response.status_code == 503
|
||||||
|
|
||||||
|
mock_create_broadcast.assert_called_once_with(
|
||||||
|
identifier=str(event.id),
|
||||||
|
headline="GOV.UK Notify Broadcast",
|
||||||
|
description='this is an emergency broadcast message',
|
||||||
|
)
|
||||||
|
|||||||
127
tests/app/clients/test_cbc_proxy.py
Normal file
127
tests/app/clients/test_cbc_proxy.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.clients.cbc_proxy import CBCProxyClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def cbc_proxy(client, mocker):
|
||||||
|
client = CBCProxyClient()
|
||||||
|
current_app = mocker.Mock(config={
|
||||||
|
'CBC_PROXY_AWS_ACCESS_KEY_ID': 'cbc-proxy-aws-access-key-id',
|
||||||
|
'CBC_PROXY_AWS_SECRET_ACCESS_KEY': 'cbc-proxy-aws-secret-access-key',
|
||||||
|
})
|
||||||
|
client.init_app(current_app)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def test_cbc_proxy_lambda_client_has_correct_region(cbc_proxy):
|
||||||
|
assert cbc_proxy._lambda_client._client_config.region_name == 'eu-west-2'
|
||||||
|
|
||||||
|
|
||||||
|
def test_cbc_proxy_lambda_client_has_correct_keys(cbc_proxy):
|
||||||
|
key = cbc_proxy._lambda_client._request_signer._credentials.access_key
|
||||||
|
secret = cbc_proxy._lambda_client._request_signer._credentials.secret_key
|
||||||
|
|
||||||
|
assert key == 'cbc-proxy-aws-access-key-id'
|
||||||
|
assert secret == 'cbc-proxy-aws-secret-access-key'
|
||||||
|
|
||||||
|
|
||||||
|
def test_cbc_proxy_create_and_send_invokes_function(mocker, cbc_proxy):
|
||||||
|
identifier = 'my-identifier'
|
||||||
|
headline = 'my-headline'
|
||||||
|
description = 'my-description'
|
||||||
|
|
||||||
|
ld_client_mock = mocker.patch.object(
|
||||||
|
cbc_proxy,
|
||||||
|
'_lambda_client',
|
||||||
|
create=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
ld_client_mock.invoke.return_value = {
|
||||||
|
'StatusCode': 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
cbc_proxy.create_and_send_broadcast(
|
||||||
|
identifier=identifier,
|
||||||
|
headline=headline,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
ld_client_mock.invoke.assert_called_once_with(
|
||||||
|
FunctionName='bt-ee-1-proxy',
|
||||||
|
InvocationType='RequestResponse',
|
||||||
|
Payload=mocker.ANY,
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs = ld_client_mock.invoke.mock_calls[0][-1]
|
||||||
|
payload_bytes = kwargs['Payload']
|
||||||
|
payload = json.loads(payload_bytes)
|
||||||
|
|
||||||
|
assert payload['identifier'] == identifier
|
||||||
|
assert payload['headline'] == headline
|
||||||
|
assert payload['description'] == description
|
||||||
|
|
||||||
|
|
||||||
|
def test_cbc_proxy_create_and_send_handles_invoke_error(mocker, cbc_proxy):
|
||||||
|
identifier = 'my-identifier'
|
||||||
|
headline = 'my-headline'
|
||||||
|
description = 'my-description'
|
||||||
|
|
||||||
|
ld_client_mock = mocker.patch.object(
|
||||||
|
cbc_proxy,
|
||||||
|
'_lambda_client',
|
||||||
|
create=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
ld_client_mock.invoke.return_value = {
|
||||||
|
'StatusCode': 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as e:
|
||||||
|
cbc_proxy.create_and_send_broadcast(
|
||||||
|
identifier=identifier,
|
||||||
|
headline=headline,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert e.match('Could not invoke lambda')
|
||||||
|
|
||||||
|
ld_client_mock.invoke.assert_called_once_with(
|
||||||
|
FunctionName='bt-ee-1-proxy',
|
||||||
|
InvocationType='RequestResponse',
|
||||||
|
Payload=mocker.ANY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cbc_proxy_create_and_send_handles_function_error(mocker, cbc_proxy):
|
||||||
|
identifier = 'my-identifier'
|
||||||
|
headline = 'my-headline'
|
||||||
|
description = 'my-description'
|
||||||
|
|
||||||
|
ld_client_mock = mocker.patch.object(
|
||||||
|
cbc_proxy,
|
||||||
|
'_lambda_client',
|
||||||
|
create=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
ld_client_mock.invoke.return_value = {
|
||||||
|
'StatusCode': 200,
|
||||||
|
'FunctionError': 'something',
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as e:
|
||||||
|
cbc_proxy.create_and_send_broadcast(
|
||||||
|
identifier=identifier,
|
||||||
|
headline=headline,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert e.match('Function exited with unhandled exception')
|
||||||
|
|
||||||
|
ld_client_mock.invoke.assert_called_once_with(
|
||||||
|
FunctionName='bt-ee-1-proxy',
|
||||||
|
InvocationType='RequestResponse',
|
||||||
|
Payload=mocker.ANY,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user