mirror of
https://github.com/GSA/notifications-api.git
synced 2026-09-11 10:28:55 -04:00
Merge pull request #1034 from alphagov/push-inbound-sms
Push inbound sms
This commit is contained in:
@@ -73,7 +73,7 @@ def requires_auth():
|
||||
|
||||
for api_key in service.api_keys:
|
||||
try:
|
||||
get_decode_errors(auth_token, api_key.unsigned_secret)
|
||||
get_decode_errors(auth_token, api_key.secret)
|
||||
except TokenDecodeError:
|
||||
continue
|
||||
|
||||
|
||||
@@ -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'))
|
||||
@@ -1,14 +1,13 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from app import db
|
||||
from app import db, encryption
|
||||
from app.models import ApiKey
|
||||
|
||||
from app.dao.dao_utils import (
|
||||
transactional,
|
||||
version_class
|
||||
)
|
||||
from app.authentication.utils import generate_secret
|
||||
|
||||
|
||||
@transactional
|
||||
@@ -16,7 +15,7 @@ from app.authentication.utils import generate_secret
|
||||
def save_model_api_key(api_key):
|
||||
if not api_key.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)
|
||||
|
||||
|
||||
@@ -39,7 +38,7 @@ def get_unsigned_secrets(service_id):
|
||||
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()
|
||||
keys = [x.unsigned_secret for x in api_keys]
|
||||
keys = [x.secret for x in api_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.
|
||||
"""
|
||||
api_key = ApiKey.query.filter_by(id=key_id, expiry_date=None).one()
|
||||
return api_key.unsigned_secret
|
||||
return api_key.secret
|
||||
|
||||
@@ -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
@@ -22,7 +22,6 @@ from app.encryption import (
|
||||
hashpw,
|
||||
check_hash
|
||||
)
|
||||
from app.authentication.utils import get_secret
|
||||
from app import (
|
||||
db,
|
||||
encryption,
|
||||
@@ -295,12 +294,46 @@ class ServiceWhitelist(db.Model):
|
||||
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):
|
||||
__tablename__ = 'api_keys'
|
||||
|
||||
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
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 = db.relationship('Service', backref='api_keys')
|
||||
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
|
||||
def unsigned_secret(self):
|
||||
return get_secret(self.secret)
|
||||
def secret(self):
|
||||
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'
|
||||
|
||||
@@ -53,12 +53,20 @@ def build_error_message(errors):
|
||||
fields.append({"error": "ValidationError", "message": field})
|
||||
message = {
|
||||
"status_code": 400,
|
||||
"errors": fields
|
||||
"errors": unique_errors(fields)
|
||||
}
|
||||
|
||||
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 get_path(e):
|
||||
error_path = None
|
||||
|
||||
@@ -18,3 +18,13 @@ personalisation = {
|
||||
"code": "1001", # yet to be 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
@@ -338,7 +338,7 @@ class ApiKeySchema(BaseSchema):
|
||||
|
||||
class Meta:
|
||||
model = models.ApiKey
|
||||
exclude = ("service", "secret")
|
||||
exclude = ("service", "_secret")
|
||||
strict = True
|
||||
|
||||
|
||||
|
||||
+61
-1
@@ -8,6 +8,7 @@ from flask import (
|
||||
current_app,
|
||||
Blueprint
|
||||
)
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from app import redis_store
|
||||
@@ -20,6 +21,11 @@ from app.dao.api_key_dao import (
|
||||
expire_api_key)
|
||||
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.service_inbound_api_dao import (
|
||||
save_service_inbound_api,
|
||||
reset_service_inbound_api,
|
||||
get_service_inbound_api
|
||||
)
|
||||
from app.dao.services_dao import (
|
||||
dao_fetch_service_by_id,
|
||||
dao_fetch_all_services,
|
||||
@@ -49,8 +55,10 @@ from app.errors import (
|
||||
InvalidRequest,
|
||||
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.service_inbound_api_schema import service_inbound_api, update_service_inbound_api_schema
|
||||
from app.service.utils import get_whitelist_objects
|
||||
from app.service.sender import send_notification_to_service_users
|
||||
from app.schemas import (
|
||||
@@ -531,3 +539,55 @@ def get_yearly_monthly_usage(service_id):
|
||||
return json.dumps(json_results)
|
||||
except TypeError:
|
||||
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
|
||||
|
||||
@@ -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
@@ -11,7 +11,7 @@ def create_authorization_header(service_id=None, key_type=KEY_TYPE_NORMAL):
|
||||
client_id = str(service_id)
|
||||
secrets = ApiKey.query.filter_by(service_id=service_id, key_type=key_type).all()
|
||||
if secrets:
|
||||
secret = secrets[0].unsigned_secret
|
||||
secret = secrets[0].secret
|
||||
else:
|
||||
service = dao_fetch_service_by_id(service_id)
|
||||
data = {
|
||||
@@ -22,7 +22,7 @@ def create_authorization_header(service_id=None, key_type=KEY_TYPE_NORMAL):
|
||||
}
|
||||
api_key = ApiKey(**data)
|
||||
save_model_api_key(api_key)
|
||||
secret = api_key.unsigned_secret
|
||||
secret = api_key.secret
|
||||
|
||||
else:
|
||||
client_id = current_app.config.get('ADMIN_CLIENT_USER_NAME')
|
||||
|
||||
@@ -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)
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
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,
|
||||
get_model_api_keys,
|
||||
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):
|
||||
unsigned_api_key = get_unsigned_secrets(sample_api_key.service_id)
|
||||
assert len(unsigned_api_key) == 1
|
||||
assert sample_api_key.secret != unsigned_api_key[0]
|
||||
assert unsigned_api_key[0] == get_secret(sample_api_key.secret)
|
||||
assert sample_api_key._secret != unsigned_api_key[0]
|
||||
assert unsigned_api_key[0] == sample_api_key.secret
|
||||
|
||||
|
||||
def test_get_unsigned_secret_returns_key(sample_api_key):
|
||||
unsigned_api_key = get_unsigned_secret(sample_api_key.id)
|
||||
assert sample_api_key.secret != unsigned_api_key
|
||||
assert unsigned_api_key == get_secret(sample_api_key.secret)
|
||||
assert sample_api_key._secret != unsigned_api_key
|
||||
assert unsigned_api_key == sample_api_key.secret
|
||||
|
||||
|
||||
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
@@ -2,10 +2,9 @@ from datetime import datetime
|
||||
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.service_inbound_api_dao import save_service_inbound_api
|
||||
from app.models import (
|
||||
InboundSms,
|
||||
Service,
|
||||
User,
|
||||
Template,
|
||||
@@ -17,7 +16,7 @@ from app.models import (
|
||||
EMAIL_TYPE,
|
||||
SMS_TYPE,
|
||||
KEY_TYPE_NORMAL,
|
||||
)
|
||||
ServiceInboundApi)
|
||||
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.templates_dao import dao_create_template
|
||||
@@ -212,3 +211,17 @@ def create_inbound_sms(
|
||||
)
|
||||
dao_create_inbound_sms(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_client() as client:
|
||||
mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
|
||||
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
|
||||
|
||||
data = {
|
||||
'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_client() as client:
|
||||
mocker.patch('app.celery.provider_tasks.deliver_email.apply_async')
|
||||
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
|
||||
|
||||
data = {
|
||||
'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_client() as client:
|
||||
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)
|
||||
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_client() as client:
|
||||
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)
|
||||
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_client() as client:
|
||||
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)
|
||||
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,
|
||||
key_type=KEY_TYPE_TEAM)
|
||||
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(
|
||||
path='/notifications/email',
|
||||
@@ -619,7 +614,7 @@ def test_should_send_sms_to_anyone_with_test_key(
|
||||
key_type=KEY_TYPE_TEST
|
||||
)
|
||||
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(
|
||||
path='/notifications/sms',
|
||||
@@ -654,7 +649,7 @@ def test_should_send_email_to_anyone_with_test_key(
|
||||
key_type=KEY_TYPE_TEST
|
||||
)
|
||||
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(
|
||||
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,
|
||||
key_type=KEY_TYPE_TEAM)
|
||||
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(
|
||||
path='/notifications/sms',
|
||||
@@ -715,7 +710,7 @@ def test_should_persist_notification(notify_api, sample_template,
|
||||
created_by=template.created_by,
|
||||
key_type=KEY_TYPE_TEAM)
|
||||
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(
|
||||
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,
|
||||
key_type=KEY_TYPE_TEAM)
|
||||
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(
|
||||
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)
|
||||
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(
|
||||
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)
|
||||
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(
|
||||
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):
|
||||
mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
|
||||
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
|
||||
|
||||
data = {
|
||||
'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):
|
||||
mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
|
||||
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
|
||||
|
||||
data = {
|
||||
'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):
|
||||
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)
|
||||
template = create_sample_template(notify_db, notify_db_session, service=service)
|
||||
|
||||
@@ -741,5 +741,5 @@ def test_get_notification_selects_correct_template_for_personalisation(client,
|
||||
|
||||
|
||||
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))]
|
||||
|
||||
@@ -21,7 +21,6 @@ def _post_notification(client, template, url, to):
|
||||
|
||||
def test_post_sms_contract(client, mocker, sample_template):
|
||||
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(
|
||||
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):
|
||||
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(
|
||||
client, sample_email_template, url='/notifications/email', to='foo@bar.com'
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytest
|
||||
from flask import url_for, current_app
|
||||
from freezegun import freeze_time
|
||||
|
||||
from app import encryption
|
||||
from app.dao.users_dao import save_model_user
|
||||
from app.dao.services_dao import dao_remove_user_from_service
|
||||
from app.models import (
|
||||
@@ -17,7 +18,7 @@ from app.models import (
|
||||
DVLA_ORG_LAND_REGISTRY
|
||||
)
|
||||
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 (
|
||||
sample_service as create_service,
|
||||
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.conftest import set_config_values
|
||||
|
||||
|
||||
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]['to'] == '+447700900855'
|
||||
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()
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user