afterhours-shift-manager/tests/slack_bot/test_handle_holiday.py

312 lines
9.1 KiB
Python
Raw Normal View History

"""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