mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 20:43:12 +00:00
* Add Slack admin modals + App Home admin section Replace the two most error-prone positional admin commands with Block Kit modals (override and holiday-add) opened from a new App Home admin section, while keeping the typed subcommands as a fallback. Validation and side effects are factored into shared helpers so the modal and command paths can't drift, and every action/view handler re-checks is_admin against get_admin_users() so a modal opened from Home can't bypass authorization. Adds Schedule.list_overrides for the upcoming-overrides overview. Refs: #136 * Update changelog date to July 02, 2026 * Add point-and-click admin actions in Slack for easier overrides and holidays * [#136] Add admin UI evaluation spike doc (#140) Co-authored-by: seahaven-openswe[bot] <296972425+seahaven-openswe[bot]@users.noreply.github.com> Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
189 lines
7.3 KiB
Python
189 lines
7.3 KiB
Python
"""Tests for the admin Block Kit modals — opening (views.open), submitting
|
|
(view_submission), shared validation, and non-admin rejection (the IDOR case:
|
|
a modal can be opened from Home, so the surface is never trusted)."""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from freezegun import freeze_time
|
|
|
|
ADMIN = "U_ADMIN"
|
|
INTRUDER = "U_INTRUDER"
|
|
|
|
# 2026-06-01 is a Monday → night shift; 18:00 ET is inside the night window.
|
|
MON_NIGHT = "2026-06-01 18:00:00"
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_schedule(schedule, seed):
|
|
seed.config(admin_users=[ADMIN])
|
|
return schedule
|
|
|
|
|
|
@pytest.fixture
|
|
def ack():
|
|
return MagicMock(name="ack")
|
|
|
|
|
|
def _override_view(date="2026-12-25", ext="114", shift="night"):
|
|
return {
|
|
"state": {
|
|
"values": {
|
|
"date": {"date": {"selected_date": date}},
|
|
"extension": {"extension": {"selected_option": {"value": ext}}},
|
|
"shift_type": {"shift_type": {"selected_option": {"value": shift}}},
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def _holiday_view(date="2026-12-25", slots="2", multiplier="", label="Christmas"):
|
|
return {
|
|
"state": {
|
|
"values": {
|
|
"date": {"date": {"selected_date": date}},
|
|
"slots": {"slots": {"value": slots}},
|
|
"multiplier": {"multiplier": {"value": multiplier or None}},
|
|
"label": {"label": {"value": label}},
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def _body(user_id=ADMIN, trigger_id="T123"):
|
|
return {"user": {"id": user_id}, "trigger_id": trigger_id}
|
|
|
|
|
|
class TestOpenModals:
|
|
def test_admin_opens_override_modal(
|
|
self, slackbot_app, admin_schedule, seed, client
|
|
):
|
|
seed.roster("114", "Alice")
|
|
slackbot_app.open_override_modal(_body(), client, admin_schedule)
|
|
view = client.views_open.call_args.kwargs["view"]
|
|
assert view["callback_id"] == "admin_override_submit"
|
|
|
|
def test_admin_opens_holiday_modal(self, slackbot_app, admin_schedule, client):
|
|
slackbot_app.open_holiday_add_modal(_body(), client, admin_schedule)
|
|
view = client.views_open.call_args.kwargs["view"]
|
|
assert view["callback_id"] == "admin_holiday_add_submit"
|
|
|
|
def test_non_admin_cannot_open_override(self, slackbot_app, admin_schedule, client):
|
|
slackbot_app.open_override_modal(_body(INTRUDER), client, admin_schedule)
|
|
client.views_open.assert_not_called()
|
|
|
|
def test_non_admin_cannot_open_holiday(self, slackbot_app, admin_schedule, client):
|
|
slackbot_app.open_holiday_add_modal(_body(INTRUDER), client, admin_schedule)
|
|
client.views_open.assert_not_called()
|
|
|
|
|
|
class TestOverrideSubmission:
|
|
def test_happy_path(self, slackbot_app, admin_schedule, seed, ack, client):
|
|
seed.roster("114", "Alice")
|
|
slackbot_app.handle_override_submission(
|
|
ack, _body(), _override_view(), client, admin_schedule, None
|
|
)
|
|
assert admin_schedule.get_override("2026-12-25")["extension"] == "114"
|
|
ack.assert_called_once_with()
|
|
assert client.chat_postMessage.call_args.kwargs["channel"] == ADMIN
|
|
|
|
@freeze_time(MON_NIGHT)
|
|
def test_same_day_repoints_and_confirms(
|
|
self, slackbot_app, admin_schedule, seed, ack, client, routing_spy
|
|
):
|
|
seed.roster("114", "Alice")
|
|
slackbot_app.handle_override_submission(
|
|
ack,
|
|
_body(),
|
|
_override_view(date="2026-06-01"),
|
|
client,
|
|
admin_schedule,
|
|
None,
|
|
)
|
|
routing_spy.assert_called_once_with("114")
|
|
assert "repointed" in client.chat_postMessage.call_args.kwargs["text"].lower()
|
|
|
|
def test_unknown_extension_surfaces_field_error(
|
|
self, slackbot_app, admin_schedule, ack, client
|
|
):
|
|
slackbot_app.handle_override_submission(
|
|
ack, _body(), _override_view(ext="999"), client, admin_schedule, None
|
|
)
|
|
kwargs = ack.call_args.kwargs
|
|
assert kwargs["response_action"] == "errors"
|
|
assert "extension" in kwargs["errors"]
|
|
assert admin_schedule.get_override("2026-12-25") is None
|
|
|
|
def test_non_admin_rejected(self, slackbot_app, admin_schedule, seed, ack, client):
|
|
seed.roster("114", "Alice")
|
|
slackbot_app.handle_override_submission(
|
|
ack, _body(INTRUDER), _override_view(), client, admin_schedule, None
|
|
)
|
|
assert ack.call_args.kwargs["response_action"] == "errors"
|
|
assert admin_schedule.get_override("2026-12-25") is None
|
|
|
|
|
|
@freeze_time("2026-06-01 12:00:00")
|
|
class TestHolidaySubmission:
|
|
def test_happy_path(self, slackbot_app, admin_schedule, ack, client, monkeypatch):
|
|
monkeypatch.setattr(slackbot_app, "_activate_holiday_inline", MagicMock())
|
|
slackbot_app.handle_holiday_add_submission(
|
|
ack, _body(), _holiday_view(multiplier="2"), client, admin_schedule, "C_T"
|
|
)
|
|
holiday = admin_schedule.get_holiday("2026-12-25")
|
|
assert holiday["slots"] == 2
|
|
assert holiday["label"] == "Christmas"
|
|
assert float(holiday["multiplier"]) == 2.0
|
|
ack.assert_called_once_with()
|
|
assert client.chat_postMessage.call_args.kwargs["channel"] == ADMIN
|
|
|
|
def test_default_multiplier_when_blank(
|
|
self, slackbot_app, admin_schedule, ack, client, monkeypatch
|
|
):
|
|
monkeypatch.setattr(slackbot_app, "_activate_holiday_inline", MagicMock())
|
|
slackbot_app.handle_holiday_add_submission(
|
|
ack, _body(), _holiday_view(), client, admin_schedule, None
|
|
)
|
|
assert float(admin_schedule.get_holiday("2026-12-25")["multiplier"]) == 1.5
|
|
|
|
def test_past_date_surfaces_field_error(
|
|
self, slackbot_app, admin_schedule, ack, client
|
|
):
|
|
slackbot_app.handle_holiday_add_submission(
|
|
ack, _body(), _holiday_view(date="2026-01-01"), client, admin_schedule, None
|
|
)
|
|
errors = ack.call_args.kwargs["errors"]
|
|
assert "date" in errors and "past" in errors["date"].lower()
|
|
assert admin_schedule.get_holiday("2026-01-01") is None
|
|
|
|
def test_bad_slots_surfaces_field_error(
|
|
self, slackbot_app, admin_schedule, ack, client
|
|
):
|
|
slackbot_app.handle_holiday_add_submission(
|
|
ack, _body(), _holiday_view(slots="lots"), client, admin_schedule, None
|
|
)
|
|
assert "slots" in ack.call_args.kwargs["errors"]
|
|
assert admin_schedule.get_holiday("2026-12-25") is None
|
|
|
|
def test_missing_label_surfaces_field_error(
|
|
self, slackbot_app, admin_schedule, ack, client
|
|
):
|
|
slackbot_app.handle_holiday_add_submission(
|
|
ack, _body(), _holiday_view(label=""), client, admin_schedule, None
|
|
)
|
|
assert "label" in ack.call_args.kwargs["errors"]
|
|
|
|
def test_bad_multiplier_surfaces_field_error(
|
|
self, slackbot_app, admin_schedule, ack, client
|
|
):
|
|
slackbot_app.handle_holiday_add_submission(
|
|
ack, _body(), _holiday_view(multiplier="huge"), client, admin_schedule, None
|
|
)
|
|
assert "multiplier" in ack.call_args.kwargs["errors"]
|
|
|
|
def test_non_admin_rejected(self, slackbot_app, admin_schedule, ack, client):
|
|
slackbot_app.handle_holiday_add_submission(
|
|
ack, _body(INTRUDER), _holiday_view(), client, admin_schedule, None
|
|
)
|
|
assert ack.call_args.kwargs["response_action"] == "errors"
|
|
assert admin_schedule.get_holiday("2026-12-25") is None
|