Merge pull request #635 from alphagov/scheduled-delivery-of-jobs

Scheduled delivery of jobs
This commit is contained in:
minglis
2016-08-30 12:48:48 +01:00
committed by GitHub
13 changed files with 312 additions and 58 deletions

View File

@@ -7,8 +7,11 @@ from app.celery.scheduled_tasks import (delete_verify_codes,
delete_successful_notifications,
delete_failed_notifications,
delete_invitations,
timeout_notifications)
from tests.app.conftest import sample_notification
timeout_notifications,
run_scheduled_jobs)
from app.dao.jobs_dao import dao_get_job_by_id
from tests.app.conftest import sample_notification, sample_job
from mock import call
def test_should_have_decorated_tasks_functions():
@@ -17,10 +20,11 @@ def test_should_have_decorated_tasks_functions():
assert delete_failed_notifications.__wrapped__.__name__ == 'delete_failed_notifications'
assert timeout_notifications.__wrapped__.__name__ == 'timeout_notifications'
assert delete_invitations.__wrapped__.__name__ == 'delete_invitations'
assert run_scheduled_jobs.__wrapped__.__name__ == 'run_scheduled_jobs'
def test_should_call_delete_notifications_more_than_week_in_task(notify_api, mocker):
mocked = mocker.patch('app.celery.scheduled_tasksgit .delete_notifications_created_more_than_a_week_ago')
mocked = mocker.patch('app.celery.scheduled_tasks.delete_notifications_created_more_than_a_week_ago')
delete_successful_notifications()
assert mocked.assert_called_with('delivered')
assert scheduled_tasks.delete_notifications_created_more_than_a_week_ago.call_count == 1
@@ -80,3 +84,39 @@ def test_not_update_status_of_notification_before_timeout(notify_api,
seconds=current_app.config.get('SENDING_NOTIFICATIONS_TIMEOUT_PERIOD') - 10))
timeout_notifications()
assert not1.status == 'sending'
def test_should_update_scheduled_jobs_and_put_on_queue(notify_db, notify_db_session, mocker):
mocked = mocker.patch('app.celery.tasks.process_job.apply_async')
one_minute_in_the_past = datetime.utcnow() - timedelta(minutes=1)
job = sample_job(notify_db, notify_db_session, scheduled_for=one_minute_in_the_past, job_status='scheduled')
run_scheduled_jobs()
updated_job = dao_get_job_by_id(job.id)
assert updated_job.job_status == 'pending'
mocked.assert_called_with([str(job.id)], queue='process-job')
def test_should_update_all_scheduled_jobs_and_put_on_queue(notify_db, notify_db_session, mocker):
mocked = mocker.patch('app.celery.tasks.process_job.apply_async')
one_minute_in_the_past = datetime.utcnow() - timedelta(minutes=1)
ten_minutes_in_the_past = datetime.utcnow() - timedelta(minutes=10)
twenty_minutes_in_the_past = datetime.utcnow() - timedelta(minutes=20)
job_1 = sample_job(notify_db, notify_db_session, scheduled_for=one_minute_in_the_past, job_status='scheduled')
job_2 = sample_job(notify_db, notify_db_session, scheduled_for=ten_minutes_in_the_past, job_status='scheduled')
job_3 = sample_job(notify_db, notify_db_session, scheduled_for=twenty_minutes_in_the_past, job_status='scheduled')
run_scheduled_jobs()
assert dao_get_job_by_id(job_1.id).job_status == 'pending'
assert dao_get_job_by_id(job_2.id).job_status == 'pending'
assert dao_get_job_by_id(job_2.id).job_status == 'pending'
mocked.assert_has_calls([
call([str(job_3.id)], queue='process-job'),
call([str(job_2.id)], queue='process-job'),
call([str(job_1.id)], queue='process-job')
])

View File

@@ -248,7 +248,9 @@ def sample_job(notify_db,
service=None,
template=None,
notification_count=1,
created_at=datetime.utcnow()):
created_at=datetime.utcnow(),
job_status='pending',
scheduled_for=None):
if service is None:
service = sample_service(notify_db, notify_db_session)
if template is None:
@@ -263,7 +265,9 @@ def sample_job(notify_db,
'original_file_name': 'some.csv',
'notification_count': notification_count,
'created_at': created_at,
'created_by': service.created_by
'created_by': service.created_by,
'job_status': job_status,
'scheduled_for': scheduled_for
}
job = Job(**data)
dao_create_job(job)

View File

