firefighter-incident 0.0.26__py3-none-any.whl → 0.0.27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. firefighter/_version.py +2 -2
  2. firefighter/confluence/models.py +16 -1
  3. firefighter/incidents/management/__init__.py +1 -0
  4. firefighter/incidents/management/commands/__init__.py +1 -0
  5. firefighter/incidents/management/commands/backdate_incident_mitigated.py +94 -0
  6. firefighter/incidents/management/commands/test_postmortem_reminders.py +113 -0
  7. firefighter/incidents/migrations/0030_add_mitigated_at_field.py +22 -0
  8. firefighter/incidents/models/incident.py +43 -8
  9. firefighter/jira_app/service_postmortem.py +13 -0
  10. firefighter/jira_app/signals/postmortem_created.py +108 -46
  11. firefighter/slack/messages/slack_messages.py +162 -0
  12. firefighter/slack/migrations/0009_add_postmortem_reminder_periodic_task.py +60 -0
  13. firefighter/slack/rules.py +22 -0
  14. firefighter/slack/tasks/send_postmortem_reminders.py +127 -0
  15. firefighter/slack/views/modals/close.py +113 -3
  16. firefighter/slack/views/modals/closure_reason.py +39 -15
  17. firefighter/slack/views/modals/update_status.py +4 -4
  18. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.27.dist-info}/METADATA +1 -1
  19. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.27.dist-info}/RECORD +29 -22
  20. firefighter_tests/test_incidents/test_incident_urls.py +4 -0
  21. firefighter_tests/test_incidents/test_models/test_incident_model.py +109 -1
  22. firefighter_tests/test_incidents/test_views/test_incident_detail_view.py +4 -0
  23. firefighter_tests/test_slack/messages/test_slack_messages.py +4 -0
  24. firefighter_tests/test_slack/views/modals/test_close.py +4 -0
  25. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +109 -26
  26. firefighter_tests/test_slack/views/modals/test_update_status.py +45 -51
  27. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.27.dist-info}/WHEEL +0 -0
  28. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.27.dist-info}/entry_points.txt +0 -0
  29. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.27.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ gunicorn.conf.py,sha256=vHsTGjaKOr8FDMp6fTKYTX4AtokmPgYvvt5Mr0Q6APc,273
6
6
  main.py,sha256=CsbprHoOYhjCLpTJmq9Z_aRYFoFgWxoz2pDLuwm8Eqg,1558
7
7
  manage.py,sha256=5ivHGD13C6nJ8QvltKsJ9T9akA5he8da70HLWaEP3k8,689
8
8
  firefighter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- firefighter/_version.py,sha256=Fnf259POysYSI-aH6DhnxuHUxCVOFWsQLs4I5Ur_KMA,706
9
+ firefighter/_version.py,sha256=vq5GihUa4KrDz-Hix5Er9gtVMhfHNDBoLhzP1VsvNuE,706
10
10
  firefighter/api/__init__.py,sha256=JQW0Bv6xwGqy7ioxx3h6UGMzkkJ4DntDpbvV1Ncgi8k,136
11
11
  firefighter/api/admin.py,sha256=x9Ysy-GiYjb0rynmFdS9g56e6n24fkN0ouGy5QD9Yrc,4629
12
12
  firefighter/api/apps.py,sha256=P5uU1_gMrDfzurdMbfqw1Bnb2uNKKcMq17WBPg2sLhc,204
@@ -55,7 +55,7 @@ firefighter/confluence/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
55
55
  firefighter/confluence/admin.py,sha256=aDXghuuLc7G_TLt-655M31smx-H6vkIgLtEmmNCA3lg,1490
56
56
  firefighter/confluence/apps.py,sha256=vKswBwQL7L9e2JQwvRb7xy3myyE_GRldYX78jSY3XCM,406
57
57
  firefighter/confluence/client.py,sha256=xjSsrsGPF75JANNvam2YgiUkztuXhOcs9pMmPbb7ymk,6361
58
- firefighter/confluence/models.py,sha256=ExFip6Tw199jI5LVLqC8t4pOPsvQxfEhtQeWCqTijFw,8769
58
+ firefighter/confluence/models.py,sha256=66RNfD-lRdixZtOo5pNW2e-LZboTRjdoH2h-R7ne-q0,9154
59
59
  firefighter/confluence/serializers.py,sha256=CzuHVXIJNS47NCAJLXSTDOevtg5sf309XXEcWKQ1sAQ,258
60
60
  firefighter/confluence/service.py,sha256=dOQXj0uDInEm25nvL6lXiSH4hQ5oC2VDyBd1zbEcZ5U,12296
61
61
  firefighter/confluence/tables.py,sha256=ANEtFXzXyPK6E5FIrBC5XoQt5R3ZUY1DME_RbD1h_NE,732
@@ -147,6 +147,10 @@ firefighter/incidents/forms/update_key_events.py,sha256=1Xmnxe5OgZqLFS2HmMzQm3VG
147
147
  firefighter/incidents/forms/update_roles.py,sha256=Q26UPfwAj-8N23RNZLQkvmHGnS1_j_X5KQWjJmPjMKY,3635
148
148
  firefighter/incidents/forms/update_status.py,sha256=7GSno_EqD2Brd6wWcSb3zsP6nz8_mUTXXnl0QCRhv48,6682
149
149
  firefighter/incidents/forms/utils.py,sha256=15e_dBebVd9SvX03DYd0FyZ8s0YpxyBlZfIzEZattwg,4267
150
+ firefighter/incidents/management/__init__.py,sha256=A2LtnedT5NvTcNAN5nXMkPwK56JBNLuptcyObvq7zcc,40
151
+ firefighter/incidents/management/commands/__init__.py,sha256=wc5DFEklUo-wB-6VAAmsV5UTbo5s3t936Lu61z4lojs,29
152
+ firefighter/incidents/management/commands/backdate_incident_mitigated.py,sha256=phAXH18TNvzA03o1XtJfRVeOPbrKp8wdsBMx-QGAIeo,3410
153
+ firefighter/incidents/management/commands/test_postmortem_reminders.py,sha256=Bx-AVhkSjkL6c2_Eh7mRHa7qOEtytDl9T2hJYxcBC-4,4233
150
154
  firefighter/incidents/migrations/0001_initial_oss.py,sha256=OCrPbxf90h3NW9xolGGcsAryHKptD1TtKj5FucjBjg8,60311
