Skip to content

Commit fdb9ffe

Browse files
authored
Add job_id to the create_service_report endpoint (#2512)
This change: - adds the job_id to the create_service_report endpoint - adds a filter for job_id to the sql query that creates the csv file in build_notifications_query - adds the template_type to the data that gets returned from the get_job_by_service_and_job_id endpoint (this is a non-breaking change, just adding some data) - adds some tests for the create_service_report endpoint, including for different notification_statuses
1 parent 97a5d33 commit fdb9ffe

File tree

7 files changed

+164
-15
lines changed

7 files changed

+164
-15
lines changed

app/celery/tasks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,7 @@ def generate_report(report_id: str, notification_statuses=[]):
905905
notification_type=report.report_type,
906906
language=report.language,
907907
notification_statuses=notification_statuses,
908+
job_id=report.job_id,
908909
days_limit=LIMIT_DAYS,
909910
s3_bucket=current_app.config["REPORTS_BUCKET_NAME"],
910911
s3_key=s3_key,

app/report/rest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def create_service_report(service_id):
4242
"requesting_user_id": data.get("requesting_user_id"),
4343
"language": data.get("language"),
4444
"notification_statuses": data.get("notification_statuses"),
45+
"job_id": data.get("job_id"),
4546
}
4647

4748
# Validate against the schema
@@ -55,6 +56,7 @@ def create_service_report(service_id):
5556
status=report_data["status"],
5657
requesting_user_id=report_data["requesting_user_id"],
5758
language=report_data["language"],
59+
job_id=report_data["job_id"],
5860
)
5961

6062
# Save the report to the database

app/report/utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def translate(self, x):
5454
return x
5555

5656

