afterhours-shift-manager/tests/slack_bot/test_handle_drop.py
Adam Moussa 7733af6af0
Lock shift drops within 24h of start (#84) (#88)
A user can no longer `/oncall drop` a shift inside the 24h window before it
starts — inside that window coverage must be handed off via a verified swap
(target accepts) or opened by an admin.

- app.py: _within_drop_lock(date, shift_type) (24h before _shift_start);
  guard in _handle_drop after the ownership check. Admin `open` is a separate
  handler and is unaffected (bypasses the lock).
- Removed the now-unreachable 3CX-repoint-on-drop branch: a same-day shift is
  always inside the lock, so a drop never reaches mark_open for today.
- Help text + README note the 24h rule.
- tests: rewritten test_handle_drop (outside/inside-24h per shift type,
  weekend day, admin bypass, plus the existing guard-precedence cases) and
  direct _shift_start/_shift_started/_within_drop_lock helper tests. 185 passed.

Closes #84
2026-06-01 19:32:40 -04:00

142 lines
5.2 KiB
Python

"""Tests for slack-bot _handle_drop, including the 24h drop lock (#84)."""
from freezegun import freeze_time
# Monday 2026-06-01 08:00 ET.
MON_0800 = "2026-06-01 12:00:00"
# Monday 2026-06-01 18:00 ET — past 17:00 (so tomorrow's night shift is <24h away).
MON_1800 = "2026-06-01 22:00:00"
# Friday 2026-06-05.
FRI_0600 = "2026-06-05 10:00:00" # 06:00 ET
FRI_0900 = "2026-06-05 13:00:00" # 09:00 ET
@freeze_time(MON_0800)
def test_drop_outside_24h_marks_open(
slackbot_app, schedule, seed, respond, client, routing_spy, text_of
):
# Tomorrow's night shift starts 06-02 17:00; now is 06-01 08:00 → ~33h away.
seed.roster("114", "Alice", slack_user_id="U_ALICE")
seed.weekly("Tuesday", "114", "Alice")
slackbot_app._handle_drop(
respond, schedule, "U_ALICE", "drop tomorrow", "C1", client, None
)
assert schedule.get_override("2026-06-02")["extension"] == "OPEN"
assert "dropped" in text_of(respond)
client.chat_postMessage.assert_called_once()
routing_spy.assert_not_called()
@freeze_time(MON_0800)
def test_drop_today_blocked(
slackbot_app, schedule, seed, respond, client, routing_spy, text_of
):
# A same-day shift is always inside the 24h lock.
seed.roster("114", "Alice", slack_user_id="U_ALICE")
seed.weekly("Monday", "114", "Alice")
slackbot_app._handle_drop(
respond, schedule, "U_ALICE", "drop today", "C1", client, None
)
assert "under 24 hours" in text_of(respond)
assert schedule.get_override("2026-06-01") is None # not opened
routing_spy.assert_not_called()
client.chat_postMessage.assert_not_called()
@freeze_time(MON_1800)
def test_drop_blocked_once_within_24h_of_start(
slackbot_app, schedule, seed, respond, client, text_of
):
# At 06-01 18:00, tomorrow's 06-02 17:00 shift is <24h away.
seed.roster("114", "Alice", slack_user_id="U_ALICE")
seed.weekly("Tuesday", "114", "Alice")
slackbot_app._handle_drop(
respond, schedule, "U_ALICE", "drop tomorrow", "C1", client, None
)
assert "under 24 hours" in text_of(respond)
assert schedule.get_override("2026-06-02") is None
@freeze_time(FRI_0600)
def test_drop_weekend_day_shift_outside_24h(
slackbot_app, schedule, seed, respond, client, text_of
):
# Saturday day shift starts 06-06 08:00; now Fri 06:00 → >24h.
seed.roster("200", "Alice", slack_user_id="U_ALICE")
seed.weekly("Saturday", "200", "Alice", shift_type="day")
slackbot_app._handle_drop(
respond, schedule, "U_ALICE", "drop saturday", "C1", client, None
)
assert "dropped" in text_of(respond)
assert schedule.get_override("2026-06-06", "day")["extension"] == "OPEN"
@freeze_time(FRI_0900)
def test_drop_weekend_day_shift_within_24h(
slackbot_app, schedule, seed, respond, client, text_of
):
# Now Fri 09:00 → Saturday 08:00 day shift is <24h away.
seed.roster("200", "Alice", slack_user_id="U_ALICE")
seed.weekly("Saturday", "200", "Alice", shift_type="day")
slackbot_app._handle_drop(
respond, schedule, "U_ALICE", "drop saturday", "C1", client, None
)
assert "under 24 hours" in text_of(respond)
assert schedule.get_override("2026-06-06", "day") is None
@freeze_time(MON_0800)
def test_admin_open_bypasses_24h_lock(
slackbot_app, schedule, seed, respond, client, routing_spy, text_of
):
# Admins can still open a same-day shift — the lock only applies to user drop.
seed.weekly("Monday", "114", "Alice")
slackbot_app._handle_admin(
respond, schedule, "U_ADMIN", "admin open today", True, client, None
)
assert schedule.get_override("2026-06-01")["extension"] == "OPEN"
assert "marked as open" in text_of(respond).lower()
@freeze_time(MON_0800)
def test_drop_not_your_shift(slackbot_app, schedule, seed, respond, client, text_of):
seed.roster("114", "Alice", slack_user_id="U_ALICE")
seed.weekly("Monday", "115", "Bob") # Bob is on shift, not Alice
slackbot_app._handle_drop(
respond, schedule, "U_ALICE", "drop today", "C1", client, None
)
assert "not your shift" in text_of(respond).lower()
@freeze_time(MON_0800)
def test_drop_past_date(slackbot_app, schedule, seed, respond, client, text_of):
seed.roster("114", "Alice", slack_user_id="U_ALICE")
seed.weekly("Monday", "114", "Alice")
slackbot_app._handle_drop(
respond, schedule, "U_ALICE", "drop 2026-05-01", "C1", client, None
)
assert "past" in text_of(respond).lower()
@freeze_time(MON_0800)
def test_drop_unregistered(slackbot_app, schedule, respond, client, text_of):
slackbot_app._handle_drop(
respond, schedule, "U_NOBODY", "drop today", "C1", client, None
)
assert "not registered" in text_of(respond).lower()
@freeze_time(MON_0800)
def test_drop_bad_date(slackbot_app, schedule, seed, respond, client, text_of):
seed.roster("114", "Alice", slack_user_id="U_ALICE")
seed.weekly("Monday", "114", "Alice")
slackbot_app._handle_drop(
respond, schedule, "U_ALICE", "drop notaday", "C1", client, None
)
assert "couldn't parse" in text_of(respond).lower()
@freeze_time(MON_0800)
def test_drop_usage(slackbot_app, schedule, respond, client, text_of):
slackbot_app._handle_drop(respond, schedule, "U_ALICE", "drop", "C1", client, None)
assert "Usage" in text_of(respond)