@@ -255,23 +255,38 @@ def run_celery_tasks():
255255
256256
257257@contextlib .contextmanager
258- def capture_notifications ():
258+ def capture_notifications (capture_email : bool = True ):
259259 """
260- Context manager to capture NotificationType emits without interfering with ORM calls.
261- Yields a list of captured emits:
262- [{'type': <NotificationType.Type>, 'args': ..., 'kwargs': ...}, ...]
260+ Context manager to capture NotificationType emits without interfering with ORM calls
261+ and (optionally) stub out actual email sending so tests don't open sockets.
262+
263+ Yields a dict with two lists:
264+ {
265+ "emits": [
266+ {"type": <str name>, "args": tuple, "kwargs": dict}, ...
267+ ],
268+ "emails": [
269+ {
270+ "protocol": "smtp" | "sendgrid",
271+ "to": <str or user object>,
272+ "notification_type": <NotificationType or str>,
273+ "context": <dict>,
274+ "email_context": <dict>
275+ }, ...
276+ ]
277+ }
263278 """
264279 NotificationType = apps .get_model ('osf' , 'NotificationType' )
265280 real_get = NotificationType .objects .get # Save the real .get()
266281
267- captured = []
282+ captured = { 'emits' : [], 'emails' : []}
268283
269284 def side_effect (* args , ** kwargs ):
270285 notifier = real_get (* args , ** kwargs ) # Call the real .get()
271286 original_emit = notifier .emit
272287
273288 def wrapped_emit (* emit_args , ** emit_kwargs ):
274- captured .append ({
289+ captured [ 'emits' ] .append ({
275290 'type' : notifier .name ,
276291 'args' : emit_args ,
277292 'kwargs' : emit_kwargs
@@ -281,7 +296,34 @@ def wrapped_emit(*emit_args, **emit_kwargs):
281296 notifier .emit = wrapped_emit
282297 return notifier
283298
284- with mock .patch ('osf.models.notification_type.NotificationType.objects.get' , side_effect = side_effect ):
285- yield captured
299+ patches = [
300+ mock .patch ('osf.models.notification_type.NotificationType.objects.get' , side_effect = side_effect ),
301+ ]
302+ if capture_email :
303+ def _fake_send_over_smtp (to_email , notification_type , context , email_context ):
304+ captured ['emails' ].append ({
305+ 'protocol' : 'smtp' ,
306+ 'to' : to_email ,
307+ 'notification_type' : notification_type ,
308+ 'context' : context ,
309+ 'email_context' : email_context ,
310+ })
286311
312+ def _fake_send_with_sendgrid (user , notification_type , context , email_context ):
313+ captured ['emails' ].append ({
314+ 'protocol' : 'sendgrid' ,
315+ 'to' : user , # keeping the object for tests that assert user props
316+ 'notification_type' : notification_type ,
317+ 'context' : context ,
318+ 'email_context' : email_context ,
319+ })
287320
321+ patches .extend ([
322+ mock .patch ('osf.email.send_email_over_smtp' , new = _fake_send_over_smtp ),
323+ mock .patch ('osf.email.send_email_with_send_grid' , new = _fake_send_with_sendgrid ),
324+ ])
325+
326+ with contextlib .ExitStack () as stack :
327+ for p in patches :
328+ stack .enter_context (p )
329+ yield captured
0 commit comments