Implementation of api key pages.

Revoke page will show the correct key name
Show api keys shows a well formatted expiry date
Fix tests for api key endpoints.
This commit is contained in:
Rebecca Law
2016-01-21 12:28:05 +00:00
parent 41c775cd68
commit 90fca93308
8 changed files with 127 additions and 42 deletions

View File

@@ -1,6 +1,7 @@
import os import os
import re import re
import dateutil
from flask import Flask, session, Markup, escape, render_template from flask import Flask, session, Markup, escape, render_template
from flask._compat import string_types from flask._compat import string_types
from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.sqlalchemy import SQLAlchemy
@@ -54,6 +55,7 @@ def create_app(config_name, config_overrides=None):
application.add_template_filter(placeholders) application.add_template_filter(placeholders)
application.add_template_filter(replace_placeholders) application.add_template_filter(replace_placeholders)
application.add_template_filter(nl2br) application.add_template_filter(nl2br)
application.add_template_filter(format_datetime)
application.after_request(useful_headers_after_request) application.after_request(useful_headers_after_request)
register_errorhandlers(application) register_errorhandlers(application)
@@ -134,6 +136,12 @@ def replace_placeholders(template, values):
)) ))
def format_datetime(date):
date = dateutil.parser.parse(date)
native = date.replace(tzinfo=None)
return native.strftime('%A %d %B %Y at %H:%M')
# https://www.owasp.org/index.php/List_of_useful_HTTP_headers # https://www.owasp.org/index.php/List_of_useful_HTTP_headers
def useful_headers_after_request(response): def useful_headers_after_request(response):
response.headers.add('X-Frame-Options', 'deny') response.headers.add('X-Frame-Options', 'deny')

View File

@@ -36,18 +36,17 @@ def create_api_key(service_id):
) )
@main.route("/services/<int:service_id>/api-keys/show")
@login_required
def show_api_key(service_id):
return render_template('views/api-keys/show.html', service_id=service_id)
@main.route("/services/<int:service_id>/api-keys/revoke/<int:key_id>", methods=['GET', 'POST']) @main.route("/services/<int:service_id>/api-keys/revoke/<int:key_id>", methods=['GET', 'POST'])
@login_required @login_required
def revoke_api_key(service_id, key_id): def revoke_api_key(service_id, key_id):
key_name = api_key_api_client.get_api_keys(service_id=service_id, key_id=key_id)['apiKeys'][0]['name']
if request.method == 'GET': if request.method == 'GET':
return render_template('views/api-keys/revoke.html', service_id=service_id) return render_template(
'views/api-keys/revoke.html',
service_id=service_id,
key_name=key_name
)
elif request.method == 'POST': elif request.method == 'POST':
api_key_api_client.revoke_api_key(service_id=service_id, key_id=key_id) api_key_api_client.revoke_api_key(service_id=service_id, key_id=key_id)
flash('Test key 1 was revoked') flash('{} was revoked'.format(key_name))
return redirect(url_for('.api_keys', service_id=service_id)) return redirect(url_for('.api_keys', service_id=service_id))

View File

@@ -12,8 +12,11 @@ class ApiKeyApiClient(BaseAPIClient):
self.client_id = app.config['ADMIN_CLIENT_USER_NAME'] self.client_id = app.config['ADMIN_CLIENT_USER_NAME']
self.secret = app.config['ADMIN_CLIENT_SECRET'] self.secret = app.config['ADMIN_CLIENT_SECRET']
def get_api_keys(self, service_id, *params): def get_api_keys(self, service_id, key_id=None, *params):
return self.get(url='/service/{}/api-keys'.format(service_id)) if key_id:
return self.get(url='/service/{}/api-keys/{}'.format(service_id, key_id))
else:
return self.get(url='/service/{}/api-keys'.format(service_id))
def create_api_key(self, service_id, key_name, *params): def create_api_key(self, service_id, key_name, *params):
data = {"name": key_name} data = {"name": key_name}

View File