151
155
  firefighter/incidents/migrations/0002_alter_severity_name_alter_user_password_featureteam.py,sha256=YfIJhw_-Yqm8qrkbp01461bkcUr7v5Zy90oHjkY3bSA,1113
152
156
  firefighter/incidents/migrations/0003_delete_featureteam.py,sha256=kH5UUSx3k5DtjR_goDxROdV0htCC2JZfBGwJpn-dEQs,336
@@ -176,12 +180,13 @@ firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py
176
180
  firefighter/incidents/migrations/0027_add_closure_fields.py,sha256=MDWckXmjJNC2iVoFJD6IIwDmmqyeL1VG_pHR568JAtk,1344
177
181
  firefighter/incidents/migrations/0028_add_closure_reason_constraint.py,sha256=z6FjCURDt9c-hyBeCvCKsbZOiuReYtbjtguIh3T6dnk,920
178
182
  firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py,sha256=G6DsnP5bM4Hy0s8IqXhLYzFKt3eumEsCnJfPIw5tcX4,567
183
+ firefighter/incidents/migrations/0030_add_mitigated_at_field.py,sha256=pELNJWbAuctv_dA-pdD3fsqg4qMm5f6upW9hFrjNLDI,546
179
184
  firefighter/incidents/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
180
185
  firefighter/incidents/models/__init__.py,sha256=FLVyBwIdyxLdgSvXRAKC3fry9YwwqlqhitTIuG0vWrk,877
181
186
  firefighter/incidents/models/environment.py,sha256=51txwua3dCrWZ1iSG3ZA8rbDn9c00pyMAZujl9gwE5c,827
182
187
  firefighter/incidents/models/group.py,sha256=VrVL315VFUvKW69AZuRUBg1h0jZJvn8zWeMxMOWec1Y,700
183
188
  firefighter/incidents/models/impact.py,sha256=D9NngMtg4XdDWnMgdVYaWCoUZ-fMXTvfL0eTEk9sc7M,4854
184
- firefighter/incidents/models/incident.py,sha256=UdL9bc-3Ou-OS8P-nHO4YF6-Hk0mDkGNq1zjOq-TNFk,28022
189
+ firefighter/incidents/models/incident.py,sha256=4G4vLurh2bgMRo5eOuYgKWNRC8Xf_07cFf_UdpLPOOg,29638
185
190
  firefighter/incidents/models/incident_category.py,sha256=g4OHv_XQhWcH6dvkqkyCgjlruo_1eih_CdtAPgPhaW4,7744
186
191
  firefighter/incidents/models/incident_cost.py,sha256=juwOfJKRaNQpOHkRUCHShDDba0FU98YjRPkU4I0ofAU,1346
187
192
  firefighter/incidents/models/incident_cost_type.py,sha256=wm8diry_VySJzIjC9M3Yavv2tYbvJgpN9UDb2gFRuH4,845
@@ -273,7 +278,7 @@ firefighter/jira_app/admin.py,sha256=ZHAAbhy0hm_DcklK59KMmid_ZiPn8n5V6g7cZCSNrpc
273
278
  firefighter/jira_app/apps.py,sha256=T6vHrQuMZHJoTth-xjy3CbNfPv6DyXgcR3PSMju2JS4,504
274
279
  firefighter/jira_app/client.py,sha256=qpMqNTjJUq5OqAxmwvVOE20uJe7kp737HSdsiqUu1G4,21982
275
280
  firefighter/jira_app/models.py,sha256=2zKy5VaKkhiHYA8Dukz8g0NTG82Qy5UHAHY9eMv67NE,3097
276
- firefighter/jira_app/service_postmortem.py,sha256=7VQwtVGCVZaVGfRxQXL19IM4hHrgHmItUbrsyy2gZlM,10764
281
+ firefighter/jira_app/service_postmortem.py,sha256=tqJN91vZPX5ISd_PAGWDebHK225eZvDvwhi7ONy_D-A,11325
277
282
  firefighter/jira_app/types.py,sha256=Ukak1U1EhcH2jQPN-UoEL6AMZ-kzPsQ8c7FUr7GmahE,956
278
283
  firefighter/jira_app/utils.py,sha256=3xuzr8viZCBm6j2J9oFzA4bUvVW8TN1DOdlpbruJ_TE,3443
279
284
  firefighter/jira_app/management/__init__.py,sha256=wy4qMZb7_K-INwwGGEhMtEeI0XTLqgUw4P8_-VEnrEw,40
@@ -283,7 +288,7 @@ firefighter/jira_app/migrations/0002_add_jira_postmortem_model.py,sha256=oFSbYNc
283
288
  firefighter/jira_app/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
284
289
  firefighter/jira_app/signals/__init__.py,sha256=OpXFlbRgIrh73DGlUzQ6WUTedKsD5wYW9bxGMq_DnIs,325
285
290
  firefighter/jira_app/signals/incident_key_events_updated.py,sha256=uaV3MON1QzeOZizzAwSdyktBwe2mWxHJeNSvy9MYc3k,3204
286
- firefighter/jira_app/signals/postmortem_created.py,sha256=eOTQKcjspL-JHvXiCu-U1FzrqIr6OJ20GOK6gJ4dojw,5904
291
+ firefighter/jira_app/signals/postmortem_created.py,sha256=S7sKbEgo5RroC5ji1OepAT3HMckwS120dpKeUteNaXA,8300
287
292
  firefighter/jira_app/tasks/__init__.py,sha256=XLCPkolM6LwIUGv0MNbk_0lCuBHyzgRFHsE3vTRD5ds,86
