"""Tests for the late-pickup approval flow (Phase 8) — picking up a shift that has already started requires admin approval, for BOTH regular and holiday shifts. """ import pytest from freezegun import freeze_time # Monday 2026-06-01. Night shift starts 17:00, ends next day 08:00. MON_BEFORE = "2026-06-01 12:00:00" # 08:00 ET — before the night shift starts MON_AFTER = "2026-06-01 22:00:00" # 18:00 ET — night shift already started MON_NEXT_DAY = "2026-06-02 13:00:00" # 09:00 ET — Monday night shift has ended # Saturday 2026-07-04 holiday day shift: 08:00–17:00 ET. SAT_BEFORE = "2026-07-04 11:00:00" # 07:00 ET — before the day shift starts SAT_DURING = "2026-07-04 16:00:00" # 12:00 ET — day shift in progress SAT_AFTER = "2026-07-04 22:00:00" # 18:00 ET — day shift has ended def _admins(schedule_seed): schedule_seed.config(admin_users=["U_ADMIN1", "U_ADMIN2"]) def _dm_channels(client): return [c.kwargs.get("channel") for c in client.chat_postMessage.call_args_list] # ── _handle_pick: regular shift, after start → approval ────────────────── class TestRegularLatePick: @freeze_time(MON_AFTER) def test_creates_request_and_dms_admins( self, slackbot_app, schedule, seed, respond, client, routing_spy, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") _admins(seed) slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick today", "C1", client, "C_TEST" ) req = schedule.get_pickup_request("2026-06-01", "night", "114") assert req is not None and req["status"] == "pending" assert req["is_holiday"] is False # Not claimed yet — no override, no 3CX. assert schedule.get_override("2026-06-01") is None routing_spy.assert_not_called() # DM'd both admins. assert set(_dm_channels(client)) == {"U_ADMIN1", "U_ADMIN2"} assert "admin approval" in text_of(respond).lower() @freeze_time(MON_NEXT_DAY) def test_after_end_rejected( self, slackbot_app, schedule, seed, respond, client, text_of ): # A night shift ends the next morning at 08:00, by which point the date # is in the past — the past-date guard rejects it (no approval request). seed.roster("114", "Alice", slack_user_id="U_ALICE") _admins(seed) slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick 2026-06-01", "C1", client, "C_TEST" ) assert "past" in text_of(respond).lower() assert schedule.get_pickup_request("2026-06-01", "night", "114") is None @freeze_time(MON_BEFORE) def test_before_start_immediate( self, slackbot_app, schedule, seed, respond, client, routing_spy, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") _admins(seed) slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick today", "C1", client, "C_TEST" ) # Immediate claim, no approval request. assert schedule.get_override("2026-06-01")["extension"] == "114" assert schedule.get_pickup_request("2026-06-01", "night", "114") is None assert "picked up" in text_of(respond).lower() @freeze_time(MON_AFTER) def test_no_admins_rolls_back( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") # No admin_users configured. slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick today", "C1", client, "C_TEST" ) assert schedule.get_pickup_request("2026-06-01", "night", "114") is None assert "couldn't reach any admin" in text_of(respond).lower() # ── _handle_pick: holiday shift, after start → approval ────────────────── class TestHolidayLatePick: @freeze_time(SAT_DURING) def test_creates_holiday_request( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") _admins(seed) seed.holiday("2026-07-04", slots=2, label="Independence Day") slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick 2026-07-04", "C1", client, "C_TEST" ) req = schedule.get_pickup_request("2026-07-04", "day", "114") assert req is not None and req["is_holiday"] is True # Slot not claimed yet. assert schedule.get_holiday("2026-07-04")["assignees"] == {} @freeze_time(SAT_BEFORE) def test_before_window_immediate_claim( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") _admins(seed) seed.holiday("2026-07-04", slots=2, label="Independence Day") slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick 2026-07-04", "C1", client, "C_TEST" ) assert "114" in schedule.get_holiday("2026-07-04")["assignees"] assert "picked up a slot" in text_of(respond).lower() @freeze_time(SAT_AFTER) def test_after_window_rejected( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") _admins(seed) seed.holiday("2026-07-04", slots=2, label="Independence Day") slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick 2026-07-04", "C1", client, "C_TEST" ) assert "already ended" in text_of(respond).lower() assert schedule.get_pickup_request("2026-07-04", "day", "114") is None # ── handle_pickup button: late pickup gating ───────────────────────────── class TestButtonLatePick: @freeze_time(MON_AFTER) def test_button_after_start_requests_approval( self, slackbot_app, schedule, seed, respond, client ): seed.roster("114", "Alice", slack_user_id="U_ALICE") _admins(seed) body = { "actions": [{"action_id": "pickup_2026-06-01"}], "user": {"id": "U_ALICE"}, "channel": {"id": "C1"}, } slackbot_app.handle_pickup(body, respond, client, schedule, "C_TEST") assert schedule.get_pickup_request("2026-06-01", "night", "114") is not None # The clicker is told via an ephemeral message. assert client.chat_postEphemeral.called @freeze_time(SAT_DURING) def test_button_holiday_late_request( self, slackbot_app, schedule, seed, respond, client ): seed.roster("114", "Alice", slack_user_id="U_ALICE") _admins(seed) seed.holiday("2026-07-04", slots=2, label="Independence Day") body = { "actions": [{"action_id": "pickup_2026-07-04_day"}], "user": {"id": "U_ALICE"}, "channel": {"id": "C1"}, } slackbot_app.handle_pickup(body, respond, client, schedule, "C_TEST") req = schedule.get_pickup_request("2026-07-04", "day", "114") assert req is not None and req["is_holiday"] is True # ── approve / deny handlers ────────────────────────────────────────────── def _approve_body(date_str, ext, suffix="", user_id="U_ADMIN1"): return { "actions": [{"action_id": f"pickup_approve_{date_str}{suffix}_{ext}"}], "user": {"id": user_id}, "channel": {"id": "D1"}, } def _deny_body(date_str, ext, suffix="", user_id="U_ADMIN1"): return { "actions": [{"action_id": f"pickup_deny_{date_str}{suffix}_{ext}"}], "user": {"id": user_id}, "channel": {"id": "D1"}, } def _resolved_text(respond): return respond.call_args.kwargs["blocks"][0]["text"]["text"] class TestApproveRegular: @pytest.fixture(autouse=True) def _seed_admins(self, seed): _admins(seed) @freeze_time(MON_AFTER) def test_approve_claims_and_repoints( self, slackbot_app, schedule, seed, respond, client, routing_spy ): seed.roster("114", "Alice", slack_user_id="U_ALICE") emp = {"extension": "114", "name": "Alice", "slack_user_id": "U_ALICE"} schedule.create_pickup_request( "2026-06-01", "night", emp, 1_900_000_000, is_holiday=False ) slackbot_app.handle_pickup_approve( _approve_body("2026-06-01", "114"), respond, client, schedule, "C_TEST" ) assert schedule.get_override("2026-06-01")["extension"] == "114" assert schedule.get_pickup_request("2026-06-01", "night", "114") is None # Today + active night → 3CX repointed. routing_spy.assert_called_once_with("114") assert "U_ALICE" in _dm_channels(client) # requester notified assert "approved" in _resolved_text(respond).lower() @freeze_time(MON_AFTER) def test_non_admin_cannot_approve( self, slackbot_app, schedule, seed, respond, client, routing_spy ): seed.roster("114", "Alice", slack_user_id="U_ALICE") emp = {"extension": "114", "name": "Alice", "slack_user_id": "U_ALICE"} schedule.create_pickup_request( "2026-06-01", "night", emp, 1_900_000_000, is_holiday=False ) # U_ALICE (the requester) is NOT in admin_users — must be rejected with # no assignment, no 3CX repoint, and the request left pending. slackbot_app.handle_pickup_approve( _approve_body("2026-06-01", "114", user_id="U_ALICE"), respond, client, schedule, "C_TEST", ) assert schedule.get_override("2026-06-01") is None assert schedule.get_pickup_request("2026-06-01", "night", "114") is not None routing_spy.assert_not_called() @freeze_time(MON_AFTER) def test_second_approve_no_longer_pending( self, slackbot_app, schedule, seed, respond, client ): emp = {"extension": "114", "name": "Alice", "slack_user_id": "U_ALICE"} schedule.create_pickup_request( "2026-06-01", "night", emp, 1_900_000_000, is_holiday=False ) slackbot_app.handle_pickup_approve( _approve_body("2026-06-01", "114"), respond, client, schedule, "C_TEST" ) # A second admin clicks the (now-resolved) request. slackbot_app.handle_pickup_approve( _approve_body("2026-06-01", "114", user_id="U_ADMIN2"), respond, client, schedule, "C_TEST", ) assert "no longer pending" in _resolved_text(respond).lower() @freeze_time(MON_NEXT_DAY) def test_approve_after_end_expires( self, slackbot_app, schedule, respond, client, routing_spy ): emp = {"extension": "114", "name": "Alice", "slack_user_id": "U_ALICE"} schedule.create_pickup_request( "2026-06-01", "night", emp, 1_900_000_000, is_holiday=False ) slackbot_app.handle_pickup_approve( _approve_body("2026-06-01", "114"), respond, client, schedule, "C_TEST" ) assert "expired" in _resolved_text(respond).lower() assert schedule.get_override("2026-06-01") is None routing_spy.assert_not_called() class TestApproveHoliday: @pytest.fixture(autouse=True) def _seed_admins(self, seed): _admins(seed) @freeze_time(SAT_DURING) def test_approve_claims_slot_and_sets_agents( self, slackbot_app, schedule, seed, respond, client, monkeypatch ): agents = _spy_queue_agents(slackbot_app, monkeypatch) seed.holiday("2026-07-04", slots=2, label="Independence Day") emp = {"extension": "114", "name": "Alice", "slack_user_id": "U_ALICE"} schedule.create_pickup_request( "2026-07-04", "day", emp, 1_900_000_000, is_holiday=True ) slackbot_app.handle_pickup_approve( _approve_body("2026-07-04", "114", suffix="_day"), respond, client, schedule, "C_TEST", ) assert "114" in schedule.get_holiday("2026-07-04")["assignees"] # Window open → queue membership refreshed. agents.assert_called_once() assert agents.call_args.args[1] == "2026-07-04" @freeze_time(SAT_DURING) def test_approve_full_holiday_fails_gracefully( self, slackbot_app, schedule, seed, respond, client, monkeypatch ): _spy_queue_agents(slackbot_app, monkeypatch) seed.holiday( "2026-07-04", slots=1, label="Independence Day", assignees={"115": {"name": "Bob", "claimed_at": "x"}}, ) emp = {"extension": "114", "name": "Alice", "slack_user_id": "U_ALICE"} schedule.create_pickup_request( "2026-07-04", "day", emp, 1_900_000_000, is_holiday=True ) slackbot_app.handle_pickup_approve( _approve_body("2026-07-04", "114", suffix="_day"), respond, client, schedule, "C_TEST", ) assert "full or already taken" in _resolved_text(respond).lower() assert "114" not in schedule.get_holiday("2026-07-04")["assignees"] class TestDeny: @pytest.fixture(autouse=True) def _seed_admins(self, seed): _admins(seed) @freeze_time(MON_AFTER) def test_deny_clears_and_notifies(self, slackbot_app, schedule, respond, client): emp = {"extension": "114", "name": "Alice", "slack_user_id": "U_ALICE"} schedule.create_pickup_request( "2026-06-01", "night", emp, 1_900_000_000, is_holiday=False ) slackbot_app.handle_pickup_deny( _deny_body("2026-06-01", "114"), respond, client, schedule, "C_TEST" ) assert schedule.get_pickup_request("2026-06-01", "night", "114") is None assert schedule.get_override("2026-06-01") is None # never claimed assert "denied" in _resolved_text(respond).lower() assert "U_ALICE" in _dm_channels(client) @freeze_time(MON_AFTER) def test_deny_no_pending(self, slackbot_app, schedule, respond, client): slackbot_app.handle_pickup_deny( _deny_body("2026-06-01", "114"), respond, client, schedule, "C_TEST" ) assert "no longer pending" in _resolved_text(respond).lower() def test_parse_pickup_action(slackbot_app): assert slackbot_app._parse_pickup_action( "pickup_approve_2026-07-04_day_114", "pickup_approve_" ) == ("2026-07-04", "day", "114") assert slackbot_app._parse_pickup_action( "pickup_deny_2026-06-01_115", "pickup_deny_" ) == ("2026-06-01", "night", "115") def _spy_queue_agents(slackbot_app, monkeypatch): from unittest.mock import MagicMock spy = MagicMock(name="_set_holiday_queue_agents") monkeypatch.setattr(slackbot_app, "_set_holiday_queue_agents", spy) return spy