mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 22:53:12 +00:00
* feat(api): add IAM-authenticated menu publication Keep weekly menu writes behind Lambda so the GitHub runtime role cannot access the shared DynamoDB table directly. * refactor(workflow): publish weekly menus through API Use SigV4 requests for settings and menu publication so the scheduled workflow no longer needs direct DynamoDB access. * fix: address review comments * style(python): apply Ruff formatting
1812 lines
60 KiB
Python
1812 lines
60 KiB
Python
"""Unit tests for functions/submit_order/handler.py
|
|
|
|
All external dependencies (DynamoDB, SSM, Google tokeninfo, Lambda invoke) are
|
|
mocked — no real AWS calls are made.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
from datetime import datetime
|
|
from unittest.mock import MagicMock, patch
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Environment variables required by the handler at import time
|
|
# ---------------------------------------------------------------------------
|
|
os.environ.setdefault("TABLE_NAME", "test-orders-table")
|
|
os.environ.setdefault(
|
|
"SLACK_NOTIFIER_ARN",
|
|
"arn:aws:lambda:us-east-1:000000000000:function:test-slack-notifier",
|
|
)
|
|
os.environ.setdefault("GOOGLE_CLIENT_ID_PARAM", "")
|
|
|
|
import importlib.util
|
|
|
|
_handler_path = os.path.join(
|
|
os.path.dirname(__file__), os.pardir, "functions", "submit_order", "handler.py"
|
|
)
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"submit_order_handler", os.path.abspath(_handler_path)
|
|
)
|
|
submit_order_handler = importlib.util.module_from_spec(_spec)
|
|
sys.modules["submit_order_handler"] = submit_order_handler
|
|
_spec.loader.exec_module(submit_order_handler)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
EASTERN = ZoneInfo("America/New_York")
|
|
VALID_GOOGLE_CLIENT_ID = "test-google-client-id"
|
|
|
|
|
|
def _make_event(
|
|
method="POST",
|
|
path="/submit",
|
|
body=None,
|
|
headers=None,
|
|
path_parameters=None,
|
|
):
|
|
"""Build an API Gateway v2 HTTP-format event."""
|
|
event = {
|
|
"requestContext": {
|
|
"http": {
|
|
"method": method,
|
|
"path": path,
|
|
}
|
|
},
|
|
"rawPath": path,
|
|
"headers": headers or {},
|
|
"body": json.dumps(body) if body is not None else "{}",
|
|
"pathParameters": path_parameters or {},
|
|
}
|
|
return event
|
|
|
|
|
|
def _submit_event(
|
|
items,
|
|
employee_name="Test User",
|
|
employee_email="test.user@seahavenind.com",
|
|
google_token="valid-google-token",
|
|
extra_body=None,
|
|
):
|
|
"""Shortcut for a typical POST /submit event with items."""
|
|
body = {
|
|
"employee_name": employee_name,
|
|
"employee_email": employee_email,
|
|
"items": items,
|
|
}
|
|
if google_token is not None:
|
|
body["google_id_token"] = google_token
|
|
if extra_body:
|
|
body.update(extra_body)
|
|
return _make_event(
|
|
method="POST",
|
|
path="/submit",
|
|
body=body,
|
|
)
|
|
|
|
|
|
def _parse_response(result):
|
|
"""Parse the Lambda response dict into (status_code, body_dict)."""
|
|
return result["statusCode"], json.loads(result["body"])
|
|
|
|
|
|
def _make_items(retail_prices_and_qtys):
|
|
"""Build item list from [(retail_price, quantity), ...]."""
|
|
items = []
|
|
for i, (price, qty) in enumerate(retail_prices_and_qtys):
|
|
items.append(
|
|
{
|
|
"name": f"Meal {i + 1}",
|
|
"retail_price": price,
|
|
"quantity": qty,
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
def _menu_doc_from_retail_pairs(pairs):
|
|
"""Dynamo-style menu document matching _make_items retail pairs."""
|
|
return {
|
|
"meals": [
|
|
{"name": f"Meal {i + 1}", "price": price}
|
|
for i, (price, _) in enumerate(pairs)
|
|
]
|
|
}
|
|
|
|
|
|
# ===========================================================================
|
|
# WEEKLY MENU PUBLICATION
|
|
# ===========================================================================
|
|
|
|
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={
|
|
"bulk_discount_percent": 10,
|
|
"company_subsidy_percent": 50,
|
|
"admin_emails": ["admin@seahavenind.com"],
|
|
},
|
|
)
|
|
def test_publish_settings_returns_only_pricing(mock_settings):
|
|
result = submit_order_handler.lambda_handler(
|
|
_make_event(method="GET", path="/api/publish/settings"), None
|
|
)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200
|
|
assert body == {
|
|
"bulk_discount_percent": 10.0,
|
|
"company_subsidy_percent": 50.0,
|
|
}
|
|
|
|
|
|
@patch("submit_order_handler.put_menu")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W30")
|
|
def test_publish_menu_uses_current_week_and_server_meal_count(mock_week, mock_put):
|
|
menu = {
|
|
"scraped_at": "2026-07-27T07:31:00-04:00",
|
|
"menu_url": "https://example.com/menu",
|
|
"meal_count": 999,
|
|
"meals": [{"name": "Chicken Bowl", "price": 12.5}],
|
|
}
|
|
result = submit_order_handler.lambda_handler(
|
|
_make_event(method="POST", path="/api/publish/menu", body=menu), None
|
|
)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200
|
|
assert body == {"status": "published", "week": "2026-W30", "meal_count": 1}
|
|
mock_put.assert_called_once_with(
|
|
"2026-W30",
|
|
{
|
|
"scraped_at": menu["scraped_at"],
|
|
"menu_url": menu["menu_url"],
|
|
"meal_count": 1,
|
|
"meals": menu["meals"],
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"body",
|
|
[
|
|
{},
|
|
{"meals": []},
|
|
{"meals": [{"name": "", "price": 12.5}]},
|
|
{"meals": [{"name": "Chicken Bowl", "price": -1}]},
|
|
{"meals": [{"name": "Chicken Bowl", "price": "12.50"}]},
|
|
],
|
|
)
|
|
@patch("submit_order_handler.put_menu")
|
|
def test_publish_menu_rejects_invalid_payload(mock_put, body):
|
|
result = submit_order_handler.lambda_handler(
|
|
_make_event(method="POST", path="/api/publish/menu", body=body), None
|
|
)
|
|
status, _ = _parse_response(result)
|
|
|
|
assert status == 400
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler.put_menu")
|
|
def test_publish_menu_rejects_invalid_json(mock_put):
|
|
event = _make_event(method="POST", path="/api/publish/menu")
|
|
event["body"] = "{"
|
|
status, _ = _parse_response(submit_order_handler.lambda_handler(event, None))
|
|
|
|
assert status == 400
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module-level patches that must be active before the handler is imported
|
|
# ---------------------------------------------------------------------------
|
|
# We patch boto3.client at the handler-module level so the module-level
|
|
# `_lambda = boto3.client("lambda")` call gets a mock.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# We need to reset module-level caches between tests to avoid cross-test
|
|
# leakage. The handler module caches settings and the Google client ID.
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_handler_caches():
|
|
"""Reset handler module-level caches before each test."""
|
|
submit_order_handler._settings = None
|
|
submit_order_handler._settings_ts = 0.0
|
|
submit_order_handler._google_client_id = None
|
|
submit_order_handler._google_client_id_ts = 0.0
|
|
yield
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_shared_caches():
|
|
"""Reset shared.secrets caches before each test."""
|
|
from shared import secrets
|
|
|
|
secrets._secret_cache.clear()
|
|
secrets._parameter_cache.clear()
|
|
yield
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _mock_google_tokeninfo():
|
|
"""Return a valid company identity unless a test overrides tokeninfo."""
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(
|
|
{
|
|
"aud": VALID_GOOGLE_CLIENT_ID,
|
|
"hd": "seahavenind.com",
|
|
"name": "Test User",
|
|
"email": "test.user@seahavenind.com",
|
|
}
|
|
).encode()
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
with patch("submit_order_handler.urllib.request.urlopen", return_value=mock_resp):
|
|
yield
|
|
|
|
|
|
# ===========================================================================
|
|
# PRICING PIPELINE (Critical)
|
|
# ===========================================================================
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 50},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_discount_two_step_rounding(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Retail $10.25, 10% bulk, 50% subsidy.
|
|
|
|
Step 1: bulk_price = 10.25 * 0.90 = 9.225 -> 9.23 (ROUND_HALF_UP)
|
|
Step 2: emp_price = 9.23 * 0.50 = 4.615 -> 4.62 (ROUND_HALF_UP)
|
|
Subtotal for qty=3: 4.62 * 3 = 13.86
|
|
"""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.25, 3)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.25, 3)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
|
|
saved_order = mock_put.call_args[0][2]
|
|
item = saved_order["items"][0]
|
|
|
|
assert item["bulk_price"] == 9.23, (
|
|
f"bulk_price should be 9.23 (10.25 * 0.90 rounded HALF_UP), got {item['bulk_price']}"
|
|
)
|
|
assert item["price"] == 4.62, (
|
|
f"emp_price should be 4.62 (9.23 * 0.50 rounded HALF_UP), got {item['price']}"
|
|
)
|
|
assert item["subtotal"] == 13.86, (
|
|
f"subtotal should be 13.86 (4.62 * 3), got {item['subtotal']}"
|
|
)
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 50, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_discount_rounding_half_up_boundary(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Retail $10.05, 50% bulk, 0% subsidy.
|
|
|
|
bulk_price = 10.05 * 0.50 = 5.025 -> 5.03 (ROUND_HALF_UP, not 5.02 banker's)
|
|
"""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.05, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.05, 1)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
|
|
saved_order = mock_put.call_args[0][2]
|
|
item = saved_order["items"][0]
|
|
|
|
assert item["bulk_price"] == 5.03, (
|
|
f"bulk_price should be 5.03 (ROUND_HALF_UP for .025), got {item['bulk_price']}"
|
|
)
|
|
assert item["price"] == 5.03, (
|
|
f"emp_price should equal bulk_price when subsidy is 0%, got {item['price']}"
|
|
)
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": -5, "company_subsidy_percent": 150},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_discount_clamping(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Negative bulk discount clamped to 0, subsidy >100 clamped to 100 (free)."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(20.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(20.00, 1)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
|
|
saved_order = mock_put.call_args[0][2]
|
|
item = saved_order["items"][0]
|
|
|
|
assert item["bulk_price"] == 20.00, (
|
|
f"bulk_price should be 20.00 (bulk_discount clamped to 0%), got {item['bulk_price']}"
|
|
)
|
|
assert item["price"] == 0.00, (
|
|
f"emp_price should be 0.00 (subsidy clamped to 100%), got {item['price']}"
|
|
)
|
|
assert saved_order["total"] == 0.00, (
|
|
f"total should be 0.00 for free items, got {saved_order['total']}"
|
|
)
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_discount_both_zero(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""When both discounts are 0%, emp_price equals retail price."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(15.99, 2)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(15.99, 2)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
|
|
saved_order = mock_put.call_args[0][2]
|
|
item = saved_order["items"][0]
|
|
|
|
assert item["retail_price"] == 15.99, (
|
|
f"retail_price mismatch: {item['retail_price']}"
|
|
)
|
|
assert item["bulk_price"] == 15.99, (
|
|
f"bulk_price should equal retail when bulk discount is 0%, got {item['bulk_price']}"
|
|
)
|
|
assert item["price"] == 15.99, (
|
|
f"emp_price should equal retail when both discounts are 0%, got {item['price']}"
|
|
)
|
|
assert item["subtotal"] == 31.98, (
|
|
f"subtotal should be 15.99 * 2 = 31.98, got {item['subtotal']}"
|
|
)
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 25},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_total_summation_multiple_items(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Total is the sum of individually rounded subtotals, not a global multiply.
|
|
|
|
Item A: retail=$10.00, bulk=10.00*0.90=9.00, emp=9.00*0.75=6.75, qty=2 -> subtotal=13.50
|
|
Item B: retail=$7.33, bulk=7.33*0.90=6.60, emp=6.60*0.75=4.95, qty=1 -> subtotal=4.95
|
|
Total = 13.50 + 4.95 = 18.45
|
|
"""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 2), (7.33, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 2), (7.33, 1)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
|
|
saved_order = mock_put.call_args[0][2]
|
|
|
|
item_a = saved_order["items"][0]
|
|
assert item_a["subtotal"] == 13.50, (
|
|
f"Item A subtotal expected 13.50, got {item_a['subtotal']}"
|
|
)
|
|
|
|
item_b = saved_order["items"][1]
|
|
assert item_b["subtotal"] == 4.95, (
|
|
f"Item B subtotal expected 4.95, got {item_b['subtotal']}"
|
|
)
|
|
|
|
assert saved_order["total"] == 18.45, (
|
|
f"total should be sum of rounded subtotals (13.50 + 4.95 = 18.45), got {saved_order['total']}"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# AUTHENTICATION (Critical + High)
|
|
# ===========================================================================
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch("submit_order_handler.get_parameter")
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_missing_ssm_param_with_env_var_fails_closed(
|
|
mock_get_menu,
|
|
mock_get_parameter,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Env var set but SSM param missing -> 503 (fail closed, not silent manual fallback)."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
class ParameterNotFoundError(Exception):
|
|
response = {"Error": {"Code": "ParameterNotFound"}}
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
mock_get_parameter.side_effect = ParameterNotFoundError("ParameterNotFound")
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{"GOOGLE_CLIENT_ID_PARAM": "/meal-order-manager/google-client-id"},
|
|
):
|
|
result = lambda_handler(_submit_event(items), None)
|
|
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 503, f"Expected 503 (fail closed), got {status}: {body}"
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
def test_google_auth_required_when_configured(
|
|
mock_gac,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""When Google auth is configured and no token is provided, return 403."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
# Body has name/email but no google_id_token
|
|
event = _submit_event(items, google_token=None)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 403, f"Expected 403, got {status}: {body}"
|
|
assert "Google authentication is required" in body["error"], (
|
|
f"Expected 'Google authentication is required' in error, got: {body['error']}"
|
|
)
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
def test_google_auth_bypass_prevention(
|
|
mock_gac,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Google auth enabled + name/email in body but no token -> 403 (can't bypass)."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
body = {
|
|
"employee_name": "Attacker Name",
|
|
"employee_email": "attacker@seahavenind.com",
|
|
"items": _make_items([(10.00, 1)]),
|
|
# No google_id_token — trying to bypass with manual name/email
|
|
}
|
|
event = _make_event(
|
|
method="POST",
|
|
path="/submit",
|
|
body=body,
|
|
)
|
|
result = lambda_handler(event, None)
|
|
status, body_resp = _parse_response(result)
|
|
|
|
assert status == 403, f"Expected 403 (bypass prevented), got {status}: {body_resp}"
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.urllib.request.urlopen")
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
def test_google_token_audience_mismatch(
|
|
mock_gac,
|
|
mock_urlopen,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Token with wrong audience -> 403."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
# Simulate Google returning token info with wrong audience
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(
|
|
{
|
|
"aud": "wrong-client-id.apps.googleusercontent.com",
|
|
"hd": "seahavenind.com",
|
|
"name": "Test User",
|
|
"email": "test@seahavenind.com",
|
|
}
|
|
).encode()
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
mock_urlopen.return_value = mock_resp
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
event = _submit_event(items, extra_body={"google_id_token": "fake-token"})
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 403, f"Expected 403 for audience mismatch, got {status}: {body}"
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.urllib.request.urlopen")
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
def test_google_token_domain_mismatch(
|
|
mock_gac,
|
|
mock_urlopen,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Token with wrong hosted domain -> 403."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(
|
|
{
|
|
"aud": VALID_GOOGLE_CLIENT_ID,
|
|
"hd": "evil-corp.com",
|
|
"name": "Evil User",
|
|
"email": "evil@evil-corp.com",
|
|
}
|
|
).encode()
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
mock_urlopen.return_value = mock_resp
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
event = _submit_event(items, extra_body={"google_id_token": "fake-token"})
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 403, f"Expected 403 for domain mismatch, got {status}: {body}"
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch(
|
|
"submit_order_handler.urllib.request.urlopen",
|
|
side_effect=urllib.error.URLError("Connection refused"),
|
|
)
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
def test_google_token_service_unavailable(
|
|
mock_gac,
|
|
mock_urlopen,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""URLError from Google tokeninfo -> 503."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
event = _submit_event(items, extra_body={"google_id_token": "fake-token"})
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 503, f"Expected 503 for service unavailable, got {status}: {body}"
|
|
assert "temporarily unavailable" in body["error"], (
|
|
f"Expected 'temporarily unavailable' in error, got: {body['error']}"
|
|
)
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch(
|
|
"submit_order_handler.urllib.request.urlopen",
|
|
side_effect=urllib.error.HTTPError(
|
|
"https://oauth2.googleapis.com/tokeninfo",
|
|
400,
|
|
"Bad Request",
|
|
{},
|
|
None,
|
|
),
|
|
)
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
def test_google_token_http_error_returns_403(
|
|
mock_gac,
|
|
mock_urlopen,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""HTTPError (e.g. 400 for expired token) -> 403, not 503."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
event = _submit_event(items, extra_body={"google_id_token": "expired-token"})
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 403, (
|
|
f"Expected 403 for HTTP error (bad token), got {status}: {body}"
|
|
)
|
|
assert "Invalid or unauthorized" in body["error"], (
|
|
f"Expected 'Invalid or unauthorized' in error, got: {body['error']}"
|
|
)
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id",
|
|
side_effect=Exception("ParameterNotFound"),
|
|
)
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
def test_ssm_failure_fails_closed(
|
|
mock_gac,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""SSM fetch failure with auth configured -> 503, not silent fallback to manual."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 503, (
|
|
f"Expected 503 for SSM failure (fail-closed), got {status}: {body}"
|
|
)
|
|
assert "temporarily unavailable" in body["error"], (
|
|
f"Expected 'temporarily unavailable' in error, got: {body['error']}"
|
|
)
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.urllib.request.urlopen")
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_google_token_valid_seahaven_com_domain(
|
|
mock_get_menu,
|
|
mock_gac,
|
|
mock_urlopen,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Google token with hd=seahaven.com (alternate domain) -> order saved."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(
|
|
{
|
|
"aud": VALID_GOOGLE_CLIENT_ID,
|
|
"hd": "seahaven.com",
|
|
"name": "Test Employee",
|
|
"email": "test@seahaven.com",
|
|
}
|
|
).encode()
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
mock_urlopen.return_value = mock_resp
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
event = _submit_event(
|
|
items,
|
|
extra_body={"google_id_token": "valid-token-seahaven"},
|
|
)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
saved_order = mock_put.call_args[0][2]
|
|
assert saved_order["employee_email"] == "test@seahaven.com"
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.urllib.request.urlopen")
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_google_token_valid_success(
|
|
mock_get_menu,
|
|
mock_gac,
|
|
mock_urlopen,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Valid Google token -> order saved with token's name/email."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(
|
|
{
|
|
"aud": VALID_GOOGLE_CLIENT_ID,
|
|
"hd": "seahavenind.com",
|
|
"name": "Adam Moussa",
|
|
"email": "adam.moussa@seahavenind.com",
|
|
}
|
|
).encode()
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
mock_urlopen.return_value = mock_resp
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
# Body has different name/email — should be overridden by token
|
|
event = _submit_event(
|
|
items,
|
|
employee_name="Wrong Name",
|
|
employee_email="wrong@seahavenind.com",
|
|
extra_body={"google_id_token": "valid-token-abc"},
|
|
)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
|
|
saved_order = mock_put.call_args[0][2]
|
|
assert saved_order["employee_name"] == "Adam Moussa", (
|
|
f"Name should come from Google token, got: {saved_order['employee_name']}"
|
|
)
|
|
assert saved_order["employee_email"] == "adam.moussa@seahavenind.com", (
|
|
f"Email should come from Google token, got: {saved_order['employee_email']}"
|
|
)
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch("submit_order_handler._get_google_client_id", return_value="")
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_missing_google_client_id_fails_closed(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""No Google client ID configured -> 503, never manual fallback."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(12.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(12.00, 1)])
|
|
event = _submit_event(
|
|
items, employee_name="Manual User", employee_email="manual@seahavenind.com"
|
|
)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 503, f"Expected fail-closed 503, got {status}: {body}"
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_submit_requires_no_api_key(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""A Google-authenticated submission succeeds without a shared API key."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200 without API key, got {status}: {body}"
|
|
mock_put.assert_called_once()
|
|
|
|
|
|
# ===========================================================================
|
|
# SLUG GENERATION (Critical)
|
|
# ===========================================================================
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch(
|
|
"submit_order_handler._verify_google_token",
|
|
return_value=(
|
|
{"name": "Adam Moussa", "email": "Adam.Moussa@seahavenind.com"},
|
|
"ok",
|
|
),
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_slug_from_email(
|
|
mock_get_menu,
|
|
mock_verify,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""'Adam.Moussa@seahavenind.com' -> slug 'adam.moussa@seahavenind.com'."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
event = _submit_event(
|
|
items, employee_name="Adam Moussa", employee_email="Adam.Moussa@seahavenind.com"
|
|
)
|
|
result = lambda_handler(event, None)
|
|
status, _ = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}"
|
|
|
|
# put_order is called with (week, slug, order_data)
|
|
slug = mock_put.call_args[0][1]
|
|
assert slug == "adam.moussa@seahavenind.com", (
|
|
f"Slug should be 'adam.moussa@seahavenind.com', got '{slug}'"
|
|
)
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler._verify_google_token")
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_slug_edge_cases(
|
|
mock_get_menu,
|
|
mock_verify,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Slug uses the full lowercase Google email across allowed domains."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_verify.side_effect = [
|
|
(
|
|
{
|
|
"name": "First Middle Last",
|
|
"email": "First.Middle.Last@seahavenind.com",
|
|
},
|
|
"ok",
|
|
),
|
|
({"name": "Bob Smith", "email": "Bob@seahaven.com"}, "ok"),
|
|
]
|
|
|
|
# Test 1: full email preserved
|
|
items = _make_items([(10.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
event = _submit_event(
|
|
items,
|
|
employee_name="First Middle Last",
|
|
employee_email="ignored@seahavenind.com",
|
|
)
|
|
lambda_handler(event, None)
|
|
slug_1 = mock_put.call_args[0][1]
|
|
assert slug_1 == "first.middle.last@seahavenind.com", (
|
|
f"Slug should be full lowercase email, got '{slug_1}'"
|
|
)
|
|
|
|
# Test 2: different domains produce different slugs (no collision)
|
|
mock_put.reset_mock()
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
event = _submit_event(
|
|
items, employee_name="Ignored", employee_email="ignored@seahavenind.com"
|
|
)
|
|
lambda_handler(event, None)
|
|
slug_2 = mock_put.call_args[0][1]
|
|
assert slug_2 == "bob@seahaven.com", (
|
|
f"Slug should be full lowercase email, got '{slug_2}'"
|
|
)
|
|
assert slug_1 != slug_2, "Different emails must produce different slugs"
|
|
|
|
|
|
# ===========================================================================
|
|
# FORM STATUS (Critical + High)
|
|
# ===========================================================================
|
|
|
|
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
def test_form_status_open(mock_week, mock_status):
|
|
"""Status 'open' — response has week and status, no reopen_at."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
event = _make_event(
|
|
method="GET", path="/form-status/2026-W20", path_parameters={"week": "2026-W20"}
|
|
)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
assert body["status"] == "open", f"Expected status='open', got '{body['status']}'"
|
|
assert body["week"] == "2026-W20", f"Expected week='2026-W20', got '{body['week']}'"
|
|
assert "reopen_at" not in body, "reopen_at should NOT be present when form is open"
|
|
|
|
|
|
@patch("submit_order_handler.get_form_status", return_value="closed")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
def test_form_status_closed_reopen_at(mock_week, mock_status):
|
|
"""Closed on Thursday -> reopen_at is next Monday 8am Eastern."""
|
|
from submit_order_handler import lambda_handler, EASTERN
|
|
|
|
# Freeze "now" to Thursday 2026-05-14 at 10:00 AM Eastern
|
|
thursday = datetime(2026, 5, 14, 10, 0, 0, tzinfo=EASTERN)
|
|
with patch("submit_order_handler._eastern_now", return_value=thursday):
|
|
event = _make_event(
|
|
method="GET",
|
|
path="/form-status/2026-W20",
|
|
path_parameters={"week": "2026-W20"},
|
|
)
|
|
result = lambda_handler(event, None)
|
|
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
assert body["status"] == "closed", (
|
|
f"Expected status='closed', got '{body['status']}'"
|
|
)
|
|
assert "reopen_at" in body, "reopen_at should be present when form is closed"
|
|
|
|
# Next Monday from Thursday 2026-05-14 is Monday 2026-05-18
|
|
expected_monday = datetime(2026, 5, 18, 8, 0, 0, tzinfo=EASTERN)
|
|
expected_ts = int(expected_monday.timestamp())
|
|
assert body["reopen_at"] == expected_ts, (
|
|
f"reopen_at should be {expected_ts} (Mon 2026-05-18 8am ET), got {body['reopen_at']}"
|
|
)
|
|
|
|
|
|
@patch("submit_order_handler.get_form_status", return_value="closed")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
def test_form_status_monday_before_8am(mock_week, mock_status):
|
|
"""Monday before 8am -> reopen_at is TODAY at 8am, not next Monday."""
|
|
from submit_order_handler import lambda_handler, EASTERN
|
|
|
|
# Monday 2026-05-18 at 6:30 AM Eastern (before 8am cutoff)
|
|
monday_early = datetime(2026, 5, 18, 6, 30, 0, tzinfo=EASTERN)
|
|
with patch("submit_order_handler._eastern_now", return_value=monday_early):
|
|
event = _make_event(
|
|
method="GET",
|
|
path="/form-status/2026-W20",
|
|
path_parameters={"week": "2026-W20"},
|
|
)
|
|
result = lambda_handler(event, None)
|
|
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
assert "reopen_at" in body, "reopen_at should be present when form is closed"
|
|
|
|
# days_until_monday = (7 - 0) % 7 = 0, and hour < 8, so days_until_monday stays 0
|
|
# -> reopen_at is TODAY (same Monday) at 8am
|
|
expected_today = datetime(2026, 5, 18, 8, 0, 0, tzinfo=EASTERN)
|
|
expected_ts = int(expected_today.timestamp())
|
|
assert body["reopen_at"] == expected_ts, (
|
|
f"reopen_at should be {expected_ts} (today Mon 2026-05-18 8am ET), got {body['reopen_at']}"
|
|
)
|
|
|
|
|
|
@patch("submit_order_handler.get_form_status", return_value="closed")
|
|
@patch("submit_order_handler.current_week", return_value="2025-W45")
|
|
def test_form_status_closed_saturday_before_dst_end(mock_week, mock_status):
|
|
"""Saturday before fall-back: reopen_at is the *calendar* Monday at 8am ET.
|
|
|
|
Using timedelta(days=n) for n weekdays is 24n hours and can land on the wrong
|
|
local calendar day when a 25-hour Sunday sits in between.
|
|
"""
|
|
from submit_order_handler import lambda_handler, EASTERN
|
|
|
|
# Nov 1 2025 is Saturday (DST still on until early Nov 2). Next Monday is Nov 3.
|
|
saturday = datetime(2025, 11, 1, 12, 0, 0, tzinfo=EASTERN)
|
|
with patch("submit_order_handler._eastern_now", return_value=saturday):
|
|
event = _make_event(
|
|
method="GET",
|
|
path="/form-status/2025-W45",
|
|
path_parameters={"week": "2025-W45"},
|
|
)
|
|
result = lambda_handler(event, None)
|
|
|
|
status, body = _parse_response(result)
|
|
assert status == 200
|
|
expected_monday = datetime(2025, 11, 3, 8, 0, 0, tzinfo=EASTERN)
|
|
expected_ts = int(expected_monday.timestamp())
|
|
assert body["reopen_at"] == expected_ts, (
|
|
f"reopen_at should be {expected_ts} (Mon 2025-11-03 8am ET), got {body['reopen_at']}"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# VALIDATION (High)
|
|
# ===========================================================================
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch(
|
|
"submit_order_handler._verify_google_token",
|
|
return_value=({"name": "", "email": "test@seahavenind.com"}, "ok"),
|
|
)
|
|
def test_submit_missing_name(
|
|
mock_verify, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam
|
|
):
|
|
"""A Google identity without a name -> 400."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
event = _submit_event(
|
|
items, employee_name="", employee_email="test@seahavenind.com"
|
|
)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 400, f"Expected 400 for missing name, got {status}: {body}"
|
|
assert "name" in body["error"].lower(), (
|
|
f"Error should mention name, got: {body['error']}"
|
|
)
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch(
|
|
"submit_order_handler._verify_google_token",
|
|
return_value=({"name": "Test User", "email": ""}, "ok"),
|
|
)
|
|
def test_submit_missing_email(
|
|
mock_verify, mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam
|
|
):
|
|
"""A Google identity without an email -> 400."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
event = _submit_event(items, employee_name="Test User", employee_email="")
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 400, f"Expected 400 for missing email, got {status}: {body}"
|
|
assert "email" in body["error"].lower(), (
|
|
f"Error should mention email, got: {body['error']}"
|
|
)
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
def test_submit_zero_quantity_only(
|
|
mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam
|
|
):
|
|
"""All items with quantity 0 -> 400."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 0), (15.00, 0)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 400, f"Expected 400 for zero-quantity items, got {status}: {body}"
|
|
assert "at least one meal" in body["error"].lower(), (
|
|
f"Error should mention 'at least one meal', got: {body['error']}"
|
|
)
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="closed")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
def test_submit_form_closed(
|
|
mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam
|
|
):
|
|
"""Form closed -> 410."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 410, f"Expected 410 for closed form, got {status}: {body}"
|
|
assert "closed" in body["error"].lower(), (
|
|
f"Error should mention 'closed', got: {body['error']}"
|
|
)
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="closed")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={
|
|
"bulk_discount_percent": 0,
|
|
"company_subsidy_percent": 0,
|
|
"admin_emails": ["adam@seahavenind.com"],
|
|
},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.urllib.request.urlopen")
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_admin_can_submit_when_form_closed(
|
|
mock_get_menu,
|
|
mock_gac,
|
|
mock_urlopen,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Admin user can submit even when form is closed."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(
|
|
{
|
|
"aud": VALID_GOOGLE_CLIENT_ID,
|
|
"hd": "seahavenind.com",
|
|
"name": "Adam Moussa",
|
|
"email": "adam@seahavenind.com",
|
|
}
|
|
).encode()
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
mock_urlopen.return_value = mock_resp
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
event = _submit_event(items, extra_body={"google_id_token": "admin-token"})
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Admin should bypass closed form, got {status}: {body}"
|
|
mock_put.assert_called_once()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="closed")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={
|
|
"bulk_discount_percent": 0,
|
|
"company_subsidy_percent": 0,
|
|
"admin_emails": ["adam@seahavenind.com"],
|
|
},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.urllib.request.urlopen")
|
|
@patch("submit_order_handler._google_auth_configured", return_value=True)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_non_admin_blocked_when_form_closed(
|
|
mock_get_menu,
|
|
mock_gac,
|
|
mock_urlopen,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Non-admin user still blocked when form is closed."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_resp = MagicMock()
|
|
mock_resp.read.return_value = json.dumps(
|
|
{
|
|
"aud": VALID_GOOGLE_CLIENT_ID,
|
|
"hd": "seahavenind.com",
|
|
"name": "Regular Employee",
|
|
"email": "employee@seahavenind.com",
|
|
}
|
|
).encode()
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
mock_urlopen.return_value = mock_resp
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
event = _submit_event(items, extra_body={"google_id_token": "user-token"})
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 410, f"Non-admin should be blocked, got {status}: {body}"
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="not_found")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
def test_submit_no_menu(
|
|
mock_gcid, mock_settings, mock_week, mock_status, mock_put, mock_lam
|
|
):
|
|
"""No menu available -> 404."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 404, f"Expected 404 for missing menu, got {status}: {body}"
|
|
assert "no menu" in body["error"].lower(), (
|
|
f"Error should mention 'no menu', got: {body['error']}"
|
|
)
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_unknown_meal_returns_400(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Line items must match menu meal names (reject client-injected SKUs)."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
event = _submit_event(
|
|
[{"name": "Totally Fake Meal", "retail_price": 1.0, "quantity": 1}]
|
|
)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 400, f"Expected 400, got {status}: {body}"
|
|
assert "not on this week's menu" in body["error"].lower(), body["error"]
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_retail_price_from_menu_not_request_body(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Billing uses Dynamo menu retail, not client-supplied retail_price."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_get_menu.return_value = {"meals": [{"name": "Meal 1", "price": 100.0}]}
|
|
items = [{"name": "Meal 1", "retail_price": 0.01, "quantity": 1}]
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
saved = mock_put.call_args[0][2]
|
|
assert saved["items"][0]["retail_price"] == 100.0, saved["items"][0]
|
|
assert saved["total"] == 100.0, saved["total"]
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_menu_missing_priced_meals_returns_503(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_get_menu.return_value = {"meals": []}
|
|
items = _make_items([(10.00, 1)])
|
|
result = lambda_handler(_submit_event(items), None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 503, f"Expected 503, got {status}: {body}"
|
|
assert "unavailable" in body["error"].lower(), body["error"]
|
|
mock_put.assert_not_called()
|
|
|
|
|
|
# ===========================================================================
|
|
# RELIABILITY (High)
|
|
# ===========================================================================
|
|
|
|
|
|
@patch("submit_order_handler._lambda")
|
|
@patch("submit_order_handler.put_order")
|
|
@patch("submit_order_handler.get_form_status", return_value="open")
|
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
|
@patch(
|
|
"submit_order_handler.get_settings",
|
|
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
|
)
|
|
@patch(
|
|
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
|
)
|
|
@patch("submit_order_handler.get_menu")
|
|
def test_slack_failure_does_not_fail_order(
|
|
mock_get_menu,
|
|
mock_gcid,
|
|
mock_settings,
|
|
mock_week,
|
|
mock_status,
|
|
mock_put,
|
|
mock_lam,
|
|
):
|
|
"""Lambda invoke for Slack notification raises, order still saved, returns 200."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_lam.invoke.side_effect = Exception("Lambda invoke failed: connection timeout")
|
|
|
|
items = _make_items([(10.00, 1)])
|
|
mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
|
|
event = _submit_event(items)
|
|
result = lambda_handler(event, None)
|
|
status, body = _parse_response(result)
|
|
|
|
assert status == 200, f"Expected 200 despite Slack failure, got {status}: {body}"
|
|
assert body["status"] == "ok", f"Expected status='ok', got '{body['status']}'"
|
|
mock_put.assert_called_once()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Admin summary-PDF endpoint
|
|
# ---------------------------------------------------------------------------
|
|
def _pdf_event(week=None):
|
|
qs = {"week": week} if week else {}
|
|
return {
|
|
"rawPath": "/api/admin/summary-pdf",
|
|
"requestContext": {"http": {"method": "GET"}},
|
|
"queryStringParameters": qs,
|
|
"headers": {"authorization": "Bearer admin-token"},
|
|
}
|
|
|
|
|
|
@patch.dict(os.environ, {"REPORTS_BUCKET": "test-reports-bucket"})
|
|
@patch("submit_order_handler._s3")
|
|
@patch("submit_order_handler.get_summary")
|
|
@patch(
|
|
"submit_order_handler._verify_admin",
|
|
return_value=({"email": "adam@seahavenind.com"}, None),
|
|
)
|
|
def test_admin_summary_pdf_returns_presigned_url(
|
|
mock_verify, mock_get_summary, mock_s3
|
|
):
|
|
"""Closed week with a stamped PDF key returns a presigned URL."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_get_summary.return_value = {
|
|
"weekly_summary_pdf_s3_key": "reports/2026-W22/weekly-summary-2026-W22.pdf"
|
|
}
|
|
mock_s3.generate_presigned_url.return_value = "https://signed.example/pdf"
|
|
|
|
status, body = _parse_response(lambda_handler(_pdf_event("2026-W22"), None))
|
|
|
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
|
assert body["url"] == "https://signed.example/pdf"
|
|
assert body["week"] == "2026-W22"
|
|
mock_s3.generate_presigned_url.assert_called_once_with(
|
|
"get_object",
|
|
Params={
|
|
"Bucket": "test-reports-bucket",
|
|
"Key": "reports/2026-W22/weekly-summary-2026-W22.pdf",
|
|
},
|
|
ExpiresIn=300,
|
|
)
|
|
|
|
|
|
@patch("submit_order_handler.get_summary", return_value=None)
|
|
@patch(
|
|
"submit_order_handler._verify_admin",
|
|
return_value=({"email": "adam@seahavenind.com"}, None),
|
|
)
|
|
def test_admin_summary_pdf_404_when_week_not_closed(mock_verify, mock_get_summary):
|
|
"""No SUMMARY item (week still open) returns 404."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
status, body = _parse_response(lambda_handler(_pdf_event("2026-W23"), None))
|
|
|
|
assert status == 404, f"Expected 404, got {status}: {body}"
|
|
assert "no summary pdf" in body["error"].lower()
|
|
|
|
|
|
@patch(
|
|
"submit_order_handler._verify_admin",
|
|
return_value=({"email": "adam@seahavenind.com"}, None),
|
|
)
|
|
def test_admin_summary_pdf_400_without_week(mock_verify):
|
|
"""Missing week query param returns 400."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
status, body = _parse_response(lambda_handler(_pdf_event(), None))
|
|
|
|
assert status == 400, f"Expected 400, got {status}: {body}"
|
|
|
|
|
|
@patch(
|
|
"submit_order_handler._verify_admin",
|
|
return_value=({"email": "adam@seahavenind.com"}, None),
|
|
)
|
|
def test_admin_summary_pdf_400_malformed_week(mock_verify):
|
|
"""Week not matching YYYY-WNN returns 400 before any lookup."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
status, body = _parse_response(lambda_handler(_pdf_event("../../etc"), None))
|
|
|
|
assert status == 400, f"Expected 400, got {status}: {body}"
|
|
|
|
|
|
@patch("submit_order_handler._s3")
|
|
@patch("submit_order_handler.get_summary")
|
|
@patch(
|
|
"submit_order_handler._verify_admin",
|
|
return_value=({"email": "adam@seahavenind.com"}, None),
|
|
)
|
|
def test_admin_summary_pdf_rejects_tampered_key(mock_verify, mock_get_summary, mock_s3):
|
|
"""A PDF key outside the expected shape is never presigned (500, no URL)."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
mock_get_summary.return_value = {
|
|
"weekly_summary_pdf_s3_key": "reports/2026-W22/payroll-deductions.csv"
|
|
}
|
|
|
|
status, body = _parse_response(lambda_handler(_pdf_event("2026-W22"), None))
|
|
|
|
assert status == 500, f"Expected 500 for tampered key, got {status}: {body}"
|
|
mock_s3.generate_presigned_url.assert_not_called()
|
|
|
|
|
|
@patch(
|
|
"submit_order_handler._verify_admin",
|
|
return_value=(None, submit_order_handler.response(401, {"error": "Missing token"})),
|
|
)
|
|
def test_admin_summary_pdf_requires_admin(mock_verify):
|
|
"""Auth failure from _verify_admin is returned as-is."""
|
|
from submit_order_handler import lambda_handler
|
|
|
|
status, body = _parse_response(lambda_handler(_pdf_event("2026-W22"), None))
|
|
|
|
assert status == 401, f"Expected 401, got {status}: {body}"
|