afterhours-shift-manager/tests/slack_bot/test_handle_holiday.py
Adam Moussa 88e782c205
Some checks are pending
Deploy / deploy (push) Waiting to run
Deploy / release (push) Blocked by required conditions
Add holiday shifts with 3CX routing and late-pickup approval (#121)
Holiday day-shifts (08:00-17:00 ET) with N slots and 1.5x pay. A new
afterhours-holiday-router Lambda, fired by per-holiday EventBridge Scheduler
one-offs, repoints IVR 800 (key-0 + no-input/timeout) to holiday queue 802 and
sets 802's membership to the day's assignees (ext 100 fallback when unfilled),
reverting at 17:00. Pickups after a shift starts go through an admin Approve/Deny
flow for both regular and holiday shifts. Pay (weekly post + /oncall pay) shows
holiday rates distinctly.

Adds HOLIDAY and PICKUP_REQUEST DynamoDB record types, scheduler IAM scoped to
holiday-* schedules with conditioned PassRole, and the holiday-router function
with a 60-day log group and error alarm.
2026-06-17 11:14:29 -04:00

311 lines
9.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for slack-bot admin holiday subcommands (add / remove / list) and the
one-off EventBridge Scheduler lifecycle (Phase 7)."""
import boto3
import pytest
from freezegun import freeze_time
# Wednesday 2026-07-01 12:00 ET — well before the 2026-07-04 holiday.
WED = "2026-07-01 16:00:00"
# Saturday 2026-07-04 12:00 ET — inside the holiday window (08:00–17:00).
SAT_MIDDAY = "2026-07-04 16:00:00"
ROLE_ARN = "arn:aws:iam::328440206208:role/holiday-sched"
ROUTER_ARN = "arn:aws:lambda:us-east-1:328440206208:function:afterhours-holiday-router"
@pytest.fixture
def holiday_env(monkeypatch):
"""Wire the scheduler/router ARNs so schedule creation is exercised."""
monkeypatch.setenv("HOLIDAY_ROUTER_ARN", ROUTER_ARN)
monkeypatch.setenv("HOLIDAY_SCHEDULER_ROLE_ARN", ROLE_ARN)
monkeypatch.setenv("HOLIDAY_SCHEDULE_GROUP", "default")
@pytest.fixture
def scheduler():
return boto3.client("scheduler", region_name="us-east-1")
def _admin(slackbot_app, schedule, text, respond, client, channel="C_TEST"):
slackbot_app._handle_admin(
respond, schedule, "U_ADMIN", text, True, client, channel
)
def _schedule_names(scheduler):
return {s["Name"] for s in scheduler.list_schedules()["Schedules"]}
class TestAdd:
@freeze_time(WED)
def test_creates_record_schedules_and_announces(
self,
slackbot_app,
schedule,
dynamodb_table,
holiday_env,
scheduler,
respond,
client,
text_of,
monkeypatch,
):
inline = _spy_inline(slackbot_app, monkeypatch)
_admin(
slackbot_app,
schedule,
"admin holiday add 2026-07-04 2 Independence Day",
respond,
client,
)
holiday = schedule.get_holiday("2026-07-04")
assert holiday is not None
assert holiday["slots"] == 2
assert holiday["label"] == "Independence Day"
assert float(holiday["multiplier"]) == 1.5 # CONFIG default
assert set(holiday["schedule_names"]) == {
"holiday-activate-20260704",
"holiday-deactivate-20260704",
}
# Both one-off schedules exist in EventBridge Scheduler.
assert _schedule_names(scheduler) >= set(holiday["schedule_names"])
# Future holiday → not inline-activated.
inline.assert_not_called()
# Channel announcement + confirmation.
assert client.chat_postMessage.call_args.kwargs["channel"] == "C_TEST"
assert "Scheduled" in text_of(respond)
@freeze_time(WED)
def test_custom_multiplier(
self,
slackbot_app,
schedule,
dynamodb_table,
holiday_env,
scheduler,
respond,
client,
monkeypatch,
):
_spy_inline(slackbot_app, monkeypatch)
_admin(
slackbot_app,
schedule,
"admin holiday add 2026-07-04 3 x2 Independence Day",
respond,
client,
)
holiday = schedule.get_holiday("2026-07-04")
assert float(holiday["multiplier"]) == 2.0
assert holiday["slots"] == 3
assert holiday["label"] == "Independence Day"
@freeze_time(SAT_MIDDAY)
def test_inline_activation_when_window_open(
self,
slackbot_app,
schedule,
dynamodb_table,
holiday_env,
scheduler,
respond,
client,
monkeypatch,
):
inline = _spy_inline(slackbot_app, monkeypatch)
_admin(
slackbot_app,
schedule,
"admin holiday add 2026-07-04 2 Independence Day",
respond,
client,
)
# Added mid-window → activate now (the 08:00 schedule already passed).
inline.assert_called_once()
assert inline.call_args.args[1] == "2026-07-04"
@freeze_time(WED)
def test_duplicate_rolls_back_schedules(
self,
slackbot_app,
schedule,
seed,
dynamodb_table,
holiday_env,
scheduler,
respond,
client,
text_of,
monkeypatch,
):
_spy_inline(slackbot_app, monkeypatch)
seed.holiday("2026-07-04", slots=1, label="Existing")
_admin(
slackbot_app,
schedule,
"admin holiday add 2026-07-04 2 Independence Day",
respond,
client,
)
assert "already exists" in text_of(respond).lower()
# The schedules created for the duplicate attempt were deleted again.
assert _schedule_names(scheduler) == set()
@freeze_time(WED)
def test_past_date_rejected(
self,
slackbot_app,
schedule,
holiday_env,
respond,
client,
text_of,
monkeypatch,
):
_spy_inline(slackbot_app, monkeypatch)
_admin(
slackbot_app,
schedule,
"admin holiday add 2026-06-01 2 Old Day",
respond,
client,
)
assert "past" in text_of(respond).lower()
@freeze_time(WED)
def test_bad_slots(
self,
slackbot_app,
schedule,
holiday_env,
respond,
client,
text_of,
monkeypatch,
):
_spy_inline(slackbot_app, monkeypatch)
_admin(
slackbot_app,
schedule,
"admin holiday add 2026-07-04 lots Independence Day",
respond,
client,
)
assert "whole number" in text_of(respond).lower()
@freeze_time(WED)
def test_missing_label(
self,
slackbot_app,
schedule,
holiday_env,
respond,
client,
text_of,
monkeypatch,
):
_spy_inline(slackbot_app, monkeypatch)
_admin(
slackbot_app,
schedule,
"admin holiday add 2026-07-04 2 x2",
respond,
client,
)
assert "label is required" in text_of(respond).lower()
def test_usage(self, slackbot_app, schedule, respond, client, text_of):
_admin(slackbot_app, schedule, "admin holiday add 2026-07-04", respond, client)
assert "Usage" in text_of(respond)
class TestRemove:
@freeze_time(WED)
def test_removes_record_and_outstanding_schedules(
self,
slackbot_app,
schedule,
dynamodb_table,
holiday_env,
scheduler,
respond,
client,
text_of,
monkeypatch,
):
_spy_inline(slackbot_app, monkeypatch)
_admin(
slackbot_app,
schedule,
"admin holiday add 2026-07-04 2 Independence Day",
respond,
client,
)
assert _schedule_names(scheduler) != set()
_admin(
slackbot_app, schedule, "admin holiday remove 2026-07-04", respond, client
)
assert schedule.get_holiday("2026-07-04") is None
assert _schedule_names(scheduler) == set() # outstanding schedules deleted
assert "Removed" in text_of(respond)
@freeze_time(WED)
def test_no_holiday(self, slackbot_app, schedule, respond, client, text_of):
_admin(
slackbot_app, schedule, "admin holiday remove 2026-07-04", respond, client
)
assert "No holiday" in text_of(respond)
class TestList:
@freeze_time(WED)
def test_lists_upcoming_only(
self, slackbot_app, schedule, seed, respond, client, text_of
):
seed.holiday("2026-07-04", slots=2, label="Independence Day")
seed.holiday("2026-12-25", slots=1, label="Christmas")
seed.holiday("2026-01-01", slots=1, label="New Year") # past
_admin(slackbot_app, schedule, "admin holiday list", respond, client)
out = text_of(respond)
assert "Independence Day" in out
assert "Christmas" in out
assert "New Year" not in out # past holiday filtered out
@freeze_time(WED)
def test_shows_filled_counts(
self, slackbot_app, schedule, seed, respond, client, text_of
):
seed.holiday(
"2026-07-04",
slots=2,
label="Independence Day",
assignees={"114": {"name": "Alice", "claimed_at": "x"}},
)
_admin(slackbot_app, schedule, "admin holiday list", respond, client)
assert "1/2 filled" in text_of(respond)
@freeze_time(WED)
def test_empty(self, slackbot_app, schedule, respond, client, text_of):
_admin(slackbot_app, schedule, "admin holiday list", respond, client)
assert "No upcoming holidays" in text_of(respond)
class TestDispatch:
def test_unknown_holiday_command(
self, slackbot_app, schedule, respond, client, text_of
):
_admin(slackbot_app, schedule, "admin holiday frobnicate", respond, client)
assert "Unknown holiday command" in text_of(respond)
def _spy_inline(slackbot_app, monkeypatch):
"""Replace inline activation (a real Lambda invoke) with a spy."""
from unittest.mock import MagicMock
spy = MagicMock(name="_activate_holiday_inline")
monkeypatch.setattr(slackbot_app, "_activate_holiday_inline", spy)
return spy