288
293
  firefighter/jira_app/tasks/sync_users_jira.py,sha256=sSSLsVCdzkPNRS6Gt8j0YwCTuoRqkJAJLxDBu7IElmM,1437
289
294
  firefighter/jira_app/templates/jira/postmortem/impact.txt,sha256=eYlX0rytUiKZZIrWxMN23QXkLZ8JwlwPe5S2oiqZyFA,259
@@ -345,7 +350,7 @@ firefighter/slack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
345
350
  firefighter/slack/admin.py,sha256=pNJbA-szxUUrghxv_Z0BNezu6lULDzFcOu_K5i4m7Cs,13963
346
351
  firefighter/slack/apps.py,sha256=gR0zWTtqT58tjPayBX22ZSzMkLiNpmoOvLShNvhJA6Q,664
347
352
  firefighter/slack/factories.py,sha256=tnrUTbtgehCuBr24MtTyJ3uezKC6gJbOdHuYZ5JBoyU,3886
348
- firefighter/slack/rules.py,sha256=PHbqi8sSTZi60TaahLfwdVr1B3GG_Rz1ldAT5q3XvG4,1651
353
+ firefighter/slack/rules.py,sha256=Y-DYJ_1D13a4nQNESCbhalNe2nC9xVNpYtSeWTgDcYc,2374
349
354
  firefighter/slack/slack_app.py,sha256=mvaH0hPFaNIUxEB7J0fy6y-PNPGsdPqjXFVmpTQ_hCo,4201
350
355
  firefighter/slack/slack_incident_context.py,sha256=PjE7-w-pGFyV4faw8EMsEFp4RG_T251RhofmqrsDG7Q,7277
351
356
  firefighter/slack/slack_templating.py,sha256=rWe8m1n648wizw08U_vLz8daRnp4zmkcWRqocIBpQj4,3841
@@ -359,7 +364,7 @@ firefighter/slack/management/commands/generate_manifest.py,sha256=zFWHAC7ioozcDd
359
364
  firefighter/slack/management/commands/switch_test_users.py,sha256=2KTSvCBxsEvZa61J8p0r3huPNhwuytcj2J7IawwZWpQ,11064
360
365
  firefighter/slack/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
361
366
  firefighter/slack/messages/base.py,sha256=biH-YEAaldJ-OLHEs5ZjW-gtUYUbjOqxrAEflqV2XS0,4593
362
- firefighter/slack/messages/slack_messages.py,sha256=xvkV_sEbBRkyqGigR_oQeC6Cr9KLWqqwxDXmPywyEok,35553
367
+ firefighter/slack/messages/slack_messages.py,sha256=eCJUPSjzF_itXH9Z0VlfhloMNvjN3tsB1lmziO9Ksg0,41469
363
368
  firefighter/slack/migrations/0001_initial_oss.py,sha256=XmTPgq7zCME2xDwzRFoVi4OegSIG9eSKoyTNoW05Qtg,12933
364
369
  firefighter/slack/migrations/0002_usergroup_tag.py,sha256=098tmGA81mT-R2uhb6uQfZ7gKiRG9bFhEwQ8rrp4SKM,583
365
370
  firefighter/slack/migrations/0003_alter_usergroup_tag.py,sha256=ncH3KUWEPZHlbdcAtOJ0KGt5H6EX-cKspTGU3osrAhE,591
@@ -368,6 +373,7 @@ firefighter/slack/migrations/0005_add_incident_categories_fields.py,sha256=KMdKf
368
373
  firefighter/slack/migrations/0006_copy_components_to_incident_categories.py,sha256=xUF7lLyWERux6SyIYHK2Uk1Yb4QLCGTaHW_KXVqX8n4,2478
369
374
  firefighter/slack/migrations/0007_remove_components_fields.py,sha256=_GXmcpB3enpVBT1NZ-tGDlh16r_cM-JkH2gebrmwIOs,563
370
375
  firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py,sha256=yzuAnunYvlF-wcYd7oe5h-kL5aOoawSXv_QGfmTFoBo,1034
376
+ firefighter/slack/migrations/0009_add_postmortem_reminder_periodic_task.py,sha256=Vze5TvhQExaF7-KymByzMZZE1wa0GHlWZXP1v1hub5w,2035
371
377
  firefighter/slack/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
372
378
  firefighter/slack/models/__init__.py,sha256=MGc4yuDnVhmAiHy1-5rjaLIfVv9JOup5arRutcUs8Ak,332
373
379
  firefighter/slack/models/conversation.py,sha256=f7a0muD0lrpf8mIhF6E2gEhNsgwZFw9jlKIQppZhNL0,16227
@@ -388,6 +394,7 @@ firefighter/slack/tasks/__init__.py,sha256=28QxZkakyi9l7Ae83fQuzOS-9EaBiwuh_peUZ
388
394
  firefighter/slack/tasks/fetch_conversations_members.py,sha256=lLQ491_l8HEJrjoDpD0AETqoFUFogkyMJ002hguA-Dg,5381
389
395
  firefighter/slack/tasks/reminder_postmortem.py,sha256=mZvT4cpzmMhC6JrWhZb1uFvTJJrbkEKgcCy6liKHrKM,2322
390
396
  firefighter/slack/tasks/send_message.py,sha256=N0FIE93bUnzbHdWkSWC-4-eLn737u6hHq8F8erMu8kI,810
397
+ firefighter/slack/tasks/send_postmortem_reminders.py,sha256=uyF9v8um2uxuYFyDSe2GhmG3iX0iAJrG1yE3LS0_NSo,4741
391
398
  firefighter/slack/tasks/send_reminders.py,sha256=hy1Q_rG2RUQdXNYEYiLyLnT7rkG8PFOmxur62YCCDrk,4370
392
399
  firefighter/slack/tasks/sync_users.py,sha256=T5ytYnZpcUqrh4sOklxWttsUk82C_2bwayg_fdcdg1g,2391