57-
def build_notifications_query(service_id, notification_type, language, notification_statuses=[], days_limit=7):
57+
def build_notifications_query(service_id, notification_type, language, notification_statuses=[], job_id=None, days_limit=7):
5858
"""
5959
Builds and returns an SQLAlchemy query for notifications with the specified parameters.
6060
@@ -64,6 +64,7 @@ def build_notifications_query(service_id, notification_type, language, notificat
6464
language: "en" or "fr"
6565
days_limit: Number of days to look back in history
6666
notification_statuses: List of notification statuses to filter by
67+
job_id: Optional filter by specific job ID
6768
Returns:
6869
SQLAlchemy query object for notifications
6970
"""
@@ -84,6 +85,9 @@ def build_notifications_query(service_id, notification_type, language, notificat
8485
statuses = Notification.substitute_status(notification_statuses)
8586
query_filters.append(n.status.in_(statuses))
8687

88+
if job_id:
89+
query_filters.append(n.job_id == job_id)
90+
8791
inner_query = (
8892
db.session.query(
8993
n.to.label("to"),
@@ -199,7 +203,7 @@ def stream_query_to_s3(copy_command, s3_bucket, s3_key):
199203

200204

201205
def generate_csv_from_notifications(
202-
service_id, notification_type, language, notification_statuses=[], days_limit=7, s3_bucket=None, s3_key=None
206+
service_id, notification_type, language, notification_statuses=[], job_id=None, days_limit=7, s3_bucket=None, s3_key=None
203207
):
204208
"""
205209
Generate CSV using SQLAlchemy for improved compatibility and type safety, and stream it directly to S3.
@@ -217,6 +221,7 @@ def generate_csv_from_notifications(
217221
notification_type=notification_type,
218222
language=language,
219223
notification_statuses=notification_statuses,
224+
job_id=job_id,
220225
days_limit=days_limit,
221226
)
222227
copy_command = compile_query_for_copy(query)

app/schemas.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,11 @@ class JobSchema(BaseSchema):
499499
)
500500
sender_id = fields.UUID(required=False, allow_none=True)
501501

502+
template_type = fields.Method("get_template_type", dump_only=True)
503+
504+
def get_template_type(self, job):
505+
return job.template.template_type if job.template else None
506+
502507
@validates("scheduled_for")
503508
def validate_scheduled_for(self, value):
504509
_validate_datetime_not_in_past(value)
@@ -843,6 +848,7 @@ class Meta:
843848
url = fields.String()
844849
language = fields.String()
845850
notification_statuses = fields.List(fields.String(), required=False, allow_none=True)
851+
job_id = fields.UUID(required=False, allow_none=True)
846852

847853
requesting_user = fields.Nested(
848854
UserSchema,

tests/app/report/test_rest.py

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,128 @@
55

66
def test_create_report_succeeds_with_valid_data(mocker, admin_request, sample_service, sample_user):
77
"""Test that creating a report with valid data succeeds"""
8+
# Mock the generate_report Celery task to avoid actual task execution
89
generate_report_mock = mocker.patch("app.report.rest.generate_report.apply_async")
10+
11+
# Use actual database operations instead of mocking create_report
912
data = {"report_type": ReportType.EMAIL.value, "requesting_user_id": str(sample_user.id), "language": "en"}
1013

14+
# Call the endpoint which will create a real report in the database
15+
response = admin_request.post("report.create_service_report", service_id=sample_service.id, _data=data, _expected_status=201)
16+
17+
# Verify response contains expected data
18+
assert response["data"]["report_type"] == ReportType.EMAIL.value
19+
assert response["data"]["service_id"] == str(sample_service.id)
20+
assert response["data"]["language"] == "en"
21+
assert response["data"]["status"] == ReportStatus.REQUESTED.value
22+
23+
# Extract the report ID from the response
24+
report_id = response["data"]["id"]
25+
26+
# Verify generate_report was called with correct parameters
27+
generate_report_mock.assert_called_once()
28+
assert str(generate_report_mock.call_args[0][0][0]) == report_id
29+
assert generate_report_mock.call_args[0][0][1] == [] # Empty notification_statuses
30+
assert generate_report_mock.call_args[1]["queue"] == "generate-reports"
31+
32+
33+
def test_create_report_succeeds_with_notification_statuses(mocker, admin_request, sample_service, sample_user):
34+
"""Test that creating a report with notification_statuses passes the data to generate_report task"""
35+
# Mock the generate_report Celery task to avoid actual task execution
36+
generate_report_mock = mocker.patch("app.report.rest.generate_report.apply_async")
37+
38+
notification_statuses = ["delivered", "sending"]
39+
data = {
40+
"report_type": ReportType.EMAIL.value,
41+
"requesting_user_id": str(sample_user.id),
42+
"notification_statuses": notification_statuses,
43+
"language": "en",
44+
}
45+
46+
# Call the endpoint which will create a real report in the database
47+
response = admin_request.post("report.create_service_report", service_id=sample_service.id, _data=data, _expected_status=201)
48+
49+
# Verify response contains expected data
50+
assert response["data"]["report_type"] == ReportType.EMAIL.value
51+
assert str(response["data"]["service_id"]) == str(sample_service.id)
52+
assert response["data"]["language"] == "en"
53+
assert response["data"]["status"] == ReportStatus.REQUESTED.value
54+
55+
# Extract the report ID from the response
56+
report_id = response["data"]["id"]
57+
58+
# Verify generate_report was called with notification_statuses
59+
generate_report_mock.assert_called_once()
60+
assert str(generate_report_mock.call_args[0][0][0]) == report_id
61+
assert generate_report_mock.call_args[0][0][1] == notification_statuses
62+
assert generate_report_mock.call_args[1]["queue"] == "generate-reports"
63+
64+
65+
def test_create_report_succeeds_with_job_id(mocker, admin_request, sample_service, sample_user, sample_job):
66+
"""Test that creating a report with job_id passes the data to generate_report task"""
67+
# Mock the generate_report Celery task to avoid actual task execution
68+
generate_report_mock = mocker.patch("app.report.rest.generate_report.apply_async")
69+
70+
data = {
71+
"report_type": ReportType.SMS.value,
72+
"requesting_user_id": str(sample_user.id),
73+
"job_id": str(sample_job.id),
74+
"language": "en",
75+
}
76+
77+
# Call the endpoint which will create a real report in the database
78+
response = admin_request.post("report.create_service_report", service_id=sample_service.id, _data=data, _expected_status=201)
79+
80+
# Verify response contains expected data
81+
assert response["data"]["report_type"] == ReportType.SMS.value
82+
assert str(response["data"]["service_id"]) == str(sample_service.id)
83+
assert response["data"]["job_id"] == str(sample_job.id)
84+
assert response["data"]["language"] == "en"
85+
assert response["data"]["status"] == ReportStatus.REQUESTED.value
86+
87+
# Extract the report ID from the response
88+
report_id = response["data"]["id"]
89+
90+
# Verify generate_report was called with empty notification_statuses
91+
generate_report_mock.assert_called_once()
92+
assert str(generate_report_mock.call_args[0][0][0]) == report_id
93+
assert generate_report_mock.call_args[0][0][1] == [] # Empty notification_statuses
94+
assert generate_report_mock.call_args[1]["queue"] == "generate-reports"
95+
96+
97+
def test_create_report_succeeds_with_both_notification_statuses_and_job_id(
98+
mocker, admin_request, sample_service, sample_user, sample_job
99+
):
100+
"""Test that creating a report with both notification_statuses and job_id passes both parameters correctly"""
101+
# Mock the generate_report Celery task to avoid actual task execution
102+
generate_report_mock = mocker.patch("app.report.rest.generate_report.apply_async")
103+
104+
notification_statuses = ["delivered", "failed"]
105+
data = {
106+
"report_type": ReportType.EMAIL.value,
107+
"requesting_user_id": str(sample_user.id),
108+
"job_id": str(sample_job.id),
109+
"notification_statuses": notification_statuses,
110+
"language": "en",
111+
}
112+
113+
# Call the endpoint which will create a real report in the database
11114
response = admin_request.post("report.create_service_report", service_id=sample_service.id, _data=data, _expected_status=201)
12115

116+
# Verify response contains expected data
13117
assert response["data"]["report_type"] == ReportType.EMAIL.value
14118
assert response["data"]["service_id"] == str(sample_service.id)
119+
assert response["data"]["job_id"] == str(sample_job.id)
15120
assert response["data"]["language"] == "en"
16121
assert response["data"]["status"] == ReportStatus.REQUESTED.value
17-
assert "id" in response["data"]
18-
assert "requested_at" in response["data"]
19122

20-
# assert generate_report_mock called once with the correct report ID
123+
# Extract the report ID from the response
124+
report_id = response["data"]["id"]
125+
126+
# Verify generate_report was called with notification_statuses
21127
generate_report_mock.assert_called_once()
22-
assert str(generate_report_mock.call_args[0][0][0]) == response["data"]["id"]
23-
# check the queue name
128+
assert str(generate_report_mock.call_args[0][0][0]) == report_id
129+
assert generate_report_mock.call_args[0][0][1] == notification_statuses
24130
assert generate_report_mock.call_args[1]["queue"] == "generate-reports"
25131

26132

tests/app/report/test_utils.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def test_calls_helper_functions_with_correct_parameters(self):
3535
s3_key = "test-key.csv"
3636
language = "en"
3737
notification_statuses = ["delivered", "failed"]
38+
job_id = "job-id-1"
3839
# When
3940
with patch("app.report.utils.build_notifications_query") as mock_build_query:
4041
with patch("app.report.utils.compile_query_for_copy") as mock_compile_query:
@@ -44,7 +45,7 @@ def test_calls_helper_functions_with_correct_parameters(self):
4445

4546
# Call the function
4647
generate_csv_from_notifications(
47-
service_id, notification_type, language, notification_statuses, days_limit, s3_bucket, s3_key
48+
service_id, notification_type, language, notification_statuses, job_id, days_limit, s3_bucket, s3_key
4849
)
4950

5051
# Then
@@ -53,6 +54,7 @@ def test_calls_helper_functions_with_correct_parameters(self):
5354
notification_type=notification_type,
5455
language=language,
5556
notification_statuses=notification_statuses,
57+
job_id=job_id,
5658
days_limit=days_limit,
5759
)
5860
mock_compile_query.assert_called_once_with("mock query")
@@ -111,6 +113,26 @@ def test_build_notifications_query_with_empty_status_filter(self):
111113
sql_str = str(query.statement.compile(compile_kwargs={"literal_binds": True}))
112114
assert "notification.status IN" not in sql_str # No status filter should be applied
113115

116+
def test_build_notifications_query_with_job_id_filter(self):
117+
# Given
118+
service_id = "service-id-1"
119+
notification_type = "email"
120+
language = "en"
121+
job_id = "job-id-1"
122+
123+
# When
124+
query = build_notifications_query(
125+
service_id=service_id,
126+
notification_type=notification_type,
127+
language=language,
128+
job_id=job_id,
129+
)
130+
131+
# Then
132+
# Convert query to string to check the SQL
133+
sql_str = str(query.statement.compile(compile_kwargs={"literal_binds": True}))
134+
assert f".job_id = '{job_id}'" in sql_str
135+
114136

115137
class TestEmails:
116138
def test_send_email_notification2(self, mocker, sample_template, sample_report, sample_service, sample_notification):
@@ -170,6 +192,7 @@ def fake_stream_query_to_s3(copy_command, s3_bucket, s3_key):
170192
"email",
171193
"en",
172194
days_limit=7,
195+
job_id=None,
173196
s3_bucket="test-bucket",
174197
s3_key="test-key.csv",
175198
)
@@ -226,6 +249,7 @@ def fake_stream_query_to_s3(copy_command, s3_bucket, s3_key):
226249
"email",
227250
"en",
228251
notification_statuses=["delivered", "failed"],
252+
job_id=None,
229253
days_limit=7,
230254
s3_bucket="test-bucket",
231255
s3_key="test-key.csv",

tests/app/v2/notifications/test_post_notifications.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from app.v2.notifications.post_notifications import _seed_bounce_data
3939
from tests import create_authorization_header
4040
from tests.app.conftest import (
41+
create_sample_job,
4142
create_sample_notification,
4243
create_sample_template,
4344
document_download_response,
@@ -945,7 +946,7 @@ def test_team_keys_only_send_to_team_members(
945946
],
946947
)
947948
def test_team_keys_only_send_to_team_members_bulk_endpoint(
948-
self, notify_db_session, client, mocker, notification_type, to_key, to_a, to_b, response_code
949+
self, notify_db, notify_db_session, client, mocker, notification_type, to_key, to_a, to_b, response_code
949950
):
950951
service = create_service(
951952
restricted=True,
@@ -956,8 +957,8 @@ def test_team_keys_only_send_to_team_members_bulk_endpoint(
956957
service.users = [user_1, user_2]
957958
template = create_template(service=service, template_type=notification_type)
958959
create_api_key(service=service, key_type="team")
959-
job_id = str(uuid.uuid4())
960-
mocker.patch("app.v2.notifications.post_notifications.create_bulk_job", return_value=job_id)
960+
job = create_sample_job(notify_db=notify_db, notify_db_session=notify_db_session)
961+
mocker.patch("app.v2.notifications.post_notifications.create_bulk_job", return_value=job)
961962

962963
data = {
963964
"name": "job_name",
@@ -1607,7 +1608,6 @@ def test_API_BULK_post_sms_with_test_key_does_not_count_towards_limits(
16071608
):
16081609
# test setup
16091610
mocker.patch("app.sms_normal_publish.publish")
1610-
mocker.patch("app.v2.notifications.post_notifications.create_bulk_job", return_value=str(uuid.uuid4()))
16111611
increment_todays_requested_sms_count = mocker.patch("app.notifications.validators.increment_todays_requested_sms_count")
16121612

16131613
def __send_sms():
@@ -1640,7 +1640,8 @@ def __send_sms():
16401640
"template_id": str(template.id),
16411641
"rows": [["phone number"], ["+16132532222"], ["+16132532223"], ["+16132532224"]],
16421642
}
1643-
1643+
job = create_sample_job(notify_db=notify_db, notify_db_session=notify_db_session, service=service, template=template)
1644+
mocker.patch("app.v2.notifications.post_notifications.create_bulk_job", return_value=job)
16441645
response = __send_sms()
16451646

16461647
assert response.status_code == 201
@@ -1653,7 +1654,6 @@ def __send_sms():
16531654
def test_API_BULK_post_sms_with_mixed_numbers(self, notify_api, client, notify_db, notify_db_session, mocker, key_type):
16541655
# test setup
16551656
mocker.patch("app.sms_normal_publish.publish")
1656-
mocker.patch("app.v2.notifications.post_notifications.create_bulk_job", return_value=str(uuid.uuid4()))
16571657
increment_todays_requested_sms_count = mocker.patch("app.notifications.validators.increment_todays_requested_sms_count")
16581658

16591659
def __send_sms():
@@ -1681,6 +1681,9 @@ def __send_sms():
16811681
# Create a service, Set limit to 10 fragments
16821682
service = create_service(sms_daily_limit=10, message_limit=100)
16831683
template = create_sample_template(notify_db, notify_db_session, content="Hello", service=service, template_type="sms")
1684+
job = create_sample_job(notify_db=notify_db, notify_db_session=notify_db_session, service=service, template=template)
1685+
mocker.patch("app.v2.notifications.post_notifications.create_bulk_job", return_value=job)
1686+
16841687
data = {
16851688
"name": "Bulk send name",
16861689
"template_id": str(template.id),
@@ -1834,7 +1837,6 @@ def __send_sms():
18341837
def test_API_BULK_sends_warning_emails_and_blocks_sends(self, notify_api, client, notify_db, notify_db_session, mocker):
18351838
# test setup
18361839
mocker.patch("app.sms_normal_publish.publish")
1837-
mocker.patch("app.v2.notifications.post_notifications.create_bulk_job", return_value=str(uuid.uuid4()))
18381840
send_warning_email = mocker.patch("app.notifications.validators.send_near_sms_limit_email")
18391841
send_limit_reached_email = mocker.patch("app.notifications.validators.send_sms_limit_reached_email")
18401842

@@ -1861,6 +1863,8 @@ def __send_sms():
18611863

18621864
# Create 7 notifications in the db
18631865
template = create_sample_template(notify_db, notify_db_session, content="Hello", service=service, template_type="sms")
1866+
job = create_sample_job(notify_db=notify_db, notify_db_session=notify_db_session, service=service, template=template)
1867+
mocker.patch("app.v2.notifications.post_notifications.create_bulk_job", return_value=job)
18641868
for x in range(7):
18651869
create_sample_notification(notify_db, notify_db_session, service=service)
18661870

@@ -2539,6 +2543,7 @@ def test_post_bulk_creates_job_and_dispatches_celery_task(
25392543
"service": str(sample_email_template.service_id),
25402544
"service_name": {"name": sample_email_template.service.name},
25412545
"template": str(sample_email_template.id),
2546+
"template_type": str(sample_email_template.template_type),
25422547
"template_version": sample_email_template.version,
25432548
"updated_at": None,
25442549
"sender_id": str(reply_to_email.id) if use_sender_id else None,

0 commit comments

Comments
 (0)