@@ -42,7 +42,7 @@
{% endcall %} {% endcall %}
{% if item.expiry_date %} {% if item.expiry_date %}
{% call field(align='right', status='default') %} {% call field(align='right', status='default') %}
Revoked {{ item.expiry_date }} Revoked {{ item.expiry_date|format_datetime }}
{% endcall %} {% endcall %}
{% else %} {% else %}
{% call field(align='right', status='error') %} {% call field(align='right', status='error') %}

View File

@@ -16,7 +16,7 @@
</h1> </h1>
<p> <p>
Test key 1 will no longer let you connect to GOV.UK Notify. {{ key_name }} will no longer let you connect to GOV.UK Notify.
</p> </p>
<p> <p>
You cant undo this. You cant undo this.

View File

@@ -39,6 +39,12 @@ def template_json(id_, name, type_, content, service_id):
} }
def api_key_json(id_, name, expiry_date=None):
return {'id': id_,
'name': name,
'expiry_date': expiry_date
}
TEST_USER_EMAIL = 'test@user.gov.uk' TEST_USER_EMAIL = 'test@user.gov.uk'

View File

@@ -1,3 +1,4 @@
from datetime import date
from flask import url_for from flask import url_for
@@ -13,16 +14,37 @@ def test_should_show_documentation_page(app_,
assert response.status_code == 200 assert response.status_code == 200
def test_should_show_api_keys_page(app_, def test_should_show_empty_api_keys_page(app_,
db_, db_,
db_session, db_session,
active_user): active_user,
mock_get_no_api_keys):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
client.login(active_user) client.login(active_user)
response = client.get(url_for('main.api_keys', service_id=123)) response = client.get(url_for('main.api_keys', service_id=123))
assert response.status_code == 200 assert response.status_code == 200
assert 'You havent created any API keys yet' in response.get_data(as_text=True)
assert 'Create a new API key' in response.get_data(as_text=True)
mock_get_no_api_keys.assert_called_once_with(service_id=123)
def test_should_show_api_keys_page(app_,
db_,
db_session,
active_user,
mock_get_api_keys):
with app_.test_request_context():
with app_.test_client() as client:
client.login(active_user)
response = client.get(url_for('main.api_keys', service_id=123))
assert response.status_code == 200
assert 'some key name' in response.get_data(as_text=True)
assert 'another key name' in response.get_data(as_text=True)
assert 'Revoked Thursday 01 January 1970 at 00:00' in response.get_data(as_text=True)
mock_get_api_keys.assert_called_once_with(service_id=123)
def test_should_show_name_api_key_page(app_, def test_should_show_name_api_key_page(app_,
@@ -37,47 +59,43 @@ def test_should_show_name_api_key_page(app_,
assert response.status_code == 200 assert response.status_code == 200
def test_should_redirect_to_new_api_key(app_, def test_should_render_show_api_key(app_,
db_, db_,
db_session, db_session,
active_user): active_user,
mock_create_api_key):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
client.login(active_user) client.login(active_user)
response = client.post(url_for('main.create_api_key', service_id=123)) response = client.post(url_for('main.create_api_key', service_id=123),
data={'key_name': 'some default key name'})
assert response.status_code == 302
assert response.location == url_for('main.show_api_key', service_id=123, _external=True)
def test_should_show_new_api_key(app_,
db_,
db_session,
active_user):
with app_.test_request_context():
with app_.test_client() as client:
client.login(active_user)
response = client.get(url_for('main.show_api_key', service_id=123))
assert response.status_code == 200 assert response.status_code == 200
assert 'some default key name' in response.get_data(as_text=True)
mock_create_api_key.assert_called_once_with(service_id=123, key_name='some default key name')
def test_should_show_confirm_revoke_api_key(app_, def test_should_show_confirm_revoke_api_key(app_,
db_, db_,
db_session, db_session,
active_user): active_user,
mock_get_api_keys):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
client.login(active_user) client.login(active_user)
response = client.get(url_for('main.revoke_api_key', service_id=123, key_id=321)) response = client.get(url_for('main.revoke_api_key', service_id=123, key_id=321))
assert response.status_code == 200 assert response.status_code == 200
assert 'some key name' in response.get_data(as_text=True)
mock_get_api_keys.assert_called_once_with(service_id=123, key_id=321)
def test_should_redirect_after_revoking_api_key(app_, def test_should_redirect_after_revoking_api_key(app_,
db_, db_,
db_session, db_session,
active_user): active_user,
mock_revoke_api_key,
mock_get_api_keys):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
client.login(active_user) client.login(active_user)
@@ -85,3 +103,5 @@ def test_should_redirect_after_revoking_api_key(app_,
assert response.status_code == 302 assert response.status_code == 302
assert response.location == url_for('.api_keys', service_id=123, _external=True) assert response.location == url_for('.api_keys', service_id=123, _external=True)
mock_revoke_api_key.assert_called_once_with(service_id=123, key_id=321)
mock_get_api_keys.assert_called_once_with(service_id=123, key_id=321)

View File

@@ -1,14 +1,17 @@
import os import os
from datetime import date
import pytest import pytest
from alembic.command import upgrade from alembic.command import upgrade
from alembic.config import Config from alembic.config import Config
from flask.ext.migrate import Migrate, MigrateCommand from flask.ext.migrate import Migrate, MigrateCommand
from flask.ext.script import Manager from flask.ext.script import Manager
from sqlalchemy.schema import MetaData from sqlalchemy.schema import MetaData
from . import (
create_test_user, create_another_test_user, service_json, TestClient,
get_test_user, template_json)
from app import create_app, db from app import create_app, db
from . import (
create_test_user, service_json, TestClient,
get_test_user, template_json, api_key_json)
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
@@ -89,6 +92,7 @@ def mock_get_service(mocker, active_user):
service_id, "Test Service", [active_user.id], limit=1000, service_id, "Test Service", [active_user.id], limit=1000,
active=False, restricted=True) active=False, restricted=True)
return {'data': service, 'token': 1} return {'data': service, 'token': 1}
return mocker.patch('app.notifications_api_client.get_service', side_effect=_create) return mocker.patch('app.notifications_api_client.get_service', side_effect=_create)
@@ -99,6 +103,7 @@ def mock_create_service(mocker):
101, service_name, [user_id], limit=limit, 101, service_name, [user_id], limit=limit,
active=active, restricted=restricted) active=active, restricted=restricted)
return {'data': service} return {'data': service}
mock_class = mocker.patch( mock_class = mocker.patch(
'app.notifications_api_client.create_service', side_effect=_create) 'app.notifications_api_client.create_service', side_effect=_create)
return mock_class return mock_class
@@ -116,6 +121,7 @@ def mock_update_service(mocker):
service_id, service_name, users, limit=limit, service_id, service_name, users, limit=limit,
active=active, restricted=restricted) active=active, restricted=restricted)
return {'data': service} return {'data': service}
mock_class = mocker.patch( mock_class = mocker.patch(
'app.notifications_api_client.update_service', side_effect=_update) 'app.notifications_api_client.update_service', side_effect=_update)
return mock_class return mock_class
@@ -129,6 +135,7 @@ def mock_get_services(mocker, active_user):
service_two = service_json( service_two = service_json(
2, "service_two", [active_user.id], 1000, True, False) 2, "service_two", [active_user.id], 1000, True, False)
return {'data': [service_one, service_two]} return {'data': [service_one, service_two]}
mock_class = mocker.patch( mock_class = mocker.patch(
'app.notifications_api_client.get_services', side_effect=_create) 'app.notifications_api_client.get_services', side_effect=_create)
return mock_class return mock_class
@@ -138,6 +145,7 @@ def mock_get_services(mocker, active_user):
def mock_delete_service(mocker, mock_get_service): def mock_delete_service(mocker, mock_get_service):
def _delete(service_id): def _delete(service_id):
return mock_get_service.side_effect(service_id) return mock_get_service.side_effect(service_id)
mock_class = mocker.patch( mock_class = mocker.patch(
'app.notifications_api_client.delete_service', side_effect=_delete) 'app.notifications_api_client.delete_service', side_effect=_delete)
return mock_class return mock_class
@@ -149,6 +157,7 @@ def mock_get_service_template(mocker):
template = template_json( template = template_json(
template_id, "Template Name", "sms", "template content", service_id) template_id, "Template Name", "sms", "template content", service_id)
return {'data': template} return {'data': template}
return mocker.patch( return mocker.patch(
'app.notifications_api_client.get_service_template', 'app.notifications_api_client.get_service_template',
side_effect=_create) side_effect=_create)
@@ -160,6 +169,7 @@ def mock_create_service_template(mocker):
template = template_json( template = template_json(
101, name, type_, content, service) 101, name, type_, content, service)
return {'data': template} return {'data': template}
mock_class = mocker.patch( mock_class = mocker.patch(
'app.notifications_api_client.create_service_template', 'app.notifications_api_client.create_service_template',
side_effect=_create) side_effect=_create)
@@ -172,6 +182,7 @@ def mock_update_service_template(mocker):
template = template_json( template = template_json(
id_, name, type_, content, service) id_, name, type_, content, service)
return {'data': template} return {'data': template}
mock_class = mocker.patch( mock_class = mocker.patch(
'app.notifications_api_client.update_service_template', 'app.notifications_api_client.update_service_template',
side_effect=_update) side_effect=_update)
@@ -186,6 +197,7 @@ def mock_get_service_templates(mocker):
template_two = template_json( template_two = template_json(
2, "template_two", "sms", "template two content", service_id) 2, "template_two", "sms", "template two content", service_id)
return {'data': [template_one, template_two]} return {'data': [template_one, template_two]}
mock_class = mocker.patch( mock_class = mocker.patch(
'app.notifications_api_client.get_service_templates', 'app.notifications_api_client.get_service_templates',
side_effect=_create) side_effect=_create)
@@ -199,6 +211,7 @@ def mock_delete_service_template(mocker):
template_id, "Template to delete", template_id, "Template to delete",
"sms", "content to be deleted", service_id) "sms", "content to be deleted", service_id)
return {'data': template} return {'data': template}
return mocker.patch( return mocker.patch(
'app.notifications_api_client.delete_service_template', side_effect=_delete) 'app.notifications_api_client.delete_service_template', side_effect=_delete)
@@ -212,5 +225,41 @@ def mock_register_user(mocker, user_data):
return mock_class return mock_class
def mock_create_api_key(mocker, key_name): @pytest.fixture(scope='function')
mock_class = mocker.patch('app.api') def mock_create_api_key(mocker):
def _create(service_id, key_name):
import uuid
return {'data': str(uuid.uuid4())}
mock_class = mocker.patch('app.api_key_api_client.create_api_key', side_effect=_create)
return mock_class
@pytest.fixture(scope='function')
def mock_revoke_api_key(mocker):
def _revoke(service_id, key_id):
return {}
mock_class = mocker.patch('app.api_key_api_client.revoke_api_key', side_effect=_revoke)
return mock_class
@pytest.fixture(scope='function')
def mock_get_api_keys(mocker):
def _get_keys(service_id, key_id=None):
keys = {'apiKeys': [api_key_json(1, 'some key name'),
api_key_json(2, 'another key name', expiry_date=str(date.fromtimestamp(0)))]}
return keys
mock_class = mocker.patch('app.api_key_api_client.get_api_keys', side_effect=_get_keys)
return mock_class
@pytest.fixture(scope='function')
def mock_get_no_api_keys(mocker):
def _get_keys(service_id):
keys = {'apiKeys': []}
return keys
mock_class = mocker.patch('app.api_key_api_client.get_api_keys', side_effect=_get_keys)
return mock_class