Merge pull request #1034 from alphagov/push-inbound-sms

Push inbound sms
This commit is contained in:
Rebecca Law
2017-06-19 15:15:07 +01:00
committed by GitHub
21 changed files with 519 additions and 64 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ def requires_auth():
for api_key in service.api_keys: for api_key in service.api_keys:
try: try:
get_decode_errors(auth_token, api_key.unsigned_secret) get_decode_errors(auth_token, api_key.secret)
except TokenDecodeError: except TokenDecodeError:
continue continue
-12
View File
@@ -1,12 +0,0 @@
from flask import current_app
from itsdangerous import URLSafeSerializer
def get_secret(secret):
serializer = URLSafeSerializer(current_app.config.get('SECRET_KEY'))
return serializer.loads(secret, salt=current_app.config.get('DANGEROUS_SALT'))
def generate_secret(token):
serializer = URLSafeSerializer(current_app.config.get('SECRET_KEY'))
return serializer.dumps(str(token), current_app.config.get('DANGEROUS_SALT'))
+4 -5
View File
@@ -1,14 +1,13 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from app import db from app import db, encryption
from app.models import ApiKey from app.models import ApiKey
from app.dao.dao_utils import ( from app.dao.dao_utils import (
transactional, transactional,
version_class version_class
) )
from app.authentication.utils import generate_secret
@transactional @transactional
@@ -16,7 +15,7 @@ from app.authentication.utils import generate_secret
def save_model_api_key(api_key): def save_model_api_key(api_key):
if not api_key.id: if not api_key.id:
api_key.id = uuid.uuid4() # must be set now so version history model can use same id api_key.id = uuid.uuid4() # must be set now so version history model can use same id
api_key.secret = generate_secret(uuid.uuid4()) api_key.secret = uuid.uuid4()
db.session.add(api_key) db.session.add(api_key)
@@ -39,7 +38,7 @@ def get_unsigned_secrets(service_id):
This method can only be exposed to the Authentication of the api calls. This method can only be exposed to the Authentication of the api calls.
""" """
api_keys = ApiKey.query.filter_by(service_id=service_id, expiry_date=None).all() api_keys = ApiKey.query.filter_by(service_id=service_id, expiry_date=None).all()
keys = [x.unsigned_secret for x in api_keys] keys = [x.secret for x in api_keys]
return keys return keys
@@ -48,4 +47,4 @@ def get_unsigned_secret(key_id):
This method can only be exposed to the Authentication of the api calls. This method can only be exposed to the Authentication of the api calls.
""" """
api_key = ApiKey.query.filter_by(id=key_id, expiry_date=None).one() api_key = ApiKey.query.filter_by(id=key_id, expiry_date=None).one()
return api_key.unsigned_secret return api_key.secret
+32
View File
@@ -0,0 +1,32 @@
from datetime import datetime
from app import db, create_uuid
from app.dao.dao_utils import transactional, version_class
from app.models import ServiceInboundApi
@transactional
@version_class(ServiceInboundApi)
def save_service_inbound_api(service_inbound_api):
service_inbound_api.id = create_uuid()
service_inbound_api.created_at == datetime.utcnow()
service_inbound_api.bearer_token = service_inbound_api.bearer_token
db.session.add(service_inbound_api)
@transactional
@version_class(ServiceInboundApi)
def reset_service_inbound_api(service_inbound_api, updated_by_id, url=None, bearer_token=None):
if url:
service_inbound_api.url = url
if bearer_token:
service_inbound_api.bearer_token = bearer_token
service_inbound_api.updated_by_id = updated_by_id
service_inbound_api.updated_at = datetime.utcnow()
db.session.add(service_inbound_api)
def get_service_inbound_api(service_inbound_api_id, service_id):
return ServiceInboundApi.query.filter_by(id=service_inbound_api_id,
service_id=service_id).first()
+44 -4
View File
@@ -22,7 +22,6 @@ from app.encryption import (
hashpw, hashpw,
check_hash check_hash
) )
from app.authentication.utils import get_secret
from app import ( from app import (
db, db,
encryption, encryption,
@@ -295,12 +294,46 @@ class ServiceWhitelist(db.Model):
return 'Recipient {} of type: {}'.format(self.recipient, self.recipient_type) return 'Recipient {} of type: {}'.format(self.recipient, self.recipient_type)
class ServiceInboundApi(db.Model, Versioned):
__tablename__ = 'service_inbound_api'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, nullable=False, unique=True)
service = db.relationship('Service', backref='inbound_api')
url = db.Column(db.String(), nullable=False)
_bearer_token = db.Column("bearer_token", db.String(), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, nullable=True)
updated_by = db.relationship('User')
updated_by_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), index=True, nullable=False)
@property
def bearer_token(self):
if self._bearer_token:
return encryption.decrypt(self._bearer_token)
return None
@bearer_token.setter
def bearer_token(self, bearer_token):
if bearer_token:
self._bearer_token = encryption.encrypt(str(bearer_token))
def serialize(self):
return {
"id": str(self.id),
"service_id": str(self.service_id),
"url": self.url,
"updated_by_id": str(self.updated_by_id),
"created_at": self.created_at.strftime(DATETIME_FORMAT),
"updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None
}
class ApiKey(db.Model, Versioned): class ApiKey(db.Model, Versioned):
__tablename__ = 'api_keys' __tablename__ = 'api_keys'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = db.Column(db.String(255), nullable=False) name = db.Column(db.String(255), nullable=False)
secret = db.Column(db.String(255), unique=True, nullable=False) _secret = db.Column("secret", db.String(255), unique=True, nullable=False)
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, nullable=False) service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, nullable=False)
service = db.relationship('Service', backref='api_keys') service = db.relationship('Service', backref='api_keys')
key_type = db.Column(db.String(255), db.ForeignKey('key_types.name'), index=True, nullable=False) key_type = db.Column(db.String(255), db.ForeignKey('key_types.name'), index=True, nullable=False)
@@ -325,8 +358,15 @@ class ApiKey(db.Model, Versioned):
) )
@property @property
def unsigned_secret(self): def secret(self):
return get_secret(self.secret) if self._secret:
return encryption.decrypt(self._secret)
return None
@secret.setter
def secret(self, secret):
if secret:
self._secret = encryption.encrypt(str(secret))
KEY_TYPE_NORMAL = 'normal' KEY_TYPE_NORMAL = 'normal'
+9 -1
View File
@@ -53,12 +53,20 @@ def build_error_message(errors):
fields.append({"error": "ValidationError", "message": field}) fields.append({"error": "ValidationError", "message": field})
message = { message = {
"status_code": 400, "status_code": 400,
"errors": fields "errors": unique_errors(fields)
} }
return json.dumps(message) return json.dumps(message)
def unique_errors(dups):
unique = []
for x in dups:
if x not in unique:
unique.append(x)
return unique
def __format_message(e): def __format_message(e):
def get_path(e): def get_path(e):
error_path = None error_path = None
+10
View File
@@ -18,3 +18,13 @@ personalisation = {
"code": "1001", # yet to be implemented "code": "1001", # yet to be implemented
"link": "link to our error documentation not yet implemented" "link": "link to our error documentation not yet implemented"
} }
https_url = {
"type": "string",
"format": "uri",
"pattern": "^https.*",
"validationMessage": "is not a valid https url",
"code": "1001", # yet to be implemented
"link": "link to our error documentation not yet implemented"
}
+1 -1
View File
@@ -338,7 +338,7 @@ class ApiKeySchema(BaseSchema):
class Meta: class Meta:
model = models.ApiKey model = models.ApiKey
exclude = ("service", "secret") exclude = ("service", "_secret")
strict = True strict = True
+61 -1
View File
@@ -8,6 +8,7 @@ from flask import (
current_app, current_app,
Blueprint Blueprint
) )
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from app import redis_store from app import redis_store
@@ -20,6 +21,11 @@ from app.dao.api_key_dao import (
expire_api_key) expire_api_key)
from app.dao.date_util import get_financial_year from app.dao.date_util import get_financial_year
from app.dao.notification_usage_dao import get_total_billable_units_for_sent_sms_notifications_in_date_range from app.dao.notification_usage_dao import get_total_billable_units_for_sent_sms_notifications_in_date_range
from app.dao.service_inbound_api_dao import (
save_service_inbound_api,
reset_service_inbound_api,
get_service_inbound_api
)
from app.dao.services_dao import ( from app.dao.services_dao import (
dao_fetch_service_by_id, dao_fetch_service_by_id,
dao_fetch_all_services, dao_fetch_all_services,
@@ -49,8 +55,10 @@ from app.errors import (
InvalidRequest, InvalidRequest,
register_errors register_errors
) )
from app.models import Service from app.models import Service, ServiceInboundApi
from app.schema_validation import validate
from app.service import statistics from app.service import statistics
from app.service.service_inbound_api_schema import service_inbound_api, update_service_inbound_api_schema
from app.service.utils import get_whitelist_objects from app.service.utils import get_whitelist_objects
from app.service.sender import send_notification_to_service_users from app.service.sender import send_notification_to_service_users
from app.schemas import ( from app.schemas import (
@@ -531,3 +539,55 @@ def get_yearly_monthly_usage(service_id):
return json.dumps(json_results) return json.dumps(json_results)
except TypeError: except TypeError:
return jsonify(result='error', message='No valid year provided'), 400 return jsonify(result='error', message='No valid year provided'), 400
@service_blueprint.route('/<uuid:service_id>/inbound-api', methods=['POST'])
def create_service_inbound_api(service_id):
data = request.get_json()
validate(data, service_inbound_api)
data["service_id"] = service_id
inbound_api = ServiceInboundApi(**data)
try:
save_service_inbound_api(inbound_api)
except SQLAlchemyError as e:
return handle_sql_errror(e)
return jsonify(data=inbound_api.serialize()), 201
@service_blueprint.route('/<uuid:service_id>/inbound-api/<uuid:inbound_api_id>', methods=['POST'])
def update_service_inbound_api(service_id, inbound_api_id):
data = request.get_json()
validate(data, update_service_inbound_api_schema)
to_update = get_service_inbound_api(inbound_api_id, service_id)
reset_service_inbound_api(service_inbound_api=to_update,
updated_by_id=data["updated_by_id"],
url=data.get("url", None),
bearer_token=data.get("bearer_token", None))
return jsonify(data=to_update.serialize()), 200
@service_blueprint.route('/<uuid:service_id>/inbound-api/<uuid:inbound_api_id>', methods=["GET"])
def fetch_service_inbound_api(service_id, inbound_api_id):
inbound_api = get_service_inbound_api(inbound_api_id, service_id)
return jsonify(data=inbound_api.serialize()), 200
def handle_sql_errror(e):
if hasattr(e, 'orig') and hasattr(e.orig, 'pgerror') and e.orig.pgerror \
and ('duplicate key value violates unique constraint "ix_service_inbound_api_service_id"'
in e.orig.pgerror):
return jsonify(
result='error',
message={'name': ["You can only have one URL and bearer token for your service."]}
), 400
elif hasattr(e, 'orig') and hasattr(e.orig, 'pgerror') and e.orig.pgerror \
and ('insert or update on table "service_inbound_api" violates '
'foreign key constraint "service_inbound_api_service_id_fkey"'
in e.orig.pgerror):
return jsonify(result='error', message="No result found"), 404
else:
raise e
+27
View File
@@ -0,0 +1,27 @@
from app.schema_validation.definitions import uuid, https_url
service_inbound_api = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST service inbound api schema",
"type": "object",
"title": "Create service inbound api",
"properties": {
"url": https_url,
"bearer_token": {"type": "string", "minLength": 10},
"updated_by_id": uuid
},
"required": ["url", "bearer_token", "updated_by_id"]
}
update_service_inbound_api_schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "POST service inbound api schema",
"type": "object",
"title": "Create service inbound api",
"properties": {
"url": https_url,
"bearer_token": {"type": "string", "minLength": 10},
"updated_by_id": uuid
},
"required": ["updated_by_id"]
}
@@ -0,0 +1,56 @@
"""empty message
Revision ID: 0098_service_inbound_api
Revises: 0097_notnull_inbound_provider
Create Date: 2017-06-13 15:02:33.609656
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = '0098_service_inbound_api'
down_revision = '0097_notnull_inbound_provider'
def upgrade():
op.create_table('service_inbound_api_history',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('url', sa.String(), nullable=False),
sa.Column('bearer_token', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('updated_by_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('version', sa.Integer(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', 'version')
)
op.create_index(op.f('ix_service_inbound_api_history_service_id'), 'service_inbound_api_history', ['service_id'],
unique=False)
op.create_index(op.f('ix_service_inbound_api_history_updated_by_id'), 'service_inbound_api_history',
['updated_by_id'], unique=False)
op.create_table('service_inbound_api',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('url', sa.String(), nullable=False),
sa.Column('bearer_token', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('updated_by_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('version', sa.Integer(), nullable=False),\
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
sa.ForeignKeyConstraint(['updated_by_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_service_inbound_api_service_id'), 'service_inbound_api', ['service_id'], unique=True)
op.create_index(op.f('ix_service_inbound_api_updated_by_id'), 'service_inbound_api', ['updated_by_id'],
unique=False)
def downgrade():
op.drop_index(op.f('ix_service_inbound_api_updated_by_id'), table_name='service_inbound_api')
op.drop_index(op.f('ix_service_inbound_api_service_id'), table_name='service_inbound_api')
op.drop_table('service_inbound_api')
op.drop_index(op.f('ix_service_inbound_api_history_updated_by_id'), table_name='service_inbound_api_history')
op.drop_index(op.f('ix_service_inbound_api_history_service_id'), table_name='service_inbound_api_history')
op.drop_table('service_inbound_api_history')
+2 -2
View File
@@ -11,7 +11,7 @@ def create_authorization_header(service_id=None, key_type=KEY_TYPE_NORMAL):
client_id = str(service_id) client_id = str(service_id)
secrets = ApiKey.query.filter_by(service_id=service_id, key_type=key_type).all() secrets = ApiKey.query.filter_by(service_id=service_id, key_type=key_type).all()
if secrets: if secrets:
secret = secrets[0].unsigned_secret secret = secrets[0].secret
else: else:
service = dao_fetch_service_by_id(service_id) service = dao_fetch_service_by_id(service_id)
data = { data = {
@@ -22,7 +22,7 @@ def create_authorization_header(service_id=None, key_type=KEY_TYPE_NORMAL):
} }
api_key = ApiKey(**data) api_key = ApiKey(**data)
save_model_api_key(api_key) save_model_api_key(api_key)
secret = api_key.unsigned_secret secret = api_key.secret
else: else:
client_id = current_app.config.get('ADMIN_CLIENT_USER_NAME') client_id = current_app.config.get('ADMIN_CLIENT_USER_NAME')
-8
View File
@@ -1,8 +0,0 @@
from app.authentication.utils import generate_secret, get_secret
def test_secret_is_signed_and_can_be_read_again(notify_api):
with notify_api.test_request_context():
signed_secret = generate_secret('some_uuid')
assert signed_secret != 'some_uuid'
assert 'some_uuid' == get_secret(signed_secret)
+5 -5
View File
@@ -4,7 +4,7 @@ import pytest
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from app.authentication.utils import get_secret from app import encryption
from app.dao.api_key_dao import (save_model_api_key, from app.dao.api_key_dao import (save_model_api_key,
get_model_api_keys, get_model_api_keys,
get_unsigned_secrets, get_unsigned_secrets,
@@ -63,14 +63,14 @@ def test_should_return_api_key_for_service(notify_api, notify_db, notify_db_sess
def test_should_return_unsigned_api_keys_for_service_id(sample_api_key): def test_should_return_unsigned_api_keys_for_service_id(sample_api_key):
unsigned_api_key = get_unsigned_secrets(sample_api_key.service_id) unsigned_api_key = get_unsigned_secrets(sample_api_key.service_id)
assert len(unsigned_api_key) == 1 assert len(unsigned_api_key) == 1
assert sample_api_key.secret != unsigned_api_key[0] assert sample_api_key._secret != unsigned_api_key[0]
assert unsigned_api_key[0] == get_secret(sample_api_key.secret) assert unsigned_api_key[0] == sample_api_key.secret
def test_get_unsigned_secret_returns_key(sample_api_key): def test_get_unsigned_secret_returns_key(sample_api_key):
unsigned_api_key = get_unsigned_secret(sample_api_key.id) unsigned_api_key = get_unsigned_secret(sample_api_key.id)
assert sample_api_key.secret != unsigned_api_key assert sample_api_key._secret != unsigned_api_key
assert unsigned_api_key == get_secret(sample_api_key.secret) assert unsigned_api_key == sample_api_key.secret
def test_should_not_allow_duplicate_key_names_per_service(sample_api_key, fake_uuid): def test_should_not_allow_duplicate_key_names_per_service(sample_api_key, fake_uuid):
@@ -0,0 +1,117 @@
import uuid
import pytest
from sqlalchemy.exc import SQLAlchemyError
from app import encryption
from app.dao.service_inbound_api_dao import (
save_service_inbound_api,
reset_service_inbound_api,
get_service_inbound_api
)
from app.models import ServiceInboundApi
def test_save_service_inbound_api(sample_service):
service_inbound_api = ServiceInboundApi(
service_id=sample_service.id,
url="https://some_service/inbound_messages",
bearer_token="some_unique_string",
updated_by_id=sample_service.users[0].id
)
save_service_inbound_api(service_inbound_api)
results = ServiceInboundApi.query.all()
assert len(results) == 1
inbound_api = results[0]
assert inbound_api.id is not None
assert inbound_api.service_id == sample_service.id
assert inbound_api.updated_by_id == sample_service.users[0].id
assert inbound_api.url == "https://some_service/inbound_messages"
assert inbound_api.bearer_token == "some_unique_string"
assert inbound_api._bearer_token != "some_unique_string"
assert inbound_api.updated_at is None
versioned = ServiceInboundApi.get_history_model().query.filter_by(id=inbound_api.id).one()
assert versioned.id == inbound_api.id
assert versioned.service_id == sample_service.id
assert versioned.updated_by_id == sample_service.users[0].id
assert versioned.url == "https://some_service/inbound_messages"
assert encryption.decrypt(versioned._bearer_token) == "some_unique_string"
assert versioned.updated_at is None
assert versioned.version == 1
def test_save_service_inbound_api_fails_if_service_does_not_exist(notify_db, notify_db_session):
service_inbound_api = ServiceInboundApi(
service_id=uuid.uuid4(),
url="https://some_service/inbound_messages",
bearer_token="some_unique_string",
updated_by_id=uuid.uuid4()
)
with pytest.raises(SQLAlchemyError):
save_service_inbound_api(service_inbound_api)
def test_update_service_inbound_api(sample_service):
service_inbound_api = ServiceInboundApi(
service_id=sample_service.id,
url="https://some_service/inbound_messages",
bearer_token="some_unique_string",
updated_by_id=sample_service.users[0].id
)
save_service_inbound_api(service_inbound_api)
results = ServiceInboundApi.query.all()
assert len(results) == 1
saved_inbound_api = results[0]
reset_service_inbound_api(saved_inbound_api, updated_by_id=sample_service.users[0].id,
url="https://some_service/changed_url")
updated_results = ServiceInboundApi.query.all()
assert len(updated_results) == 1
updated = updated_results[0]
assert updated.id is not None
assert updated.service_id == sample_service.id
assert updated.updated_by_id == sample_service.users[0].id
assert updated.url == "https://some_service/changed_url"
assert updated.bearer_token == "some_unique_string"
assert updated._bearer_token != "some_unique_string"
assert updated.updated_at is not None
versioned_results = ServiceInboundApi.get_history_model().query.filter_by(id=saved_inbound_api.id).all()
assert len(versioned_results) == 2
for x in versioned_results:
if x.version == 1:
assert x.url == "https://some_service/inbound_messages"
assert not x.updated_at
elif x.version == 2:
assert x.url == "https://some_service/changed_url"
assert x.updated_at
else:
pytest.fail("version should not exist")
assert x.id is not None
assert x.service_id == sample_service.id
assert x.updated_by_id == sample_service.users[0].id
assert encryption.decrypt(x._bearer_token) == "some_unique_string"
def test_get_service_inbound_api(sample_service):
service_inbound_api = ServiceInboundApi(
service_id=sample_service.id,
url="https://some_service/inbound_messages",
bearer_token="some_unique_string",
updated_by_id=sample_service.users[0].id
)
save_service_inbound_api(service_inbound_api)
inbound_api = get_service_inbound_api(service_inbound_api.id, sample_service.id)
assert inbound_api.id is not None
assert inbound_api.service_id == sample_service.id
assert inbound_api.updated_by_id == sample_service.users[0].id
assert inbound_api.url == "https://some_service/inbound_messages"
assert inbound_api.bearer_token == "some_unique_string"
assert inbound_api._bearer_token != "some_unique_string"
assert inbound_api.updated_at is None
+16 -3
View File
@@ -2,10 +2,9 @@ from datetime import datetime
import uuid import uuid
from app.dao.inbound_sms_dao import dao_create_inbound_sms
from app.dao.jobs_dao import dao_create_job from app.dao.jobs_dao import dao_create_job
from app.dao.service_inbound_api_dao import save_service_inbound_api
from app.models import ( from app.models import (
InboundSms,
Service, Service,
User, User,
Template, Template,
@@ -17,7 +16,7 @@ from app.models import (
EMAIL_TYPE, EMAIL_TYPE,
SMS_TYPE, SMS_TYPE,
KEY_TYPE_NORMAL, KEY_TYPE_NORMAL,
) ServiceInboundApi)
from app.dao.users_dao import save_model_user from app.dao.users_dao import save_model_user
from app.dao.notifications_dao import dao_create_notification, dao_created_scheduled_notification from app.dao.notifications_dao import dao_create_notification, dao_created_scheduled_notification
from app.dao.templates_dao import dao_create_template from app.dao.templates_dao import dao_create_template
@@ -212,3 +211,17 @@ def create_inbound_sms(
) )
dao_create_inbound_sms(inbound) dao_create_inbound_sms(inbound)
return inbound return inbound
def create_service_inbound_api(
service,
url="https://something.com",
bearer_token="some_super_secret",
):
service_inbound_api = ServiceInboundApi(service_id=service.id,
url=url,
bearer_token=bearer_token,
updated_by_id=service.users[0].id
)
save_service_inbound_api(service_inbound_api)
return service_inbound_api
@@ -321,7 +321,6 @@ def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
data = { data = {
'to': '07700 900 855', 'to': '07700 900 855',
@@ -374,7 +373,6 @@ def test_should_allow_valid_email_notification(notify_api, sample_email_template
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
mocker.patch('app.celery.provider_tasks.deliver_email.apply_async') mocker.patch('app.celery.provider_tasks.deliver_email.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
data = { data = {
'to': 'ok@ok.com', 'to': 'ok@ok.com',
@@ -411,7 +409,6 @@ def test_should_block_api_call_if_over_day_limit_for_live_service(
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
mocker.patch('app.celery.provider_tasks.deliver_email.apply_async') mocker.patch('app.celery.provider_tasks.deliver_email.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
service = create_sample_service(notify_db, notify_db_session, limit=1, restricted=False) service = create_sample_service(notify_db, notify_db_session, limit=1, restricted=False)
email_template = create_sample_email_template(notify_db, notify_db_session, service=service) email_template = create_sample_email_template(notify_db, notify_db_session, service=service)
@@ -443,7 +440,6 @@ def test_should_block_api_call_if_over_day_limit_for_restricted_service(
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
service = create_sample_service(notify_db, notify_db_session, limit=1, restricted=True) service = create_sample_service(notify_db, notify_db_session, limit=1, restricted=True)
email_template = create_sample_email_template(notify_db, notify_db_session, service=service) email_template = create_sample_email_template(notify_db, notify_db_session, service=service)
@@ -479,7 +475,6 @@ def test_should_allow_api_call_if_under_day_limit_regardless_of_type(
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
service = create_sample_service(notify_db, notify_db_session, limit=2, restricted=restricted) service = create_sample_service(notify_db, notify_db_session, limit=2, restricted=restricted)
email_template = create_sample_email_template(notify_db, notify_db_session, service=service) email_template = create_sample_email_template(notify_db, notify_db_session, service=service)
@@ -586,7 +581,7 @@ def test_should_send_email_if_team_api_key_and_a_service_user(notify_api, sample
created_by=sample_email_template.created_by, created_by=sample_email_template.created_by,
key_type=KEY_TYPE_TEAM) key_type=KEY_TYPE_TEAM)
save_model_api_key(api_key) save_model_api_key(api_key)
auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) auth_header = create_jwt_token(secret=api_key.secret, client_id=str(api_key.service_id))
response = client.post( response = client.post(
path='/notifications/email', path='/notifications/email',
@@ -619,7 +614,7 @@ def test_should_send_sms_to_anyone_with_test_key(
key_type=KEY_TYPE_TEST key_type=KEY_TYPE_TEST
) )
save_model_api_key(api_key) save_model_api_key(api_key)
auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) auth_header = create_jwt_token(secret=api_key.secret, client_id=str(api_key.service_id))
response = client.post( response = client.post(
path='/notifications/sms', path='/notifications/sms',
@@ -654,7 +649,7 @@ def test_should_send_email_to_anyone_with_test_key(
key_type=KEY_TYPE_TEST key_type=KEY_TYPE_TEST
) )
save_model_api_key(api_key) save_model_api_key(api_key)
auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) auth_header = create_jwt_token(secret=api_key.secret, client_id=str(api_key.service_id))
response = client.post( response = client.post(
path='/notifications/email', path='/notifications/email',
@@ -682,7 +677,7 @@ def test_should_send_sms_if_team_api_key_and_a_service_user(notify_api, sample_t
created_by=sample_template.created_by, created_by=sample_template.created_by,
key_type=KEY_TYPE_TEAM) key_type=KEY_TYPE_TEAM)
save_model_api_key(api_key) save_model_api_key(api_key)
auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) auth_header = create_jwt_token(secret=api_key.secret, client_id=str(api_key.service_id))
response = client.post( response = client.post(
path='/notifications/sms', path='/notifications/sms',
@@ -715,7 +710,7 @@ def test_should_persist_notification(notify_api, sample_template,
created_by=template.created_by, created_by=template.created_by,
key_type=KEY_TYPE_TEAM) key_type=KEY_TYPE_TEAM)
save_model_api_key(api_key) save_model_api_key(api_key)
auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) auth_header = create_jwt_token(secret=api_key.secret, client_id=str(api_key.service_id))
response = client.post( response = client.post(
path='/notifications/{}'.format(template_type), path='/notifications/{}'.format(template_type),
@@ -758,7 +753,7 @@ def test_should_delete_notification_and_return_error_if_sqs_fails(
created_by=template.created_by, created_by=template.created_by,
key_type=KEY_TYPE_TEAM) key_type=KEY_TYPE_TEAM)
save_model_api_key(api_key) save_model_api_key(api_key)
auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) auth_header = create_jwt_token(secret=api_key.secret, client_id=str(api_key.service_id))
response = client.post( response = client.post(
path='/notifications/{}'.format(template_type), path='/notifications/{}'.format(template_type),
@@ -862,7 +857,7 @@ def test_should_not_send_notification_to_non_whitelist_recipient_in_trial_mode(
} }
api_key = create_sample_api_key(notify_db, notify_db_session, service, key_type=key_type) api_key = create_sample_api_key(notify_db, notify_db_session, service, key_type=key_type)
auth_header = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) auth_header = create_jwt_token(secret=api_key.secret, client_id=str(api_key.service_id))
response = client.post( response = client.post(
path='/notifications/{}'.format(notification_type), path='/notifications/{}'.format(notification_type),
@@ -923,7 +918,7 @@ def test_should_send_notification_to_whitelist_recipient(
} }
sample_key = create_sample_api_key(notify_db, notify_db_session, service, key_type=key_type) sample_key = create_sample_api_key(notify_db, notify_db_session, service, key_type=key_type)
auth_header = create_jwt_token(secret=sample_key.unsigned_secret, client_id=str(sample_key.service_id)) auth_header = create_jwt_token(secret=sample_key.secret, client_id=str(sample_key.service_id))
response = client.post( response = client.post(
path='/notifications/{}'.format(notification_type), path='/notifications/{}'.format(notification_type),
@@ -1101,7 +1096,6 @@ def test_returns_a_429_limit_exceeded_if_rate_limit_exceeded(
def test_should_allow_store_original_number_on_sms_notification(client, sample_template, mocker): def test_should_allow_store_original_number_on_sms_notification(client, sample_template, mocker):
mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
data = { data = {
'to': '+(44) 7700-900 855', 'to': '+(44) 7700-900 855',
@@ -1128,7 +1122,6 @@ def test_should_allow_store_original_number_on_sms_notification(client, sample_t
def test_should_not_allow_international_number_on_sms_notification(client, sample_template, mocker): def test_should_not_allow_international_number_on_sms_notification(client, sample_template, mocker):
mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
data = { data = {
'to': '20-12-1234-1234', 'to': '20-12-1234-1234',
@@ -1151,7 +1144,6 @@ def test_should_not_allow_international_number_on_sms_notification(client, sampl
def test_should_allow_international_number_on_sms_notification(client, notify_db, notify_db_session, mocker): def test_should_allow_international_number_on_sms_notification(client, notify_db, notify_db_session, mocker):
mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
service = sample_service(notify_db, notify_db_session, can_send_international_sms=True) service = sample_service(notify_db, notify_db_session, can_send_international_sms=True)
template = create_sample_template(notify_db, notify_db_session, service=service) template = create_sample_template(notify_db, notify_db_session, service=service)
+1 -1
View File
@@ -741,5 +741,5 @@ def test_get_notification_selects_correct_template_for_personalisation(client,
def _create_auth_header_from_key(api_key): def _create_auth_header_from_key(api_key):
token = create_jwt_token(secret=api_key.unsigned_secret, client_id=str(api_key.service_id)) token = create_jwt_token(secret=api_key.secret, client_id=str(api_key.service_id))
return [('Authorization', 'Bearer {}'.format(token))] return [('Authorization', 'Bearer {}'.format(token))]
@@ -21,7 +21,6 @@ def _post_notification(client, template, url, to):
def test_post_sms_contract(client, mocker, sample_template): def test_post_sms_contract(client, mocker, sample_template):
mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
response_json = return_json_from_response(_post_notification( response_json = return_json_from_response(_post_notification(
client, sample_template, url='/notifications/sms', to='07700 900 855' client, sample_template, url='/notifications/sms', to='07700 900 855'
@@ -31,7 +30,6 @@ def test_post_sms_contract(client, mocker, sample_template):
def test_post_email_contract(client, mocker, sample_email_template): def test_post_email_contract(client, mocker, sample_email_template):
mocker.patch('app.celery.provider_tasks.deliver_email.apply_async') mocker.patch('app.celery.provider_tasks.deliver_email.apply_async')
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
response_json = return_json_from_response(_post_notification( response_json = return_json_from_response(_post_notification(
client, sample_email_template, url='/notifications/email', to='foo@bar.com' client, sample_email_template, url='/notifications/email', to='foo@bar.com'
+80 -2
View File
@@ -8,6 +8,7 @@ import pytest
from flask import url_for, current_app from flask import url_for, current_app
from freezegun import freeze_time from freezegun import freeze_time
from app import encryption
from app.dao.users_dao import save_model_user from app.dao.users_dao import save_model_user
from app.dao.services_dao import dao_remove_user_from_service from app.dao.services_dao import dao_remove_user_from_service
from app.models import ( from app.models import (
@@ -17,7 +18,7 @@ from app.models import (
DVLA_ORG_LAND_REGISTRY DVLA_ORG_LAND_REGISTRY
) )
from tests import create_authorization_header from tests import create_authorization_header
from tests.app.db import create_template from tests.app.db import create_template, create_service_inbound_api
from tests.app.conftest import ( from tests.app.conftest import (
sample_service as create_service, sample_service as create_service,
sample_user_service_permission as create_user_service_permission, sample_user_service_permission as create_user_service_permission,
@@ -27,7 +28,6 @@ from tests.app.conftest import (
) )
from tests.app.db import create_user from tests.app.db import create_user
from tests.conftest import set_config_values
def test_get_service_list(client, service_factory): def test_get_service_list(client, service_factory):
@@ -2149,3 +2149,81 @@ def test_search_for_notification_by_to_field_returns_content(
assert notifications[0]['id'] == str(notification.id) assert notifications[0]['id'] == str(notification.id)
assert notifications[0]['to'] == '+447700900855' assert notifications[0]['to'] == '+447700900855'
assert notifications[0]['body'] == 'Hello Foo\nYour thing is due soon' assert notifications[0]['body'] == 'Hello Foo\nYour thing is due soon'
def test_create_service_inbound_api(client, sample_service):
data = {
"url": "https://some_service/inbound-sms",
"bearer_token": "some-unique-string",
"updated_by_id": str(sample_service.users[0].id)
}
response = client.post(
'/service/{}/inbound-api'.format(sample_service.id),
data=json.dumps(data),
headers=[('Content-Type', 'application/json'), create_authorization_header()]
)
assert response.status_code == 201
resp_json = json.loads(response.get_data(as_text=True))["data"]
assert resp_json["id"]
assert resp_json["service_id"] == str(sample_service.id)
assert resp_json["url"] == "https://some_service/inbound-sms"
assert resp_json["updated_by_id"] == str(sample_service.users[0].id)
assert resp_json["created_at"]
assert not resp_json["updated_at"]
def test_set_service_inbound_api_raises_404_when_service_does_not_exist(client):
data = {
"url": "https://some_service/inbound-sms",
"bearer_token": "some-unique-string",
"updated_by_id": str(uuid.uuid4())
}
response = client.post(
'/service/{}/inbound-api'.format(uuid.uuid4()),
data=json.dumps(data),
headers=[('Content-Type', 'application/json'), create_authorization_header()]
)
assert response.status_code == 404
assert json.loads(response.get_data(as_text=True))['message'] == 'No result found'
def test_update_service_inbound_api_updates_url(client, sample_service):
service_inbound_api = create_service_inbound_api(service=sample_service,
url="https://original_url.com")
data = {
"url": "https://another_url.com",
"updated_by_id": str(sample_service.users[0].id)
}
response = client.post("/service/{}/inbound-api/{}".format(sample_service.id, service_inbound_api.id),
data=json.dumps(data),
headers=[('Content-Type', 'application/json'), create_authorization_header()])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))["data"]
assert resp_json["url"] == "https://another_url.com"
assert service_inbound_api.url == "https://another_url.com"
def test_update_service_inbound_api_updates_bearer_token(client, sample_service):
service_inbound_api = create_service_inbound_api(service=sample_service,
bearer_token="some_super_secret")
data = {
"bearer_token": "different_token",
"updated_by_id": str(sample_service.users[0].id)
}
response = client.post("/service/{}/inbound-api/{}".format(sample_service.id, service_inbound_api.id),
data=json.dumps(data),
headers=[('Content-Type', 'application/json'), create_authorization_header()])
assert response.status_code == 200
assert service_inbound_api.bearer_token == "different_token"
def test_fetch_service_inbound_api(client, sample_service):
service_inbound_api = create_service_inbound_api(service=sample_service)
response = client.get("/service/{}/inbound-api/{}".format(sample_service.id, service_inbound_api.id),
headers=[create_authorization_header()])
assert response.status_code == 200
assert json.loads(response.get_data(as_text=True))["data"] == service_inbound_api.serialize()
+45
View File
@@ -0,0 +1,45 @@
import json
import uuid
import pytest
from jsonschema import ValidationError
from app.schema_validation import validate
from app.service.service_inbound_api_schema import service_inbound_api
def test_service_inbound_api_schema_validates():
under_test = {"url": "https://some_url.for_service",
"bearer_token": "something_ten_chars",
"updated_by_id": str(uuid.uuid4())
}
validated = validate(under_test, service_inbound_api)
assert validated == under_test
@pytest.mark.parametrize("url", ["not a url", "https not a url", "http://valid.com"])
def test_service_inbound_api_schema_errors_for_url_not_valid_url(url):
under_test = {"url": url,
"bearer_token": "something_ten_chars",
"updated_by_id": str(uuid.uuid4())
}
with pytest.raises(ValidationError) as e:
validate(under_test, service_inbound_api)
errors = json.loads(str(e.value)).get('errors')
assert len(errors) == 1
assert errors[0]['message'] == "url is not a valid https url"
def test_service_inbound_api_schema_bearer_token_under_ten_char():
under_test = {"url": "https://some_url.for_service",
"bearer_token": "shorty",
"updated_by_id": str(uuid.uuid4())
}
with pytest.raises(ValidationError) as e:
validate(under_test, service_inbound_api)
errors = json.loads(str(e.value)).get('errors')
assert len(errors) == 1
assert errors[0]['message'] == "bearer_token shorty is too short"