@@ -6,7 +6,9 @@ from app.dao.jobs_dao import (
dao_create_job,
dao_update_job,
dao_get_jobs_by_service_id,
dao_get_notification_outcomes_for_job)
dao_get_scheduled_jobs,
dao_get_notification_outcomes_for_job
)
from app.models import Job
from tests.app.conftest import sample_notification, sample_job, sample_service
@@ -222,3 +224,30 @@ def test_update_job(sample_job):
job_from_db = Job.query.get(sample_job.id)
assert job_from_db.status == 'in progress'
def test_get_scheduled_jobs_gets_all_jobs_in_scheduled_state_scheduled_before_now(notify_db, notify_db_session):
one_minute_ago = datetime.utcnow() - timedelta(minutes=1)
one_hour_ago = datetime.utcnow() - timedelta(minutes=60)
job_new = sample_job(notify_db, notify_db_session, scheduled_for=one_minute_ago, job_status='scheduled')
job_old = sample_job(notify_db, notify_db_session, scheduled_for=one_hour_ago, job_status='scheduled')
jobs = dao_get_scheduled_jobs()
assert len(jobs) == 2
assert jobs[0].id == job_old.id
assert jobs[1].id == job_new.id
def test_get_scheduled_jobs_gets_ignores_jobs_not_scheduled(notify_db, notify_db_session):
one_minute_ago = datetime.utcnow() - timedelta(minutes=1)
sample_job(notify_db, notify_db_session)
job_scheduled = sample_job(notify_db, notify_db_session, scheduled_for=one_minute_ago, job_status='scheduled')
jobs = dao_get_scheduled_jobs()
assert len(jobs) == 1
assert jobs[0].id == job_scheduled.id
def test_get_scheduled_jobs_gets_ignores_jobs_scheduled_in_the_future(notify_db, notify_db_session):
one_minute_in_the_future = datetime.utcnow() + timedelta(minutes=1)
sample_job(notify_db, notify_db_session, scheduled_for=one_minute_in_the_future, job_status='scheduled')
jobs = dao_get_scheduled_jobs()
assert len(jobs) == 0

View File

