mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 06:43:12 +00:00
* feat(api): collapse Slack, portal, and jobs onto Fargate (PLAT-216) Move HTTP and scheduled work onto one always-on Flask task so after-hours loses Lambda cold start without changing the Cognito or roster contracts. * fix(portal-api): keep CORS headers on unexpected 500s Portal SPA error handling needs Access-Control-Allow-Origin even when DynamoDB or other internals fail, otherwise the browser hides the 500. * fix(api): retarget holidays per account and ship App Home changelog (PLAT-216) * fix(iam): list ECS tasks and fail closed on non-prod Paychex (PLAT-216) * fix(portal-api): serve portal JSON with an explicit JSON content type
101 lines
3.2 KiB
Python
101 lines
3.2 KiB
Python
"""Tests for the portal shift API handler."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
|
|
def _event(method="GET", path="/api/shifts", body=None, token="id-token", origin=None):
|
|
headers = {"authorization": f"Bearer {token}"}
|
|
if origin:
|
|
headers["origin"] = origin
|
|
payload = None
|
|
if body is not None:
|
|
payload = json.dumps(body)
|
|
return {
|
|
"version": "2.0",
|
|
"routeKey": f"{method} {path}",
|
|
"rawPath": path,
|
|
"headers": headers,
|
|
"queryStringParameters": {},
|
|
"requestContext": {"http": {"method": method, "path": path}},
|
|
"body": payload,
|
|
"isBase64Encoded": False,
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def identity(portalapi_app, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"shared.portal_http.verify_cognito_id_token",
|
|
lambda _token: {"name": "Alice", "email": "alice@seahavenind.com"},
|
|
)
|
|
|
|
|
|
def test_unlinked_email_returns_linked_false(portalapi_app, schedule, identity):
|
|
result = portalapi_app.handler(_event(), None)
|
|
assert result["statusCode"] == 200
|
|
body = json.loads(result["body"])
|
|
assert body["linked"] is False
|
|
assert body["email"] == "alice@seahavenind.com"
|
|
|
|
|
|
def test_401_without_bearer(portalapi_app, schedule):
|
|
event = _event()
|
|
event["headers"] = {}
|
|
result = portalapi_app.handler(event, None)
|
|
assert result["statusCode"] == 401
|
|
|
|
|
|
def test_linked_snapshot_and_admin_flag(portalapi_app, schedule, seed, identity):
|
|
seed.roster(
|
|
"114",
|
|
"Alice",
|
|
slack_user_id="U_ADMIN",
|
|
email="alice@seahavenind.com",
|
|
)
|
|
seed.config(admin_users=["U_ADMIN"])
|
|
seed.weekly("Monday", "114", "Alice")
|
|
result = portalapi_app.handler(_event(), None)
|
|
assert result["statusCode"] == 200
|
|
body = json.loads(result["body"])
|
|
assert body["linked"] is True
|
|
assert body["isAdmin"] is True
|
|
assert body["me"]["extension"] == "114"
|
|
assert len(body["days"]) == 7
|
|
|
|
|
|
def test_non_admin_cannot_override(portalapi_app, schedule, seed, identity):
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE", email="alice@seahavenind.com")
|
|
seed.config(admin_users=["U_OTHER"])
|
|
result = portalapi_app.handler(
|
|
_event(
|
|
method="POST",
|
|
path="/api/shifts/admin/override",
|
|
body={"date": "2026-06-10", "extension": "114", "shiftType": "night"},
|
|
),
|
|
None,
|
|
)
|
|
assert result["statusCode"] == 403
|
|
|
|
|
|
def test_cors_header_for_portal_origin(portalapi_app, schedule, identity):
|
|
result = portalapi_app.handler(_event(origin="https://internal.seahaven.com"), None)
|
|
assert (
|
|
result["headers"]["Access-Control-Allow-Origin"]
|
|
== "https://internal.seahaven.com"
|
|
)
|
|
|
|
|
|
def test_unexpected_failure_keeps_cors(portalapi_app, identity, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"shared.portal_http.ShiftSchedule",
|
|
lambda: (_ for _ in ()).throw(RuntimeError("ddb down")),
|
|
)
|
|
result = portalapi_app.handler(_event(origin="https://internal.seahaven.com"), None)
|
|
assert result["statusCode"] == 500
|
|
assert (
|
|
result["headers"]["Access-Control-Allow-Origin"]
|
|
== "https://internal.seahaven.com"
|
|
)
|
|
assert json.loads(result["body"])["error"]["code"] == "INTERNAL"
|