393
400
  firefighter/slack/tasks/update_usergroups_members.py,sha256=W-rPt3r2c9UboVMNiyQFvi_W7XWrR8ireVBWcCJMj5A,4642
@@ -410,8 +417,8 @@ firefighter/slack/views/events/message.py,sha256=c8tvo0btOUu_5Bc83oiO3IQbaEyoRiU
410
417
  firefighter/slack/views/events/message_deleted.py,sha256=tyA1-sAlG9ImcKIhqSn6EgujHmbvj4Uw2QzQ4JH4QwI,747
411
418
  firefighter/slack/views/events/reaction_added.py,sha256=AipwBnrU5B35D97YIZCXdSW8W7-9QTIIQqUcrLTLQ5c,4241
412
419
  firefighter/slack/views/modals/__init__.py,sha256=U9PapAIlpuYqBonOUmBGWT8_HjQa35ilMQJXGaFLgd0,1945
413
- firefighter/slack/views/modals/close.py,sha256=4j5iA-lmIFuCz7B9pgDmjxrqmfWFysqWEn1YIsE75zc,12161
414
- firefighter/slack/views/modals/closure_reason.py,sha256=N-gp0E6W8Z1d4aH1-8BxizNTlZ4syNIF4l5B9WZHFFA,7898
420
+ firefighter/slack/views/modals/close.py,sha256=eTT1IVEMXUrdUXsu5VSmC6-cGUsOYUGG7CN-3yAnuDo,17482
421
+ firefighter/slack/views/modals/closure_reason.py,sha256=qgjg6x5JBh5ggR6Z6aVhpnc3k8En38UxKtFb5RkTU4U,9016
415
422
  firefighter/slack/views/modals/downgrade_workflow.py,sha256=cRWsm3DmKRRI1-Jpjprb5xeY2U7HvRo6eZlUbGuzr1A,3192
416
423
  firefighter/slack/views/modals/edit.py,sha256=1N0OBSxsDuN6lJoH-djbEljy7f0LcDEpJF-U5YoEFXA,5895
417
424
  firefighter/slack/views/modals/key_event_message.py,sha256=C6yhQLQ6jBuhIr-YAoAyt-qZKu0V6nJMGZ_t3DLtUbo,5943
@@ -423,7 +430,7 @@ firefighter/slack/views/modals/status.py,sha256=C8-eJRtquSeaHe568SC7yCFef1k14m2_
423
430
  firefighter/slack/views/modals/trigger_oncall.py,sha256=h_LAD5X5rjMFWiDYTEp5VB9OaF7sTvKZhNaW3KQkw5M,5065
424
431
  firefighter/slack/views/modals/update.py,sha256=OF9sf-Z6IiviNmjN28MQNYiUbJ5tha0MdHUQyPpVFiY,2150
425
432
  firefighter/slack/views/modals/update_roles.py,sha256=De3Gv67MZQHyNdonX3S99F5MtKF_Rj3y71gdWibxBaM,2419
426
- firefighter/slack/views/modals/update_status.py,sha256=kJYcyruEwo7hok-z4Ge8HPxLMc-XPm-OrB7X_faNd7U,6000
433
+ firefighter/slack/views/modals/update_status.py,sha256=SYjQLXnWSZLk461b-L9WFRSxy7clIA4O0C4ZMrNWiuc,5964
427
434
  firefighter/slack/views/modals/utils.py,sha256=zKLJD2KhTGcX2d9WCYwshYRa6ok_9-ED1_pgOLp028s,2133
428
435
  firefighter/slack/views/modals/base_modal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
429
436
  firefighter/slack/views/modals/base_modal/base.py,sha256=7mvOxZTtegSmitSMnDvu8BK0qLUXoudUsda6CaLjdkY,12479
@@ -459,7 +466,7 @@ firefighter_tests/test_firefighter/test_logging.py,sha256=4HUH73vLDwmOCpMiXwDasM
459
466
  firefighter_tests/test_firefighter/test_sso.py,sha256=uX2ry0REDgXzQc9Y1BmAgI0OgbmzWoOv9H_GDyOqQmQ,5205
460
467
  firefighter_tests/test_firefighter/test_urls.py,sha256=UMGx4oW98RoL0ceePkIIKEVjbHdFECvQuGNXYAJForQ,4839
461
468
  firefighter_tests/test_incidents/test_enums.py,sha256=wMxxL1uakrmzJIi-2xkAvG-Y3NDDmIt0PHyOAJBz0yQ,4341
462
- firefighter_tests/test_incidents/test_incident_urls.py,sha256=j663qeBkv31aMlA0sLfzBOmp0iEv9OB-xvIwzrjlSZk,3793
469
+ firefighter_tests/test_incidents/test_incident_urls.py,sha256=VD9dj0IHHKXJHC5ApZg-L9CMtotaQb9uRAKcZIhOrDI,3978
463
470
  firefighter_tests/test_incidents/test_forms/conftest.py,sha256=YYF5Lm-Jmt-HM9zt_gjrNkiuqOaNMW8lLBr1crAP6J8,5423
464
471
  firefighter_tests/test_incidents/test_forms/test_closure_reason.py,sha256=H6RObqazFAit_pvo7N-lotiSsLOYMafZIk23A5Wiodg,3533
465
472
  firefighter_tests/test_incidents/test_forms/test_form_select_impact.py,sha256=DTaPGrJi8mXHfh7mhvDTKYVvDCxqarILauE59UDlwqo,3210
@@ -471,10 +478,10 @@ firefighter_tests/test_incidents/test_forms/test_update_key_events.py,sha256=rHR
471
478
  firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py,sha256=q0xXU2BbBG8B0uvvyBWlo4HM8ckbcNAP05Fq8oJNtOw,16270
472
479
  firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py,sha256=priKh7QYZxGDPu2SvPC8pGnqOsZWg5cLkyC40pDvLAU,7184
473
480
  firefighter_tests/test_incidents/test_models/test_incident_category.py,sha256=aRoBOhb8fNjLF9CMPZ1FXM8AT51Cd80XPsY2Y3wHY_M,5701
