Skip to content

Commit 024c538

Browse files
authored
Send notification to admins when secure directory request is ready for processing (#739)
* Send admins email when secure dir request is ready for processing * Add unit, component Pytest tests for email to admins re: secure directory ready for processing
1 parent 8a80f28 commit 024c538

8 files changed

Lines changed: 421 additions & 0 deletions

File tree

bootstrap/ansible/main.copyme

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ email_admin_notification_recipients:
291291
created: []
292292
secure_directory_requests:
293293
agreement_uploaded: []
294+
approved: []
294295
created: []
295296
secure_directory_add_user_requests:
296297
created: []

coldfront/core/allocation/tests/pytest/test_views/__init__.py

Whitespace-only changes.

coldfront/core/allocation/tests/pytest/test_views/test_secure_dir_views/__init__.py

Whitespace-only changes.

coldfront/core/allocation/tests/pytest/test_views/test_secure_dir_views/test_new_directory_views/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
"""Unit and component tests for SecureDirRequestReviewMOUView.
2+
3+
Component tests for secure directory MOU review view's email
4+
sending functionality.
5+
6+
Unit tests for the _conditionally_send_ready_for_processing_email()
7+
method.
8+
"""
9+
10+
import pytest
11+
from unittest.mock import patch, MagicMock
12+
from django.urls import reverse
13+
from django.contrib.auth import get_user_model
14+
15+
from coldfront.core.allocation.models import (
16+
SecureDirRequest, SecureDirRequestStatusChoice,
17+
)
18+
from coldfront.core.project.models import (
19+
Project, ProjectStatusChoice, ProjectUser,
20+
ProjectUserStatusChoice, ProjectUserRoleChoice,
21+
)
22+
from coldfront.core.user.models import UserProfile
23+
from coldfront.core.utils.common import utc_now_offset_aware
24+
from flags.state import enable_flag
25+
26+
User = get_user_model()
27+
28+
29+
@pytest.fixture
30+
def superuser(db):
31+
"""Create a superuser for testing."""
32+
user = User.objects.create_superuser(
33+
username='admin',
34+
email='admin@test.com'
35+
)
36+
user.set_password('adminpass')
37+
user.save()
38+
return user
39+
40+
41+
@pytest.fixture
42+
def pi(db):
43+
"""Create a PI user for testing."""
44+
pi_user = User.objects.create_user(
45+
username='pi',
46+
email='pi@test.com'
47+
)
48+
pi_user.set_password('pipass')
49+
pi_user.save()
50+
user_profile = UserProfile.objects.get(user=pi_user)
51+
user_profile.is_pi = True
52+
user_profile.save()
53+
return pi_user
54+
55+
56+
@pytest.fixture
57+
def project(db):
58+
"""Create a project for testing."""
59+
project_status = ProjectStatusChoice.objects.get(
60+
name='Active'
61+
)
62+
return Project.objects.create(
63+
name='test_project',
64+
status=project_status
65+
)
66+
67+
68+
@pytest.fixture
69+
def project_with_pi(db, pi, project):
70+
"""Add PI to project."""
71+
project_user_status = ProjectUserStatusChoice.objects.get(
72+
name='Active'
73+
)
74+
pi_role = ProjectUserRoleChoice.objects.get(
75+
name='Principal Investigator'
76+
)
77+
ProjectUser.objects.create(
78+
user=pi,
79+
project=project,
80+
role=pi_role,
81+
status=project_user_status,
82+
enable_notifications=True
83+
)
84+
return project
85+
86+
87+
def _create_secure_dir_request(
88+
db,
89+
project,
90+
requester,
91+
pi,
92+
rdm_consultation_status='Pending'
93+
):
94+
"""Helper to create a SecureDirRequest.
95+
96+
Args:
97+
db: pytest database fixture
98+
project: Project object
99+
requester: User object for the requester
100+
pi: User object for the PI
101+
rdm_consultation_status: Initial RDM consultation status
102+
"""
103+
request_obj = SecureDirRequest.objects.create(
104+
directory_name='test_dir',
105+
requester=requester,
106+
pi=pi,
107+
data_description='a' * 20,
108+
project=project,
109+
status=SecureDirRequestStatusChoice.objects.get(
110+
name='Under Review'
111+
),
112+
request_time=utc_now_offset_aware()
113+
)
114+
115+
# Initialize state with all required keys
116+
request_obj.state = {
117+
'rdm_consultation': {
118+
'status': rdm_consultation_status,
119+
'justification': '',
120+
'timestamp': ''
121+
},
122+
'notified': {
123+
'status': 'Pending',
124+
'timestamp': ''
125+
},
126+
'mou': {
127+
'status': 'Pending',
128+
'justification': '',
129+
'timestamp': ''
130+
},
131+
'setup': {
132+
'status': 'Pending',
133+
'justification': '',
134+
'timestamp': ''
135+
},
136+
'other': {
137+
'justification': '',
138+
'timestamp': ''
139+
}
140+
}
141+
request_obj.save()
142+
return request_obj
143+
144+
145+
@pytest.mark.django_db
146+
class TestSecureDirRequestReviewMOUViewEmailSending:
147+
"""Test email sending in SecureDirRequestReviewMOUView."""
148+
149+
@pytest.fixture(autouse=True)
150+
def setup(self, db, superuser, pi, project_with_pi):
151+
"""Set up test fixtures."""
152+
enable_flag('SECURE_DIRS_REQUESTABLE')
153+
self.superuser = superuser
154+
self.pi = pi
155+
self.project = project_with_pi
156+
157+
@pytest.fixture
158+
def secure_dir_request_for_mou(self):
159+
"""Request with RDM consultation already approved."""
160+
return _create_secure_dir_request(
161+
None,
162+
self.project,
163+
self.pi,
164+
self.pi,
165+
rdm_consultation_status='Approved'
166+
)
167+
168+
def test_email_sent_when_mou_approved(
169+
self, client, secure_dir_request_for_mou
170+
):
171+
"""Email sent to admins when MOU is approved."""
172+
with patch(
173+
'coldfront.core.allocation.views_.'
174+
'secure_dir_views.new_directory.approval_views.'
175+
'send_secure_directory_request_ready_for_processing_email'
176+
) as mock_send:
177+
client.force_login(self.superuser)
178+
url = reverse(
179+
'secure-dir-request-review-mou',
180+
kwargs={'pk': secure_dir_request_for_mou.pk}
181+
)
182+
data = {
183+
'status': 'Approved',
184+
'justification': 'Test justification'
185+
}
186+
response = client.post(url, data)
187+
188+
# Verify redirect
189+
success_url = reverse(
190+
'secure-dir-request-detail',
191+
kwargs={'pk': secure_dir_request_for_mou.pk}
192+
)
193+
assert response.status_code == 302
194+
assert response.url == success_url
195+
196+
# Verify request status updated
197+
secure_dir_request_for_mou.refresh_from_db()
198+
assert (
199+
secure_dir_request_for_mou.status.name ==
200+
'Approved - Processing'
201+
)
202+
203+
# Verify email sent
204+
mock_send.assert_called_once_with(
205+
secure_dir_request_for_mou
206+
)
207+
208+
def test_email_not_sent_if_rdm_pending(
209+
self, client, superuser, pi, project_with_pi
210+
):
211+
"""Email not sent if RDM consultation still pending."""
212+
# Create request with RDM consultation pending
213+
request_obj = _create_secure_dir_request(
214+
None,
215+
project_with_pi,
216+
pi,
217+
pi,
218+
rdm_consultation_status='Pending'
219+
)
220+
221+
with patch(
222+
'coldfront.core.allocation.views_.'
223+
'secure_dir_views.new_directory.approval_views.'
224+
'send_secure_directory_request_ready_for_processing_email'
225+
) as mock_send:
226+
client.force_login(superuser)
227+
url = reverse(
228+
'secure-dir-request-review-mou',
229+
kwargs={'pk': request_obj.pk}
230+
)
231+
data = {
232+
'status': 'Approved',
233+
'justification': 'Test justification'
234+
}
235+
response = client.post(url, data)
236+
237+
# Status should remain 'Under Review' (not
238+
# 'Approved - Processing') because RDM is still pending
239+
request_obj.refresh_from_db()
240+
assert request_obj.status.name == 'Under Review'
241+
242+
# Email should NOT be sent
243+
mock_send.assert_not_called()
244+
245+
def test_email_not_sent_if_denied(
246+
self, client, superuser, pi, project_with_pi
247+
):
248+
"""Email not sent if MOU is denied."""
249+
# Create request with RDM consultation approved
250+
request_obj = _create_secure_dir_request(
251+
None,
252+
project_with_pi,
253+
pi,
254+
pi,
255+
rdm_consultation_status='Approved'
256+
)
257+
258+
with patch(
259+
'coldfront.core.allocation.views_.'
260+
'secure_dir_views.new_directory.approval_views.'
261+
'send_secure_directory_request_ready_for_processing_email'
262+
) as mock_send:
263+
client.force_login(superuser)
264+
url = reverse(
265+
'secure-dir-request-review-mou',
266+
kwargs={'pk': request_obj.pk}
267+
)
268+
data = {
269+
'status': 'Denied',
270+
'justification': 'Test denial'
271+
}
272+
response = client.post(url, data)
273+
274+
# Status should be 'Denied' (not 'Approved - Processing')
275+
request_obj.refresh_from_db()
276+
assert request_obj.status.name == 'Denied'
277+
278+
# Email should NOT be sent
279+
mock_send.assert_not_called()
280+
281+
def test_permission_required(
282+
self, client, pi, secure_dir_request_for_mou
283+
):
284+
"""Only superusers can access MOU review view."""
285+
client.force_login(pi)
286+
url = reverse(
287+
'secure-dir-request-review-mou',
288+
kwargs={'pk': secure_dir_request_for_mou.pk}
289+
)
290+
response = client.get(url)
291+
assert response.status_code == 403
292+
293+
294+
@pytest.mark.unit
295+
class TestSecureDirRequestMixinEmailMethod:
296+
"""Unit tests for
297+
_conditionally_send_ready_for_processing_email()."""
298+
299+
@patch(
300+
'coldfront.core.allocation.views_.'
301+
'secure_dir_views.new_directory.approval_views.'
302+
'send_secure_directory_request_ready_for_processing_email'
303+
)
304+
def test_sends_email_if_status_is_approved_processing(
305+
self, mock_send
306+
):
307+
"""Email sent when status is Approved - Processing."""
308+
from coldfront.core.allocation.views_\
309+
.secure_dir_views.new_directory.approval_views\
310+
import SecureDirRequestReviewMOUView
311+
312+
view = SecureDirRequestReviewMOUView()
313+
view.request_obj = MagicMock()
314+
view.request_obj.status.name = 'Approved - Processing'
315+
316+
view._conditionally_send_ready_for_processing_email()
317+
318+
mock_send.assert_called_once_with(view.request_obj)
319+
320+
@patch(
321+
'coldfront.core.allocation.views_.'
322+
'secure_dir_views.new_directory.approval_views.'
323+
'logger'
324+
)
325+
@patch(
326+
'coldfront.core.allocation.views_.'
327+
'secure_dir_views.new_directory.approval_views.'
328+
'send_secure_directory_request_ready_for_processing_email'
329+
)
330+
def test_handles_email_send_exception_gracefully(
331+
self, mock_send, mock_logger
332+
):
333+
"""Exceptions during email send are logged."""
334+
from coldfront.core.allocation.views_\
335+
.secure_dir_views.new_directory.approval_views\
336+
import SecureDirRequestReviewMOUView
337+
338+
view = SecureDirRequestReviewMOUView()
339+
view.request_obj = MagicMock()
340+
view.request_obj.status.name = 'Approved - Processing'
341+
342+
test_exception = ValueError('Test error')
343+
mock_send.side_effect = test_exception
344+
345+
# Should not raise, but log the exception
346+
view._conditionally_send_ready_for_processing_email()
347+
348+
mock_logger.exception.assert_called_once()
349+
350+
@pytest.mark.parametrize('status_name', [
351+
'Under Review',
352+
'Approved - Complete',
353+
'Denied'
354+
])
355+
@patch(
356+
'coldfront.core.allocation.views_.'
357+
'secure_dir_views.new_directory.approval_views.'
358+
'send_secure_directory_request_ready_for_processing_email'
359+
)
360+
def test_does_not_send_email_for_non_processing_statuses(
361+
self, mock_send, status_name
362+
):
363+
"""Email not sent for statuses other than
364+
Approved - Processing."""
365+
from coldfront.core.allocation.views_\
366+
.secure_dir_views.new_directory.approval_views\
367+
import SecureDirRequestReviewMOUView
368+
369+
view = SecureDirRequestReviewMOUView()
370+
view.request_obj = MagicMock()
371+
view.request_obj.status.name = status_name
372+
373+
view._conditionally_send_ready_for_processing_email()
374+
375+
mock_send.assert_not_called()

0 commit comments

Comments
 (0)