@@ -1,9 +1,10 @@
import json
import uuid
from datetime import datetime, timedelta
from freezegun import freeze_time
import pytest
import pytz
import app.celery.tasks
from tests import create_authorization_header
@@ -94,7 +95,21 @@ def test_get_job_with_unknown_id_returns404(notify_api, sample_template, fake_uu
}
def test_create_job(notify_api, sample_template, mocker, fake_uuid):
def test_get_job_by_id(notify_api, sample_job):
job_id = str(sample_job.id)
service_id = sample_job.service.id
with notify_api.test_request_context():
with notify_api.test_client() as client:
path = '/service/{}/job/{}'.format(service_id, job_id)
auth_header = create_authorization_header(service_id=sample_job.service.id)
response = client.get(path, headers=[auth_header])
assert response.status_code == 200
resp_json = json.loads(response.get_data(as_text=True))
assert resp_json['data']['id'] == job_id
assert resp_json['data']['created_by']['name'] == 'Test User'
def test_create_unscheduled_job(notify_api, sample_template, mocker, fake_uuid):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocker.patch('app.celery.tasks.process_job.apply_async')
@@ -124,11 +139,121 @@ def test_create_job(notify_api, sample_template, mocker, fake_uuid):
resp_json = json.loads(response.get_data(as_text=True))
assert resp_json['data']['id'] == fake_uuid
assert resp_json['data']['service'] == str(sample_template.service.id)
assert resp_json['data']['status'] == 'pending'
assert not resp_json['data']['scheduled_for']
assert resp_json['data']['job_status'] == 'pending'
assert resp_json['data']['template'] == str(sample_template.id)
assert resp_json['data']['original_file_name'] == 'thisisatest.csv'
def test_create_scheduled_job(notify_api, sample_template, mocker, fake_uuid):
with notify_api.test_request_context():
with notify_api.test_client() as client:
with freeze_time("2016-01-01 12:00:00.000000"):
scheduled_date = (datetime.utcnow() + timedelta(hours=23, minutes=59)).isoformat()
mocker.patch('app.celery.tasks.process_job.apply_async')
data = {
'id': fake_uuid,
'service': str(sample_template.service.id),
'template': str(sample_template.id),
'original_file_name': 'thisisatest.csv',
'notification_count': 1,
'created_by': str(sample_template.created_by.id),
'scheduled_for': scheduled_date
}
path = '/service/{}/job'.format(sample_template.service.id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
headers = [('Content-Type', 'application/json'), auth_header]
response = client.post(
path,
data=json.dumps(data),
headers=headers)
assert response.status_code == 201
app.celery.tasks.process_job.apply_async.assert_not_called()
resp_json = json.loads(response.get_data(as_text=True))
assert resp_json['data']['id'] == fake_uuid
assert resp_json['data']['status'] == 'pending'
assert resp_json['data']['scheduled_for'] == datetime(2016, 1, 2, 11, 59, 0,
tzinfo=pytz.UTC).isoformat()
assert resp_json['data']['job_status'] == 'scheduled'
assert resp_json['data']['template'] == str(sample_template.id)
assert resp_json['data']['original_file_name'] == 'thisisatest.csv'
def test_should_not_create_scheduled_job_more_then_24_hours_hence(notify_api, sample_template, mocker, fake_uuid):
with notify_api.test_request_context():
with notify_api.test_client() as client:
with freeze_time("2016-01-01 11:09:00.061258"):
scheduled_date = (datetime.utcnow() + timedelta(hours=24, minutes=1)).isoformat()
mocker.patch('app.celery.tasks.process_job.apply_async')
data = {
'id': fake_uuid,
'service': str(sample_template.service.id),
'template': str(sample_template.id),
'original_file_name': 'thisisatest.csv',
'notification_count': 1,
'created_by': str(sample_template.created_by.id),
'scheduled_for': scheduled_date
}
path = '/service/{}/job'.format(sample_template.service.id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
headers = [('Content-Type', 'application/json'), auth_header]
print(json.dumps(data))
response = client.post(
path,
data=json.dumps(data),
headers=headers)
assert response.status_code == 400
app.celery.tasks.process_job.apply_async.assert_not_called()
resp_json = json.loads(response.get_data(as_text=True))
assert resp_json['result'] == 'error'
assert 'scheduled_for' in resp_json['message']
assert resp_json['message']['scheduled_for'] == ['Date cannot be more than 24hrs in the future']
def test_should_not_create_scheduled_job_in_the_past(notify_api, sample_template, mocker, fake_uuid):
with notify_api.test_request_context():
with notify_api.test_client() as client:
with freeze_time("2016-01-01 11:09:00.061258"):
scheduled_date = (datetime.utcnow() - timedelta(minutes=1)).isoformat()
mocker.patch('app.celery.tasks.process_job.apply_async')
data = {
'id': fake_uuid,
'service': str(sample_template.service.id),
'template': str(sample_template.id),
'original_file_name': 'thisisatest.csv',
'notification_count': 1,
'created_by': str(sample_template.created_by.id),
'scheduled_for': scheduled_date
}
path = '/service/{}/job'.format(sample_template.service.id)
auth_header = create_authorization_header(service_id=sample_template.service.id)
headers = [('Content-Type', 'application/json'), auth_header]
print(json.dumps(data))
response = client.post(
path,
data=json.dumps(data),
headers=headers)
assert response.status_code == 400
app.celery.tasks.process_job.apply_async.assert_not_called()
resp_json = json.loads(response.get_data(as_text=True))
assert resp_json['result'] == 'error'
assert 'scheduled_for' in resp_json['message']
assert resp_json['message']['scheduled_for'] == ['Date cannot be in the past']
def test_create_job_returns_400_if_missing_data(notify_api, sample_template, mocker):
with notify_api.test_request_context():
with notify_api.test_client() as client:
@@ -287,35 +412,19 @@ def test_get_all_notifications_for_job_in_order_of_job_number(notify_api,
@pytest.mark.parametrize(
"expected_notification_count, status_args",
[
(
1,
'?status={}'.format(NOTIFICATION_STATUS_TYPES[0])
),
(
0,
'?status={}'.format(NOTIFICATION_STATUS_TYPES[1])
),
(
1,
'?status={}&status={}&status={}'.format(
*NOTIFICATION_STATUS_TYPES[0:3]
)
),
(
0,
'?status={}&status={}&status={}'.format(
*NOTIFICATION_STATUS_TYPES[3:6]
)
),
(1, '?status={}'.format(NOTIFICATION_STATUS_TYPES[0])),
(0, '?status={}'.format(NOTIFICATION_STATUS_TYPES[1])),
(1, '?status={}&status={}&status={}'.format(*NOTIFICATION_STATUS_TYPES[0:3])),
(0, '?status={}&status={}&status={}'.format(*NOTIFICATION_STATUS_TYPES[3:6])),
]
)
def test_get_all_notifications_for_job_filtered_by_status(
notify_api,
notify_db,
notify_db_session,
sample_service,
expected_notification_count,
status_args
notify_api,
notify_db,
notify_db_session,
sample_service,
expected_notification_count,
status_args
):
with notify_api.test_request_context(), notify_api.test_client() as client:
job = create_job(notify_db, notify_db_session, service=sample_service)

View File

@@ -2,6 +2,7 @@ from datetime import date, timedelta
from flask import json
from freezegun import freeze_time
from datetime import datetime
from tests import create_authorization_header
from tests.app.conftest import (
@@ -196,7 +197,6 @@ def test_get_notification_statistics_returns_both_existing_stats_and_generated_z
assert response.status_code == 200
@freeze_time('1955-11-05T12:00:00')
def test_get_notification_statistics_returns_zeros_when_only_stats_for_different_date(
notify_api,
sample_notification_statistics
@@ -208,7 +208,7 @@ def test_get_notification_statistics_returns_zeros_when_only_stats_for_different
service_id=sample_notification_statistics.service_id
)
response = client.get(
'/notifications/statistics?day={}'.format(date.today().isoformat()),
'/notifications/statistics?day={}'.format(datetime.utcnow().isoformat()),
headers=[auth_header]
)

View File

@@ -1,4 +1,3 @@
from datetime import datetime, timedelta
import uuid
import pytest

View File

@@ -49,7 +49,7 @@ def notify_db_session(request, notify_db):
def teardown():
notify_db.session.remove()
for tbl in reversed(notify_db.metadata.sorted_tables):
if tbl.name not in ["provider_details", "key_types", "branding_type"]:
if tbl.name not in ["provider_details", "key_types", "branding_type", "job_status"]:
notify_db.engine.execute(tbl.delete())
notify_db.session.commit()