474
- firefighter_tests/test_incidents/test_models/test_incident_model.py,sha256=P5tmU5X4grt-yvoqjfuBkbd5tAWX3TptN-b_R3-1a9A,4719
481
+ firefighter_tests/test_incidents/test_models/test_incident_model.py,sha256=AWyWfQYcHNP9GPizIo0wRxNGTJTEJnAwNSd4UmRq-dk,8626
475
482
  firefighter_tests/test_incidents/test_models/test_migrations/test_incident_migrations.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
476
483
  firefighter_tests/test_incidents/test_utils/test_date_utils.py,sha256=ogP7qOEwItL4YGI5gbQPVssOS9ilwiuZC8OrT2qngBY,6568
477
- firefighter_tests/test_incidents/test_views/test_incident_detail_view.py,sha256=gKKFWIZVrD_P4p6DJjeHCW5uGXBUBVlCd95gJJYDpWQ,680
484
+ firefighter_tests/test_incidents/test_views/test_incident_detail_view.py,sha256=lkCIRfz99Ea0o0Id08LFWrjXLDmHv6XvezaSsjg-eYQ,871
478
485
  firefighter_tests/test_incidents/test_views/test_index_view.py,sha256=InpxbaWOFwRn4YWeIKZhj17vMymrQQf2p2LFhe2Bcdw,816
479
486
  firefighter_tests/test_jira_app/__init__.py,sha256=JxZ3v-0kiHOoO-N3kR8NHTmD8tEvuEYKW1GX_S1ZLMY,33
480
487
  firefighter_tests/test_jira_app/conftest.py,sha256=HmZd7EBZgng-rb3kIaB14TPVMixMG4YEvnShVqgjodE,545
@@ -502,13 +509,13 @@ firefighter_tests/test_slack/test_conversation_tags.py,sha256=nNqTZRRBfF6Z4wpFSY
502
509
  firefighter_tests/test_slack/test_signals_downgrade.py,sha256=mgl4H5vwr2kImf6g4IZbhv7YEPmMzbYSaVr8E6taL88,5420
503
510
  firefighter_tests/test_slack/test_slack_utils.py,sha256=9PLobMNXh3xDyFuwzcQFpKJhe4j__sIgf_WRHIpANJw,3957
504
511
  firefighter_tests/test_slack/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
505
- firefighter_tests/test_slack/messages/test_slack_messages.py,sha256=rs7zeBa_xsAyZIfXbOI1RA-zgViEPT5ZFNw6paBJvxc,17299
512
+ firefighter_tests/test_slack/messages/test_slack_messages.py,sha256=uyxfeAy1BQxx1zcCzlSJWn5YF1EnH-5Kt2XoIn9dekM,17484
506
513
  firefighter_tests/test_slack/test_models/test_conversations.py,sha256=t3ttmgwiu7c-N55iU3XZPmrkEhvkTzJoXszJncy4Bts,793
507
514
  firefighter_tests/test_slack/test_models/test_incident_channel.py,sha256=qWoGe9iadmK6-R8usWvjH87AHRkvhG_dHQeC3kHeJrs,17487
508
515
  firefighter_tests/test_slack/test_models/test_slack_user.py,sha256=uzur-Rf03I5dpUTO4ZI6O1arBUrAorg1Zvgshf8M-J4,7000
509
516
  firefighter_tests/test_slack/views/modals/conftest.py,sha256=TKJVQgqWaFs3Gg1T526pti9XpZBtQs47WBH6L_qSDeo,4532
510
- firefighter_tests/test_slack/views/modals/test_close.py,sha256=wuqMkpyUWKvC_gaTnJvRmdm8-wsmbLpCU_ith4ihsLY,45447
511
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py,sha256=hKfBv8fHfObmZc7OL6NUnxFueA1bowkNbu_-MF1anYI,5615
517
+ firefighter_tests/test_slack/views/modals/test_close.py,sha256=FWNV7RIUpqp3tiz9IBbBxaZk1XQt2f7vWB5TzJKYK3o,45630
518
+ firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py,sha256=mvg5RiCXQEp1GhyOBCNW4idkNR1StgZjPvFrjzJ549Q,8333
512
519
  firefighter_tests/test_slack/views/modals/test_edit.py,sha256=ykirry-S3i6PtoSs3rff_k6jqmvv1oMWC_iR8e5Jsg0,12022
513
520
  firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py,sha256=Svab_ZyYTMf0T-uJEQcm7gS1WzxtC4gPh1W--Z2v_Y8,8415
514
521
  firefighter_tests/test_slack/views/modals/test_key_event_message.py,sha256=BCg-c27ZLJqNgFuG4JDgXrSTp8_sT4FeBtpASzSq8NI,1107
@@ -516,10 +523,10 @@ firefighter_tests/test_slack/views/modals/test_open.py,sha256=IzgG9le5NN_CvltehA
516
523
  firefighter_tests/test_slack/views/modals/test_opening_unified.py,sha256=OejtLyc_mehav2TDaLzUnhilMNvhCzc6T4FodCqfQPk,17406
517
524
  firefighter_tests/test_slack/views/modals/test_send_sos.py,sha256=_rE6jD-gOzcGyhlY0R9GzlGtPx65oOOguJYdENgxtLc,1289
518
525
  firefighter_tests/test_slack/views/modals/test_status.py,sha256=oQzPfwdg2tkbo9nfkO1GfS3WydxqSC6vy1AZjZDKT30,2226
519
- firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=3ARHZPs22FTx7IjgOldzEpVxxWeHqEbe4kQphUuSp34,55928
526
+ firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=vbHGx6dkM_0swE1vJ0HrkhI1oJzD_WHZuIQ-_arAxXo,55686
520
527
  firefighter_tests/test_slack/views/modals/test_utils.py,sha256=DJd2n9q6fFu8UuCRdiq9U_Cn19MdnC5c-ydLLrk6rkc,5218
