This is the first PR in the story to create a suspend service feature.

- Renaming /service/<id>/deactivate to /service/<id>/archive to match language on the UI.
  - Will need to update admin before deleting the deactive service method
- Created dao and endpoint methods to suspend and resume a service.
- I confirm the use of suspend and resume with a couple people on the team, seems to be the right choice.

The idea is that if you archive a service there is no coming back from that.
To suspend a service is marking it as inactive and revoking the api keys. To resume a service is to mark the service as active, the service will need to create new API keys.
It makes sense that if a service is under threat that the API keys should be renewed.

The next PR will update the code to check that the service is active before sending the request or allowing any actions by the service.
This commit is contained in:
Rebecca Law
2017-01-30 16:32:44 +00:00
parent 1d734d3b28
commit 87556687ab
6 changed files with 309 additions and 9 deletions

View File

@@ -22,8 +22,9 @@ from app.dao.services_dao import (
dao_fetch_weekly_historical_stats_for_service,
fetch_todays_total_message_count,
dao_fetch_todays_stats_for_all_services,
fetch_stats_by_date_range_for_all_services
)
fetch_stats_by_date_range_for_all_services,
dao_suspend_service,
dao_resume_service)
from app.dao.users_dao import save_model_user
from app.models import (
NotificationStatistics,
@@ -656,3 +657,25 @@ def test_fetch_stats_by_date_range_for_all_services(notify_db, notify_db_session
assert len(results) == 1
assert results[0] == ('sms', 'created', result_one.service_id, 2)
def test_dao_suspend_service_marks_service_as_inactive_and_expires_api_keys(sample_service, sample_api_key):
dao_suspend_service(sample_service.id)
service = Service.query.get(sample_service.id)
assert not service.active
assert service.name == sample_service.name
api_key = ApiKey.query.get(sample_api_key.id)
assert api_key.expiry_date.date() == datetime.utcnow().date()
def test_dao_resume_service_marks_service_as_active_and_api_keys_are_still_revoked(sample_service, sample_api_key):
dao_suspend_service(sample_service.id)
service = Service.query.get(sample_service.id)
assert not service.active
dao_resume_service(service.id)
assert Service.query.get(service.id).active
api_key = ApiKey.query.get(sample_api_key.id)
assert api_key.expiry_date.date() == datetime.utcnow().date()

View File

@@ -0,0 +1,123 @@
import uuid
from datetime import datetime
import pytest
from freezegun import freeze_time
from app import db
from app.models import Service
from app.dao.services_dao import dao_archive_service
from app.dao.api_key_dao import expire_api_key
from app.dao.templates_dao import dao_update_template
from tests import create_authorization_header, unwrap_function
from tests.app.conftest import (
sample_template as create_template,
sample_api_key as create_api_key
)
def test_archive_only_allows_post(client):
auth_header = create_authorization_header()
response = client.get('/service/{}/archive'.format(uuid.uuid4()), headers=[auth_header])
assert response.status_code == 405
def test_archive_service_errors_with_bad_service_id(client):
auth_header = create_authorization_header()
response = client.post('/service/{}/archive'.format(uuid.uuid4()), headers=[auth_header])
assert response.status_code == 404
def test_deactivating_inactive_service_does_nothing(client, sample_service):
auth_header = create_authorization_header()
sample_service.active = False
response = client.post('/service/{}/archive'.format(sample_service.id), headers=[auth_header])
assert response.status_code == 204
assert sample_service.name == 'Sample service'
@pytest.fixture
def archived_service(client, notify_db, notify_db_session, sample_service):
create_template(notify_db, notify_db_session, template_name='a')
create_template(notify_db, notify_db_session, template_name='b')
create_api_key(notify_db, notify_db_session)
create_api_key(notify_db, notify_db_session)
auth_header = create_authorization_header()
response = client.post('/service/{}/archive'.format(sample_service.id), headers=[auth_header])
assert response.status_code == 204
assert response.data == b''
return sample_service
def test_deactivating_service_changes_name_and_email(archived_service):
assert archived_service.name == '_archived_Sample service'
assert archived_service.email_from == '_archived_sample.service'
def test_deactivating_service_revokes_api_keys(archived_service):
assert len(archived_service.api_keys) == 2
for key in archived_service.api_keys:
assert key.expiry_date is not None
assert key.version == 2
def test_deactivating_service_archives_templates(archived_service):
assert len(archived_service.templates) == 2
for template in archived_service.templates:
assert template.archived is True
assert template.version == 2
def test_deactivating_service_creates_history(archived_service):
ServiceHistory = Service.get_history_model()
history = ServiceHistory.query.filter_by(
id=archived_service.id
).order_by(
ServiceHistory.version.desc()
).first()
assert history.version == 2
assert history.active is False
@pytest.fixture
def archived_service_with_deleted_stuff(client, notify_db, notify_db_session, sample_service):
with freeze_time('2001-01-01'):
template = create_template(notify_db, notify_db_session, template_name='a')
api_key = create_api_key(notify_db, notify_db_session)
expire_api_key(sample_service.id, api_key.id)
template.archived = True
dao_update_template(template)
with freeze_time('2002-02-02'):
auth_header = create_authorization_header()
response = client.post('/service/{}/archive'.format(sample_service.id), headers=[auth_header])
assert response.status_code == 204
assert response.data == b''
return sample_service
def test_deactivating_service_doesnt_affect_existing_archived_templates(archived_service_with_deleted_stuff):
assert archived_service_with_deleted_stuff.templates[0].archived is True
assert archived_service_with_deleted_stuff.templates[0].updated_at == datetime(2001, 1, 1, 0, 0, 0)
assert archived_service_with_deleted_stuff.templates[0].version == 2
def test_deactivating_service_doesnt_affect_existing_revoked_api_keys(archived_service_with_deleted_stuff):
assert archived_service_with_deleted_stuff.api_keys[0].expiry_date == datetime(2001, 1, 1, 0, 0, 0)
assert archived_service_with_deleted_stuff.api_keys[0].version == 2
def test_deactivating_service_rolls_back_everything_on_error(sample_service, sample_api_key, sample_template):
unwrapped_deactive_service = unwrap_function(dao_archive_service)
unwrapped_deactive_service(sample_service.id)
assert sample_service in db.session.dirty
assert sample_api_key in db.session.dirty
assert sample_template in db.session.dirty

View File

@@ -6,7 +6,7 @@ from freezegun import freeze_time
from app import db
from app.models import Service
from app.dao.services_dao import dao_deactive_service
from app.dao.services_dao import dao_archive_service
from app.dao.api_key_dao import expire_api_key
from app.dao.templates_dao import dao_update_template
@@ -114,7 +114,7 @@ def test_deactivating_service_doesnt_affect_existing_revoked_api_keys(deactivate
def test_deactivating_service_rolls_back_everything_on_error(sample_service, sample_api_key, sample_template):
unwrapped_deactive_service = unwrap_function(dao_deactive_service)
unwrapped_deactive_service = unwrap_function(dao_archive_service)
unwrapped_deactive_service(sample_service.id)

View File

@@ -0,0 +1,72 @@
from datetime import datetime
import pytest
import uuid
from app.models import Service
from tests import create_authorization_header
@pytest.mark.parametrize("endpoint", ["suspend", "resume"])
def test_only_allows_post(client, endpoint):
auth_header = create_authorization_header()
response = client.get("/service/{}/{}".format(uuid.uuid4(), endpoint),
headers=[auth_header])
assert response.status_code == 405
@pytest.mark.parametrize("endpoint", ["suspend", "resume"])
def test_returns_404_when_service_does_not_exist(client, endpoint):
auth_header = create_authorization_header()
response = client.post("/service/{}/{}".format(uuid.uuid4(), endpoint),
headers=[auth_header])
assert response.status_code == 404
@pytest.mark.parametrize("action, active", [("suspend", False), ("resume", True)])
def test_has_not_effect_when_service_is_already_that_state(client, sample_service, action, active, mocker):
mocked = mocker.patch("app.service.rest.dao_{}_service".format(action))
sample_service.active = active
auth_header = create_authorization_header()
response = client.post("/service/{}/{}".format(sample_service.id, action),
headers=[auth_header])
assert response.status_code == 204
mocked.assert_not_called()
assert mocked.called == 0
def test_suspending_service_revokes_api_keys(client, sample_service, sample_api_key):
auth_header = create_authorization_header()
response = client.post("/service/{}/archive".format(sample_service.id),
headers=[auth_header])
assert response.status_code == 204
assert sample_api_key.expiry_date.date() == datetime.utcnow().date()
def test_resume_service_leaves_api_keys_revokes(client, sample_service, sample_api_key):
sample_api_key.expiry_date = datetime.utcnow()
sample_service.active = False
auth_header = create_authorization_header()
response = client.post("/service/{}/archive".format(sample_service.id),
headers=[auth_header])
assert response.status_code == 204
assert sample_api_key.expiry_date.date() == datetime.utcnow().date()
@pytest.mark.parametrize("action, original_state", [("suspend", True), ("resume", False)])
def test_service_history_is_created(client, sample_service, action, original_state):
sample_service.active = original_state
auth_header = create_authorization_header()
response = client.post("/service/{}/{}".format(sample_service.id, action),
headers=[auth_header])
ServiceHistory = Service.get_history_model()
history = ServiceHistory.query.filter_by(
id=sample_service.id
).order_by(
ServiceHistory.version.desc()
).first()
assert response.status_code == 204
assert history.version == 2
assert history.active != original_state