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