521
- firefighter_incident-0.0.26.dist-info/METADATA,sha256=6maVl16hOTcZXnEYTy1PhaP4ligQTcK5QVZesD6C5a0,5570
522
- firefighter_incident-0.0.26.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
523
- firefighter_incident-0.0.26.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
524
- firefighter_incident-0.0.26.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
525
- firefighter_incident-0.0.26.dist-info/RECORD,,
528
+ firefighter_incident-0.0.27.dist-info/METADATA,sha256=lHCUnR9PYiRLl_zG_MfvG23UsE_FZWbgQ4zlRdeIilY,5570
529
+ firefighter_incident-0.0.27.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
530
+ firefighter_incident-0.0.27.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
531
+ firefighter_incident-0.0.27.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
532
+ firefighter_incident-0.0.27.dist-info/RECORD,,
@@ -4,6 +4,7 @@ import logging
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  import pytest
7
+ from django.apps import apps
7
8
  from django.urls import reverse
8
9
 
9
10
  from firefighter.incidents.factories import IncidentFactory, UserFactory
@@ -15,6 +16,9 @@ if TYPE_CHECKING:
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
19
+ if not apps.is_installed("firefighter.confluence"):
20
+ pytest.skip("Confluence app not installed; skipping incident URLs tests", allow_module_level=True)
21
+
18
22
 
19
23
  @pytest.mark.django_db
20
24
  def test_incidents_dashboard_unauthorized(client: Client) -> None:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
+ from unittest.mock import PropertyMock, patch
4
5
 
5
6
  import pytest
6
7
  from hypothesis import given
@@ -10,8 +11,11 @@ from hypothesis.strategies import builds
10
11
  from firefighter.incidents.enums import ClosureReason, IncidentStatus
11
12
  from firefighter.incidents.factories import IncidentFactory
12
13
  from firefighter.incidents.models import IncidentUpdate
14
+ from firefighter.jira_app.models import JiraPostMortem
13
15
 
14
16
  if TYPE_CHECKING:
17
+ from pytest_mock import MockerFixture
18
+
15
19
  from firefighter.incidents.models import Incident
16
20
 
17
21
 
@@ -58,7 +62,9 @@ class TestIncidentCanBeClosed:
58
62
  can_close, reasons = incident.can_be_closed
59
63
 
60
64
  # Should be closable (assuming no missing milestones)
61
- assert can_close is True or "STATUS_NOT_MITIGATED" not in [r[0] for r in reasons]
65
+ assert can_close is True or "STATUS_NOT_MITIGATED" not in [
66
+ r[0] for r in reasons
67
+ ]
62
68
 
63
69
  def test_can_close_incident_with_closure_reason(self) -> None:
64
70
  """Test that incidents with closure_reason can always be closed."""
@@ -73,6 +79,108 @@ class TestIncidentCanBeClosed:
73
79
  assert can_close is True
74
80
  assert reasons == []
75
81
 
82
+ def test_cannot_close_when_jira_postmortem_not_ready(self, settings: None) -> None:
83
+ """Block closure if Jira post-mortem exists but is not in Ready status."""
84
+ settings.ENABLE_JIRA_POSTMORTEM = True
85
+ incident = IncidentFactory.create(
86
+ _status=IncidentStatus.POST_MORTEM,
87
+ priority__value=1,
88
+ priority__needs_postmortem=True,
89
+ environment__value="PRD",
90
+ )
91
+ JiraPostMortem.objects.create(
92
+ incident=incident,
93
+ jira_issue_key="INC-999",
94
+ jira_issue_id="999",
95
+ created_by=incident.created_by,
96
+ )
97
+ incident.refresh_from_db()
98
+ assert hasattr(incident, "jira_postmortem_for")
99
+
100
+ with (
101
+ patch.object(
102
+ type(incident),
103
+ "needs_postmortem",
104
+ new_callable=PropertyMock,
105
+ return_value=True,
106
+ ),
107
+ patch.object(type(incident), "missing_milestones", return_value=[]),
108
+ patch(
109
+ "firefighter.jira_app.service_postmortem.jira_postmortem_service.is_postmortem_ready",
110
+ return_value=(False, "In Progress"),
111
+ ),
112
+ ):
113
+ can_close, reasons = incident.can_be_closed
114
+
115
+ assert can_close is False
116
+ assert any(r[0] == "POSTMORTEM_NOT_READY" for r in reasons)
117
+
118
+ def test_postmortem_ready_allows_closure(
119
+ self, mocker: MockerFixture, settings: None
120
+ ) -> None:
121
+ """When Jira PM is Ready, can_be_closed should allow closure for PM incidents."""
122
+ settings.ENABLE_JIRA_POSTMORTEM = True
123
+ incident = IncidentFactory.create(
124
+ _status=IncidentStatus.POST_MORTEM,
125
+ priority__value=1,
126
+ priority__needs_postmortem=True,
127
+ environment__value="PRD",
128
+ )
129
+ JiraPostMortem.objects.create(
130
+ incident=incident,
131
+ jira_issue_key="INC-READY",
132
+ jira_issue_id="123",
133
+ created_by=incident.created_by,
134
+ )
135
+
136
+ mocker.patch.object(type(incident), "missing_milestones", return_value=[])
137
+ mocker.patch(
138
+ "firefighter.jira_app.service_postmortem.jira_postmortem_service.is_postmortem_ready",
139
+ return_value=(True, "Ready"),
140
+ )
141
+
142
+ can_close, reasons = incident.can_be_closed
143
+
144
+ assert can_close is True
145
+ assert reasons == []
146
+
147
+ def test_postmortem_status_unknown_sets_reason(
148
+ self, mocker: MockerFixture, settings: None
149
+ ) -> None:
150
+ """Errors while checking Jira PM should return POSTMORTEM_STATUS_UNKNOWN."""
151
+ settings.ENABLE_JIRA_POSTMORTEM = True
152
+ incident = IncidentFactory.create(
153
+ _status=IncidentStatus.POST_MORTEM,
154
+ priority__value=1,
155
+ priority__needs_postmortem=True,
156
+ environment__value="PRD",
157
+ )
158
+ JiraPostMortem.objects.create(
159
+ incident=incident,
160
+ jira_issue_key="INC-ERR",
161
+ jira_issue_id="124",
162
+ created_by=incident.created_by,
163
+ )
164
+ incident.refresh_from_db()
165
+ assert hasattr(incident, "jira_postmortem_for")
166
+
167
+ mocker.patch.object(type(incident), "missing_milestones", return_value=[])
168
+ mocker.patch(
169
+ "firefighter.jira_app.service_postmortem.jira_postmortem_service.is_postmortem_ready",
170
+ side_effect=Exception("boom"),
171
+ )
172
+ mocker.patch.object(
173
+ type(incident),
174
+ "needs_postmortem",
175
+ new_callable=PropertyMock,
176
+ return_value=True,
177
+ )
178
+
179
+ can_close, reasons = incident.can_be_closed
180
+
181
+ assert can_close is False
182
+ assert any(r[0] == "POSTMORTEM_STATUS_UNKNOWN" for r in reasons)
183
+
76
184
 
