@@ -427,6 +427,139 @@ def test_retry_noop_when_no_failed_sent_to(api_client, email_history):
427427 mock_client .send_message .assert_not_called ()
428428
429429
430+ # ---- History Retry SentTo ---------------------------------------------------
431+
432+
433+ @pytest .mark .django_db (transaction = True )
434+ def test_retry_sent_to_only_resends_specified_sent_to (api_client , email_history , email_template ):
435+ # 같은 history에 FAILED sent_to가 여러 개 있어도 retry_sent_to는 지정된 1건만 재시도.
436+ extra = EmailNotificationHistory .objects .create_for_recipients (
437+ template = email_template ,
438+ recipients = [
439+ {"recipient" : "a@example.com" , "context" : {"name" : "a" , "recipient" : "x" }},
440+ {"recipient" : "b@example.com" , "context" : {"name" : "b" , "recipient" : "x" }},
441+ ],
442+ )
443+ extra .sent_to_list .update (status = NotificationStatus .FAILED )
444+ target = extra .sent_to_list .order_by ("recipient" ).first ()
445+
446+ with patch ("notification.models.email.EmailNotificationHistory.client" ) as mock_client :
447+ response = api_client .post (
448+ reverse (
449+ "v1:admin-notification-email-history-retry-sent-to" ,
450+ kwargs = {"pk" : extra .id , "sent_to_id" : target .id },
451+ )
452+ )
453+ assert response .status_code == http .HTTPStatus .OK
454+ assert mock_client .send_message .call_count == 1
455+ target .refresh_from_db ()
456+ assert target .status == NotificationStatus .SENT
457+ other = extra .sent_to_list .exclude (pk = target .pk ).get ()
458+ assert other .status == NotificationStatus .FAILED
459+
460+
461+ @pytest .mark .django_db (transaction = True )
462+ def test_retry_sent_to_404_when_status_not_in_filter (api_client , email_history ):
463+ # 지정된 sent_to의 status가 요청 status에 포함되지 않으면 404.
464+ sent_to = email_history .sent_to_list .get () # 기본 status는 CREATED, default 필터는 [FAILED].
465+ with patch ("notification.models.email.EmailNotificationHistory.client" ) as mock_client :
466+ response = api_client .post (
467+ reverse (
468+ "v1:admin-notification-email-history-retry-sent-to" ,
469+ kwargs = {"pk" : email_history .id , "sent_to_id" : sent_to .id },
470+ )
471+ )
472+ assert response .status_code == http .HTTPStatus .NOT_FOUND
473+ mock_client .send_message .assert_not_called ()
474+
475+
476+ @pytest .mark .django_db (transaction = True )
477+ def test_retry_with_status_query_resends_matching_statuses (api_client , email_template ):
478+ # ?status=CREATED&status=FAILED → 둘 다 재발송, SENT는 제외.
479+ history = EmailNotificationHistory .objects .create_for_recipients (
480+ template = email_template ,
481+ recipients = [
482+ {"recipient" : "a@example.com" , "context" : {"name" : "a" , "recipient" : "x" }},
483+ {"recipient" : "b@example.com" , "context" : {"name" : "b" , "recipient" : "x" }},
484+ {"recipient" : "c@example.com" , "context" : {"name" : "c" , "recipient" : "x" }},
485+ ],
486+ )
487+ by_recipient = {s .recipient : s for s in history .sent_to_list .all ()}
488+ history .sent_to_list .filter (pk = by_recipient ["a@example.com" ].pk ).update (status = NotificationStatus .SENT )
489+ history .sent_to_list .filter (pk = by_recipient ["b@example.com" ].pk ).update (status = NotificationStatus .FAILED )
490+ # c@는 CREATED 그대로
491+
492+ with patch ("notification.models.email.EmailNotificationHistory.client" ) as mock_client :
493+ response = api_client .post (
494+ reverse ("v1:admin-notification-email-history-retry" , kwargs = {"pk" : history .id })
495+ + "?status=CREATED&status=FAILED"
496+ )
497+ assert response .status_code == http .HTTPStatus .OK
498+ assert mock_client .send_message .call_count == 2
499+ assert history .sent_to_list .get (pk = by_recipient ["a@example.com" ].pk ).status == NotificationStatus .SENT
500+
501+
502+ @pytest .mark .django_db (transaction = True )
503+ def test_retry_with_status_query_force_resends_sent (api_client , email_template ):
504+ # ?status=SENT → admin retry 경로는 task 가드를 우회해 실제 재발송이 일어남.
505+ history = EmailNotificationHistory .objects .create_for_recipients (
506+ template = email_template ,
507+ recipients = [{"recipient" : "a@example.com" , "context" : {"name" : "a" , "recipient" : "x" }}],
508+ )
509+ history .sent_to_list .update (status = NotificationStatus .SENT )
510+
511+ with patch ("notification.models.email.EmailNotificationHistory.client" ) as mock_client :
512+ response = api_client .post (
513+ reverse ("v1:admin-notification-email-history-retry" , kwargs = {"pk" : history .id }) + "?status=SENT"
514+ )
515+ assert response .status_code == http .HTTPStatus .OK
516+ assert mock_client .send_message .call_count == 1
517+
518+
519+ @pytest .mark .django_db
520+ def test_retry_with_invalid_status_query_returns_400 (api_client , email_history ):
521+ response = api_client .post (
522+ reverse ("v1:admin-notification-email-history-retry" , kwargs = {"pk" : email_history .id }) + "?status=BOGUS"
523+ )
524+ assert response .status_code == http .HTTPStatus .BAD_REQUEST
525+
526+
527+ @pytest .mark .django_db (transaction = True )
528+ def test_retry_sent_to_with_status_query_respects_filter (api_client , email_history ):
529+ # sent_to_id 대상의 status가 query에 포함되면 재시도, 아니면 404.
530+ sent_to = email_history .sent_to_list .get () # 기본 status는 CREATED.
531+ url = reverse (
532+ "v1:admin-notification-email-history-retry-sent-to" ,
533+ kwargs = {"pk" : email_history .id , "sent_to_id" : sent_to .id },
534+ )
535+
536+ with patch ("notification.models.email.EmailNotificationHistory.client" ) as mock_client :
537+ # CREATED가 query에 없으면 404
538+ response = api_client .post (url + "?status=FAILED" )
539+ assert response .status_code == http .HTTPStatus .NOT_FOUND
540+ assert mock_client .send_message .call_count == 0
541+ # CREATED 포함하면 발송 O
542+ response = api_client .post (url + "?status=CREATED&status=FAILED" )
543+ assert response .status_code == http .HTTPStatus .OK
544+ assert mock_client .send_message .call_count == 1
545+
546+
547+ @pytest .mark .django_db
548+ def test_retry_sent_to_404_when_sent_to_belongs_to_other_history (api_client , email_history , email_template ):
549+ other = EmailNotificationHistory .objects .create_for_recipients (
550+ template = email_template ,
551+ recipients = [{"recipient" : "other@example.com" , "context" : {"name" : "다른" , "recipient" : "y" }}],
552+ )
553+ other_sent_to = other .sent_to_list .get ()
554+ response = api_client .post (
555+ reverse (
556+ "v1:admin-notification-email-history-retry-sent-to" ,
557+ kwargs = {"pk" : email_history .id , "sent_to_id" : other_sent_to .id },
558+ )
559+ )
560+ assert response .status_code == http .HTTPStatus .NOT_FOUND
561+
562+
430563# ---- History Render SentTo As HTML -----------------------------------------
431564
432565
0 commit comments