meal-order-manager/tests/test_submit_order.py
Cursor Agent 6bc5ccaedd
fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:32:08 +00:00

1172 lines
42 KiB
Python

"""Unit tests for functions/submit_order/handler.py
All external dependencies (DynamoDB, SSM, Secrets Manager, 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("FORM_API_KEY_SECRET", "test/form-api-key")
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")
TEST_API_KEY = "test-api-key-12345"
VALID_GOOGLE_CLIENT_ID = "123456789.apps.googleusercontent.com"
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,
api_key=TEST_API_KEY,
employee_name="Test User",
employee_email="test.user@seahavenind.com",
extra_body=None,
):
"""Shortcut for a typical POST /submit event with items."""
body = {
"employee_name": employee_name,
"employee_email": employee_email,
"items": items,
}
if extra_body:
body.update(extra_body)
return _make_event(
method="POST",
path="/submit",
body=body,
headers={"x-api-key": api_key},
)
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
# ---------------------------------------------------------------------------
# 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 _api_key, _settings, _google_client_id.
@pytest.fixture(autouse=True)
def _reset_handler_caches():
"""Reset handler module-level caches before each test."""
submit_order_handler._api_key = None
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
# ===========================================================================
# 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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_discount_two_step_rounding(
mock_gcid, mock_secret, 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)])
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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_discount_rounding_half_up_boundary(
mock_gcid, mock_secret, 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)])
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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_discount_clamping(
mock_gcid, mock_secret, 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)])
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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_discount_both_zero(
mock_gcid, mock_secret, 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)])
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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_total_summation_multiple_items(
mock_gcid, mock_secret, 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)])
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_secret", return_value=TEST_API_KEY)
@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_secret,
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)
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_secret", return_value=TEST_API_KEY)
@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_secret,
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,
headers={"x-api-key": TEST_API_KEY},
)
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_secret", return_value=TEST_API_KEY)
@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_secret,
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_secret", return_value=TEST_API_KEY)
@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_secret,
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_secret", return_value=TEST_API_KEY)
@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_secret,
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_secret", return_value=TEST_API_KEY)
@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_secret,
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_secret", return_value=TEST_API_KEY)
@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_secret,
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_secret", return_value=TEST_API_KEY)
@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_valid_success(
mock_gac,
mock_urlopen,
mock_gcid,
mock_secret,
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)])
# 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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_manual_fallback_when_google_not_configured(
mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam
):
"""No Google client ID configured -> manual name/email accepted and order saved."""
from submit_order_handler import lambda_handler
items = _make_items([(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 == 200, f"Expected 200, got {status}: {body}"
saved_order = mock_put.call_args[0][2]
assert saved_order["employee_name"] == "Manual User", (
f"Name should be from manual input, got: {saved_order['employee_name']}"
)
assert saved_order["employee_email"] == "manual@seahavenind.com", (
f"Email should be from manual input, 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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_submit_invalid_api_key(
mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam
):
"""Wrong x-api-key -> 403."""
from submit_order_handler import lambda_handler
items = _make_items([(10.00, 1)])
event = _submit_event(items, api_key="wrong-api-key")
result = lambda_handler(event, None)
status, body = _parse_response(result)
assert status == 403, f"Expected 403 for invalid API key, got {status}: {body}"
assert "Invalid API key" in body["error"], (
f"Expected 'Invalid API key' in error, got: {body['error']}"
)
mock_put.assert_not_called()
# ===========================================================================
# 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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_slug_from_email(
mock_gcid, mock_secret, 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)])
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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_slug_edge_cases(
mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam
):
"""Slug uses full lowercase email, preserving uniqueness across domains."""
from submit_order_handler import lambda_handler
# Test 1: full email preserved
items = _make_items([(10.00, 1)])
event = _submit_event(
items,
employee_name="First Middle Last",
employee_email="First.Middle.Last@x.com",
)
lambda_handler(event, None)
slug_1 = mock_put.call_args[0][1]
assert slug_1 == "first.middle.last@x.com", (
f"Slug should be full lowercase email, got '{slug_1}'"
)
# Test 2: different domains produce different slugs (no collision)
mock_put.reset_mock()
event = _submit_event(
items, employee_name="Bob Smith", employee_email="Bob@other.com"
)
lambda_handler(event, None)
slug_2 = mock_put.call_args[0][1]
assert slug_2 == "bob@other.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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_submit_missing_name(
mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam
):
"""Empty employee 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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_submit_missing_email(
mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam
):
"""Empty employee 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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_submit_zero_quantity_only(
mock_gcid, mock_secret, 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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_submit_form_closed(
mock_gcid, mock_secret, 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="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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_submit_no_menu(
mock_gcid, mock_secret, 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()
# ===========================================================================
# 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_secret", return_value=TEST_API_KEY)
@patch("submit_order_handler._get_google_client_id", return_value="")
def test_slack_failure_does_not_fail_order(
mock_gcid, mock_secret, 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)])
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()