77
185
  @pytest.mark.django_db
78
186
  class TestIncidentSetStatus:
@@ -1,12 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import pytest
4
+ from django.apps import apps
4
5
  from django.test import Client
5
6
  from django.urls import reverse
6
7
 
7
8
  from firefighter.incidents.models import Incident
8
9
  from firefighter.incidents.models.user import User
9
10
 
11
+ if not apps.is_installed("firefighter.confluence"):
12
+ pytest.skip("Confluence app not installed; skipping incident detail view test", allow_module_level=True)
13
+
10
14
 
11
15
  @pytest.mark.django_db
12
16
  def test_incident_detail_view(
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import pytest
4
+ from django.apps import apps
4
5
 
5
6
  from firefighter.incidents.enums import IncidentStatus
6
7
  from firefighter.incidents.factories import IncidentFactory, UserFactory
@@ -18,6 +19,9 @@ try:
18
19
  except (ImportError, AttributeError):
19
20
  PostMortem = None
20
21
 
22
+ if not apps.is_installed("firefighter.confluence"):
23
+ pytest.skip("Confluence app not installed; skipping slack message tests", allow_module_level=True)
24
+
21
25
 
22
26
  @pytest.mark.django_db
23
27
  class TestSlackMessageIncidentStatusUpdated:
@@ -5,6 +5,7 @@ from copy import deepcopy
5
5
  from unittest.mock import MagicMock, PropertyMock
6
6
 
7
7
  import pytest
8
+ from django.apps import apps
8
9
  from pytest_mock import MockerFixture
9
10
 
10
11
  from firefighter.incidents.enums import IncidentStatus
@@ -14,6 +15,9 @@ from firefighter.slack.views import CloseModal
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
18
+ if not apps.is_installed("firefighter.confluence"):
19
+ pytest.skip("Confluence app not installed; skipping close modal tests", allow_module_level=True)
20
+
17
21
 
18
22
  @pytest.mark.django_db
19
23
  class TestCloseModal:
@@ -9,6 +9,7 @@ from slack_sdk.errors import SlackApiError
9
9
 
10
10
  from firefighter.incidents.enums import ClosureReason, IncidentStatus
11
11
  from firefighter.incidents.factories import IncidentFactory, UserFactory
12
+ from firefighter.incidents.models import Environment, Priority
12
13
  from firefighter.slack.views.modals.closure_reason import ClosureReasonModal
13
14
 
14
15
 
@@ -16,12 +17,16 @@ from firefighter.slack.views.modals.closure_reason import ClosureReasonModal
16
17
  class TestClosureReasonModalMessageTabDisabled:
17
18
  """Test ClosureReasonModal handles messages_tab_disabled gracefully."""
18
19
 
19
- def test_closure_reason_handles_messages_tab_disabled(self, caplog: pytest.LogCaptureFixture, mocker) -> None:
20
+ def test_closure_reason_handles_messages_tab_disabled(
21
+ self, caplog: pytest.LogCaptureFixture, mocker
22
+ ) -> None:
20
23
  """Test that messages_tab_disabled error is handled gracefully with warning log."""
21
24
  # Create test data
22
25
  user = UserFactory.build()
23
26
  user.save()
24
- incident = IncidentFactory.build(_status=IncidentStatus.INVESTIGATING, created_by=user)
27
+ incident = IncidentFactory.build(
28
+ _status=IncidentStatus.INVESTIGATING, created_by=user
29
+ )
25
30
  incident.save()
26
31
 
27
32
  # Mock can_be_closed to return True so the closure can proceed
@@ -29,7 +34,7 @@ class TestClosureReasonModalMessageTabDisabled:
29
34
  type(incident),
30
35
  "can_be_closed",
31
36
  new_callable=mocker.PropertyMock,
32
- return_value=(True, [])
37
+ return_value=(True, []),
33
38
  )
34
39
 
35
40
  # Create modal and mock
@@ -46,9 +51,7 @@ class TestClosureReasonModalMessageTabDisabled:
46
51
  "selected_option": {"value": ClosureReason.CANCELLED}
47
52
  }
48
53
  },
49
- "closure_reference": {
50
- "input_closure_reference": {"value": ""}
51
- },
54
+ "closure_reference": {"input_closure_reference": {"value": ""}},
52
55
  "closure_message": {
53
56
  "input_closure_message": {"value": "Test closure message"}
54
57
  },
@@ -63,18 +66,17 @@ class TestClosureReasonModalMessageTabDisabled:
63
66
  slack_error_response = MagicMock()
64
67
  slack_error_response.get.return_value = "messages_tab_disabled"
65
68
 
66
- with patch("firefighter.slack.views.modals.closure_reason.respond") as mock_respond:
69
+ with patch(
70
+ "firefighter.slack.views.modals.closure_reason.respond"
71
+ ) as mock_respond:
67
72
  mock_respond.side_effect = SlackApiError(
68
73
  message="The request to the Slack API failed.",
69
- response=slack_error_response
74
+ response=slack_error_response,
70
75
  )
