afterhours-shift-manager/tests/shared/test_portal_ops.py
Adam Moussa 470e00affb
Some checks are pending
Deploy API / Deploy API to dev (push) Waiting to run
Deploy API / Deploy API to prod (push) Waiting to run
feat(schedule): align the work week with Sunday-Saturday payroll (DEV-300) (#282)
* feat(schedule): align the work week with Sunday-Saturday payroll

Saturday night stays in the week that ends Saturday, and the first Flex close skips dates already sent.

* fix(slack-bot): show the last pay close on Sunday

The Monday 7am row for the week that just ended is not written yet, so /oncall pay now falls back to the prior close.
2026-09-25 17:56:46 +00:00

193 lines
6.7 KiB
Python

"""Tests for portal shift mutations."""
from zoneinfo import ZoneInfo
import freezegun
import pytest
from shared.portal_ops import (
ActionError,
admin_clear,
admin_holiday_add,
admin_open,
drop,
pick,
respond_swap,
snapshot,
swap,
)
from shared import side_effects as effects
ET = ZoneInfo("America/New_York")
@pytest.fixture
def quiet_slack(monkeypatch):
monkeypatch.setattr(effects, "slack_token", lambda: None)
monkeypatch.setattr(effects, "refresh_schedule_post", lambda *a, **k: None)
monkeypatch.setattr(effects, "post_shift_change", lambda *a, **k: None)
monkeypatch.setattr(effects, "dm_late_pickup_admins", lambda *a, **k: 1)
monkeypatch.setattr(effects, "dm_swap_request", lambda *a, **k: True)
monkeypatch.setattr(effects, "dm_text", lambda *a, **k: None)
monkeypatch.setattr(effects, "maybe_repoint_today", lambda *a, **k: False)
def _alice(schedule, seed):
seed.roster("114", "Alice", slack_user_id="U_ALICE", email="alice@seahavenind.com")
seed.roster("115", "Bob", slack_user_id="U_BOB", email="bob@seahavenind.com")
return schedule.get_employee_by_extension("114")
def _admin(schedule, seed):
employee = _alice(schedule, seed)
seed.config(admin_users=["U_ALICE"])
return employee
def test_pick_open_shift(schedule, seed, quiet_slack):
employee = _alice(schedule, seed)
seed.open_shift("2026-06-10")
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
result = pick(schedule, employee, "2026-06-10", "night")
assert result["ok"] is True
assert schedule.get_override("2026-06-10")["extension"] == "114"
def test_drop_lock(schedule, seed, quiet_slack):
employee = _alice(schedule, seed)
seed.override("2026-06-10", "114", "Alice")
with freezegun.freeze_time("2026-06-10 12:00:00-04:00"):
with pytest.raises(ActionError) as err:
drop(schedule, employee, "2026-06-10", "night")
assert err.value.code == "DROP_LOCK"
def test_swap_creates_pending(schedule, seed, quiet_slack):
employee = _alice(schedule, seed)
seed.override("2026-06-12", "114", "Alice")
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
result = swap(schedule, employee, "2026-06-12", "115", "night")
assert result["ok"] is True
pending = schedule.get_swap("2026-06-12", "night")
assert pending["target_ext"] == "115"
snap = snapshot(schedule, schedule.get_employee_by_extension("115"))
assert snap["pendingSwaps"][0]["requesterExt"] == "114"
assert snap["pendingSwaps"][0]["incoming"] is True
mine = snapshot(schedule, employee)
assert mine["pendingSwaps"][0]["incoming"] is False
def test_late_pickup_creates_request(schedule, seed, quiet_slack):
employee = _alice(schedule, seed)
seed.open_shift("2026-06-10")
with freezegun.freeze_time("2026-06-10 20:00:00-04:00"):
result = pick(schedule, employee, "2026-06-10", "night")
assert result["latePickup"] is True
req = schedule.get_pickup_request("2026-06-10", "night", "114")
assert req["status"] == "pending"
def test_snapshot_includes_weekday_holiday_day_shift(schedule, seed):
employee = _alice(schedule, seed)
seed.holiday(
"2026-06-10",
assignees={"114": {"name": "Alice", "claimed_at": "now"}},
)
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
snap = snapshot(schedule, employee)
assert snap["weekStart"] == "2026-06-07"
wednesday = next(day for day in snap["days"] if day["date"] == "2026-06-10")
assert wednesday["slots"][0]["kind"] == "holiday"
assert wednesday["slots"][0]["shiftType"] == "day"
assert wednesday["slots"][0]["mine"] is True
def test_drop_accepts_day_for_holiday_slot(schedule, seed, quiet_slack):
employee = _alice(schedule, seed)
seed.holiday(
"2026-06-10",
assignees={"114": {"name": "Alice", "claimed_at": "now"}},
)
with freezegun.freeze_time("2026-06-08 06:00:00-04:00"):
result = drop(schedule, employee, "2026-06-10", "day")
assert result["ok"] is True
assert schedule.get_holiday("2026-06-10")["assignees"] == {}
def test_swap_accept_rechecks_resolved_holder(schedule, seed, quiet_slack):
employee = _alice(schedule, seed)
seed.roster("116", "Charlie", slack_user_id="U_CHARLIE")
seed.weekly("Friday", "114", "Alice")
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
swap(schedule, employee, "2026-06-12", "115", "night")
seed.weekly("Friday", "116", "Charlie")
with pytest.raises(ActionError) as err:
respond_swap(
schedule,
schedule.get_employee_by_extension("115"),
"2026-06-12",
"night",
True,
)
assert err.value.code == "CONFLICT"
assert schedule.get_override("2026-06-12") is None
assert schedule.get_swap("2026-06-12", "night") is None
def test_admin_open_repoints_active_shift_to_fallback(
schedule, seed, quiet_slack, monkeypatch
):
employee = _admin(schedule, seed)
calls = []
monkeypatch.setattr(
effects,
"maybe_repoint_today",
lambda date, shift_type, extension: (
calls.append((date, shift_type, extension)) or True
),
)
with freezegun.freeze_time("2026-06-10 20:00:00-04:00"):
result = admin_open(schedule, employee, "2026-06-10", "night")
assert result["repointed"] is True
assert calls == [("2026-06-10", "night", "100")]
def test_admin_clear_repoints_active_shift_to_resolved_holder(
schedule, seed, quiet_slack, monkeypatch
):
employee = _admin(schedule, seed)
seed.weekly("Wednesday", "115", "Bob")
seed.override("2026-06-10", "114", "Alice")
calls = []
monkeypatch.setattr(
effects,
"maybe_repoint_today",
lambda date, shift_type, extension: (
calls.append((date, shift_type, extension)) or True
),
)
with freezegun.freeze_time("2026-06-10 20:00:00-04:00"):
result = admin_clear(schedule, employee, "2026-06-10", "night")
assert result["repointed"] is True
assert calls == [("2026-06-10", "night", "115")]
def test_invalid_holiday_multiplier_rejected_before_schedules(
schedule, seed, quiet_slack, monkeypatch
):
employee = _admin(schedule, seed)
monkeypatch.setattr(
effects, "create_holiday_schedules", lambda _date: pytest.fail("called")
)
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
with pytest.raises(ActionError) as err:
admin_holiday_add(
schedule,
employee,
"2026-12-25",
2,
"Christmas",
"not-a-number",
)
assert err.value.code == "INVALID_MULTIPLIER"
assert schedule.get_holiday("2026-12-25") is None