meal-order-manager/tests/test_submit_order.py

2023 lines
67 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 decimal import Decimal
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()
# ===========================================================================
# PUBLIC MENU
# ===========================================================================
@patch(
"submit_order_handler.get_settings",
return_value={
"bulk_discount_percent": Decimal("10"),
"company_subsidy_percent": Decimal("50"),
"order_deadline": "Thursday at 11:59 PM",
},
)
@patch("submit_order_handler.get_menu")
@patch("submit_order_handler.current_week", return_value="2026-W37")
def test_menu_current_returns_portal_payload(mock_week, mock_get_menu, mock_settings):
mock_get_menu.return_value = {
"form_status": "open",
"scraped_at": "2026-09-14T07:30:00-04:00",
"menu_url": "https://example.com/menu",
"meal_count": Decimal("1"),
"meals": [{"name": "Chicken Bowl", "price": Decimal("12.50")}],
}
event = _make_event(
method="GET",
path="/api/menu/current",
path_parameters={"week": "current"},
)
status, body = _parse_response(submit_order_handler.lambda_handler(event, None))
assert status == 200
assert body == {
"week": "2026-W37",
"form_status": "open",
"scraped_at": "2026-09-14T07:30:00-04:00",
"menu_url": "https://example.com/menu",
"meal_count": 1,
"meals": [{"name": "Chicken Bowl", "price": 12.5}],
"bulk_discount_percent": 10.0,
"company_subsidy_percent": 50.0,
"order_deadline": "Thursday at 11:59 PM",
}
mock_get_menu.assert_called_once_with("2026-W37")
@patch(
"submit_order_handler.get_settings",
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
)
@patch("submit_order_handler.get_menu")
def test_menu_explicit_week_uses_safe_defaults(mock_get_menu, mock_settings):
mock_get_menu.return_value = {"meals": [{"name": "Soup", "price": 8}]}
event = _make_event(
method="GET",
path="/api/menu/2026-W36",
path_parameters={"week": "2026-W36"},
)
status, body = _parse_response(submit_order_handler.lambda_handler(event, None))
assert status == 200
assert body["week"] == "2026-W36"
assert body["form_status"] == "closed"
assert body["meal_count"] == 1
assert body["order_deadline"] == "Thursday at 11:59 PM"
mock_get_menu.assert_called_once_with("2026-W36")
@patch("submit_order_handler.get_settings")
@patch("submit_order_handler.get_menu", return_value=None)
def test_menu_missing_returns_404(mock_get_menu, mock_settings):
event = _make_event(
method="GET",
path="/api/menu/2026-W35",
path_parameters={"week": "2026-W35"},
)
status, body = _parse_response(submit_order_handler.lambda_handler(event, None))
assert status == 404
assert body == {"error": "No menu available for this week"}
mock_settings.assert_not_called()
@patch("submit_order_handler.get_menu")
def test_menu_rejects_malformed_week_without_lookup(mock_get_menu):
event = _make_event(
method="GET",
path="/api/menu/not-a-week",
path_parameters={"week": "not-a-week"},
)
status, body = _parse_response(submit_order_handler.lambda_handler(event, None))
assert status == 400
assert "current or YYYY-WNN" in body["error"]
mock_get_menu.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_google_client_id")
@patch(
"submit_order_handler._verify_portal_token",
return_value={
"name": "Portal Employee",
"email": "portal.employee@seahavenind.com",
},
)
@patch("submit_order_handler.get_menu")
def test_portal_token_identity_takes_precedence(
mock_get_menu,
mock_portal,
mock_google_client_id,
mock_settings,
mock_week,
mock_status,
mock_put,
mock_lambda,
):
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="Body Name",
employee_email="body@example.com",
extra_body={"google_id_token": "body-google-token"},
)
event["headers"] = {"authorization": "Bearer portal-id-token"}
status, body = _parse_response(submit_order_handler.lambda_handler(event, None))
assert status == 200, body
saved_order = mock_put.call_args.args[2]
assert saved_order["employee_name"] == "Portal Employee"
assert saved_order["employee_email"] == "portal.employee@seahavenind.com"
mock_portal.assert_called_once_with("portal-id-token")
mock_google_client_id.assert_not_called()
@patch("submit_order_handler.put_order")
@patch("submit_order_handler._get_google_client_id")
@patch("submit_order_handler._verify_portal_token", return_value=None)
def test_invalid_portal_token_never_falls_back_to_google_or_body(
mock_portal, mock_google_client_id, mock_put
):
event = _submit_event(
_make_items([(10.00, 1)]),
employee_name="Body Name",
employee_email="body@seahavenind.com",
extra_body={"google_id_token": "valid-google-token"},
)
event["headers"] = {"Authorization": "Bearer invalid-portal-token"}
status, body = _parse_response(submit_order_handler.lambda_handler(event, None))
assert status == 403
assert body == {"error": "Invalid or unauthorized portal account"}
mock_portal.assert_called_once_with("invalid-portal-token")
mock_google_client_id.assert_not_called()
mock_put.assert_not_called()
@patch("submit_order_handler.put_order")
@patch(
"submit_order_handler._verify_portal_token",
side_effect=submit_order_handler.CognitoVerificationUnavailable,
)
def test_portal_verification_outage_returns_503(mock_portal, mock_put):
event = _submit_event(_make_items([(10.00, 1)]))
event["headers"] = {"authorization": "Bearer portal-id-token"}
status, body = _parse_response(submit_order_handler.lambda_handler(event, None))
assert status == 503
assert body == {"error": "Authentication service temporarily unavailable"}
mock_put.assert_not_called()
@patch(
"submit_order_handler.get_settings",
return_value={"admin_emails": ["portal.admin@seahaven.com"]},
)
@patch(
"submit_order_handler._verify_portal_token",
return_value={"name": "Portal Admin", "email": "portal.admin@seahaven.com"},
)
@patch("submit_order_handler.looks_like_cognito_token", return_value=True)
def test_in_handler_admin_check_accepts_portal_token(
mock_looks_like, mock_portal, mock_settings
):
event = _make_event(
method="GET",
path="/api/admin/orders",
headers={"authorization": "Bearer portal-id-token"},
)
user, error = submit_order_handler._verify_admin(event)
assert error is None
assert user == {"name": "Portal Admin", "email": "portal.admin@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_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}"