71
76
 
72
77
  # Execute
73
78
  result = modal.handle_modal_fn(
74
- ack=ack,
75
- body=body,
76
- incident=incident,
77
- user=user
79
+ ack=ack, body=body, incident=incident, user=user
78
80
  )
79
81
 
80
82
  # Assertions
@@ -87,7 +89,8 @@ class TestClosureReasonModalMessageTabDisabled:
87
89
 
88
90
  # Verify warning was logged
89
91
  assert any(
90
- "Cannot send DM to user" in record.message and record.levelname == "WARNING"
92
+ "Cannot send DM to user" in record.message
93
+ and record.levelname == "WARNING"
91
94
  for record in caplog.records
92
95
  )
93
96
 
@@ -96,7 +99,9 @@ class TestClosureReasonModalMessageTabDisabled:
96
99
  # Create test data
97
100
  user = UserFactory.build()
98
101
  user.save()
99
- incident = IncidentFactory.build(_status=IncidentStatus.INVESTIGATING, created_by=user)
102
+ incident = IncidentFactory.build(
103
+ _status=IncidentStatus.INVESTIGATING, created_by=user
104
+ )
100
105
  incident.save()
101
106
 
102
107
  # Mock can_be_closed to return True so the closure can proceed
@@ -104,7 +109,7 @@ class TestClosureReasonModalMessageTabDisabled:
104
109
  type(incident),
105
110
  "can_be_closed",
106
111
  new_callable=mocker.PropertyMock,
107
- return_value=(True, [])
112
+ return_value=(True, []),
108
113
  )
109
114
 
110
115
  # Create modal and mock
@@ -121,9 +126,7 @@ class TestClosureReasonModalMessageTabDisabled:
121
126
  "selected_option": {"value": ClosureReason.CANCELLED}
122
127
  }
123
128
  },
124
- "closure_reference": {
125
- "input_closure_reference": {"value": ""}
126
- },
129
+ "closure_reference": {"input_closure_reference": {"value": ""}},
127
130
  "closure_message": {
128
131
  "input_closure_message": {"value": "Test closure message"}
129
132
  },
@@ -138,17 +141,97 @@ class TestClosureReasonModalMessageTabDisabled:
138
141
  slack_error_response = MagicMock()
139
142
  slack_error_response.get.return_value = "channel_not_found"
140
143
 
141
- with patch("firefighter.slack.views.modals.closure_reason.respond") as mock_respond:
144
+ with patch(
145
+ "firefighter.slack.views.modals.closure_reason.respond"
146
+ ) as mock_respond:
142
147
  mock_respond.side_effect = SlackApiError(
143
148
  message="The request to the Slack API failed.",
144
- response=slack_error_response
149
+ response=slack_error_response,
145
150
  )
146
151
 
147
152
  # Execute and expect exception
148
153
  with pytest.raises(SlackApiError):
149
- modal.handle_modal_fn(
150
- ack=ack,
151
- body=body,
152
- incident=incident,
153
- user=user
154
- )
154
+ modal.handle_modal_fn(ack=ack, body=body, incident=incident, user=user)
155
+
156
+
157
+ @pytest.mark.django_db
158
+ class TestClosureReasonModalEarlyClosureBypass:
159
+ """Test early-closure path respects submitted closure reason."""
160
+
161
+ def test_allows_early_closure_with_submitted_reason(self, settings) -> None:
162
+ """Ensure can_be_closed passes when a closure reason is provided for early closure."""
163
+ settings.ENABLE_JIRA_POSTMORTEM = True
164
+
165
+ # Ensure required priority/environment for needs_postmortem + PRD
166
+ priority = Priority.objects.create(
167
+ name="P1-test",
168
+ value=9991,
169
+ description="P1 test",
170
+ order=9991,
171
+ needs_postmortem=True,
172
+ )
173
+ env, _ = Environment.objects.get_or_create(
174
+ value="PRD",
175
+ defaults={
176
+ "name": "Production",
177
+ "description": "Production",
178
+ "order": 9991,
179
+ },
180
+ )
181
+
182
+ user = UserFactory.create()
183
+ incident = IncidentFactory.create(
184
+ _status=IncidentStatus.INVESTIGATING,
185
+ created_by=user,
186
+ priority=priority,
187
+ environment=env,
188
+ )
189
+
190
+ modal = ClosureReasonModal()
191
+ ack = MagicMock()
192
+
193
+ body = {
194
+ "view": {
195
+ "state": {
196
+ "values": {
197
+ "closure_reason": {
198
+ "select_closure_reason": {
199
+ "selected_option": {"value": ClosureReason.CANCELLED}
200
+ }
201
+ },
202
+ "closure_reference": {
203
+ "input_closure_reference": {"value": "INC-42"}
204
+ },
205
+ "closure_message": {
206
+ "input_closure_message": {
207
+ "value": "Closing early with reason"
208
+ }
209
+ },
210
+ }
211
+ },
212
+ "private_metadata": str(incident.id),
213
+ },
214
+ "user": {"id": "U123456"},
215
+ }
216
+
217
+ with patch(
218
+ "firefighter.slack.views.modals.closure_reason.respond"
219
+ ) as mock_respond:
220
+ mock_respond.return_value = None
221
+
222
+ result = modal.handle_modal_fn(
223
+ ack=ack,
224
+ body=body,
225
+ incident=incident,
226
+ user=user,
227
+ )
228
+
229
+ # Early closure should succeed and close the incident
230
+ assert result is True
231
+ incident.refresh_from_db()
232
+ assert incident.status == IncidentStatus.CLOSED
233
+ assert incident.closure_reason == ClosureReason.CANCELLED
234
+ assert incident.closure_reference == "INC-42"
235
+
236
+ # Ack should clear modal stack
237
+ ack.assert_called_once_with(response_action="clear")