mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 09:03:11 +00:00
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.
311 lines
9.1 KiB
Python
311 lines
9.1 KiB
Python
"""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
|