mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 17:03:13 +00:00
* feat(api): serve meals on ECS Fargate instead of Lambda Keep the Flask app always-on with in-process jobs so CloudFront no longer fronts a cold-start API Gateway. * fix(jobs): run delayed close and reminder deliveries Wall-clock skip windows dropped the only weekly SQS attempt when Scheduler already fired in Eastern time. Dev schedules stay disabled. * fix(api): return JSON objects and stop logging job payloads Flask now jsonify-s handler dicts so API responses are not HTML, and the worker logs only event and status. * fix(ci): restore the reusable workflow so the required check is named ci / ci Inlining the job reported `ci` instead of the org ruleset's `ci / ci`. * fix(secrets): drop unused os import so ruff check passes * style: apply ruff format so ci-python-app lint passes * fix(infra): give meals its own VPC because prod has none * chore(security): re-key ALB SG checkov suppression after vpc.tf
613 lines
22 KiB
Python
613 lines
22 KiB
Python
"""Unit tests for functions/aggregate_orders/handler.py."""
|
|
|
|
import csv
|
|
import io
|
|
import json
|
|
import os
|
|
from decimal import Decimal
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers — reusable order builders
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_order(
|
|
name: str, email: str, items: list[dict], total: float | None = None
|
|
) -> dict:
|
|
"""Build a minimal order dict matching DynamoDB shape."""
|
|
if total is None:
|
|
total = sum(
|
|
float(
|
|
i.get("subtotal", float(i.get("price", 0)) * int(i.get("quantity", 0)))
|
|
)
|
|
for i in items
|
|
)
|
|
return {
|
|
"employee_name": name,
|
|
"employee_email": email,
|
|
"items": items,
|
|
"total": Decimal(str(total)),
|
|
}
|
|
|
|
|
|
def _make_item(
|
|
name: str,
|
|
quantity: int = 1,
|
|
price: float = 10.0,
|
|
bulk_price: float | None = None,
|
|
subtotal: float | None = None,
|
|
) -> dict:
|
|
"""Build a minimal item dict."""
|
|
item = {"name": name, "quantity": quantity, "price": price}
|
|
if bulk_price is not None:
|
|
item["bulk_price"] = bulk_price
|
|
if subtotal is not None:
|
|
item["subtotal"] = subtotal
|
|
return item
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Import handler AFTER patching boto3 + env vars so module-level clients
|
|
# don't try to hit real AWS.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _patch_env(monkeypatch):
|
|
monkeypatch.setenv("TABLE_NAME", "test-table")
|
|
monkeypatch.setenv("REPORTS_BUCKET", "test-bucket")
|
|
monkeypatch.setenv(
|
|
"SLACK_NOTIFIER_ARN",
|
|
"arn:aws:lambda:us-east-1:123456789012:function:test-notifier",
|
|
)
|
|
monkeypatch.setenv(
|
|
"CHECKCOMPONENTS_QUEUE_URL",
|
|
"https://sqs.us-east-1.amazonaws.com/011934824531/paychex-checkcomponents",
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def handler_module():
|
|
"""Import the handler with boto3 patched at module level."""
|
|
with patch("boto3.client") as mock_client, patch("boto3.resource"):
|
|
mock_s3 = MagicMock()
|
|
mock_sqs = MagicMock()
|
|
mock_client.side_effect = lambda svc, **kw: {
|
|
"s3": mock_s3,
|
|
"sqs": mock_sqs,
|
|
}.get(svc, MagicMock())
|
|
|
|
import importlib
|
|
import server.jobs.aggregate as mod
|
|
|
|
importlib.reload(mod)
|
|
|
|
# Inject mocked clients so tests can assert on them
|
|
mod._s3 = mock_s3
|
|
mod._sqs = mock_sqs
|
|
mod._dispatch_job = MagicMock()
|
|
yield mod
|
|
|
|
|
|
# ===================================================================
|
|
# Summary Building (Critical + High)
|
|
# ===================================================================
|
|
|
|
|
|
class TestBuildSummarySingleOrder:
|
|
"""test_build_summary_single_order — one order with 2 items."""
|
|
|
|
def test_build_summary_single_order(self, handler_module):
|
|
orders = [
|
|
_make_order(
|
|
"Alice Smith",
|
|
"alice@example.com",
|
|
[
|
|
_make_item(
|
|
"Chicken Parm", quantity=1, price=12.00, bulk_price=10.00
|
|
),
|
|
_make_item("Caesar Salad", quantity=1, price=8.00, bulk_price=6.50),
|
|
],
|
|
),
|
|
]
|
|
summary = handler_module.build_summary(orders, "2026-W20")
|
|
|
|
assert summary["week"] == "2026-W20", "Week should be passed through"
|
|
assert summary["total_employees"] == 1, "Should count 1 employee"
|
|
assert summary["total_meals"] == 2, "Should count 2 total meals"
|
|
|
|
meals_by_name = {m["meal"]: m for m in summary["meals"]}
|
|
assert "Chicken Parm" in meals_by_name, "Chicken Parm should appear"
|
|
assert "Caesar Salad" in meals_by_name, "Caesar Salad should appear"
|
|
|
|
chicken = meals_by_name["Chicken Parm"]
|
|
assert chicken["quantity"] == 1
|
|
assert chicken["unit_price"] == 10.00, "unit_price should use bulk_price"
|
|
assert chicken["employee_unit_price"] == 12.00, (
|
|
"employee_unit_price should use price"
|
|
)
|
|
assert chicken["line_total"] == 10.00, "line_total = bulk_price * qty"
|
|
assert chicken["employee_line_total"] == 12.00, (
|
|
"employee_line_total = price * qty"
|
|
)
|
|
|
|
assert summary["grand_total"] == 16.50, (
|
|
"grand_total should sum bulk line totals"
|
|
)
|
|
assert summary["employee_total"] == 20.00, (
|
|
"employee_total should sum employee line totals"
|
|
)
|
|
|
|
|
|
class TestBuildSummaryMultipleOrdersSameMeal:
|
|
"""test_build_summary_multiple_orders_same_meal — 3 employees order the same meal."""
|
|
|
|
def test_build_summary_multiple_orders_same_meal(self, handler_module):
|
|
orders = [
|
|
_make_order(
|
|
"Alice",
|
|
"a@x.com",
|
|
[_make_item("Burger", quantity=1, price=10.00, bulk_price=8.00)],
|
|
),
|
|
_make_order(
|
|
"Bob",
|
|
"b@x.com",
|
|
[_make_item("Burger", quantity=2, price=10.00, bulk_price=8.00)],
|
|
),
|
|
_make_order(
|
|
"Carol",
|
|
"c@x.com",
|
|
[_make_item("Burger", quantity=1, price=10.00, bulk_price=8.00)],
|
|
),
|
|
]
|
|
summary = handler_module.build_summary(orders, "2026-W20")
|
|
|
|
assert len(summary["meals"]) == 1, (
|
|
"All three ordered the same meal — should aggregate to 1 entry"
|
|
)
|
|
burger = summary["meals"][0]
|
|
assert burger["quantity"] == 4, "Total quantity should be 1 + 2 + 1 = 4"
|
|
assert burger["line_total"] == 32.00, "line_total = 8.00 * 4"
|
|
assert burger["employee_line_total"] == 40.00, "employee_line_total = 10.00 * 4"
|
|
|
|
|
|
class TestBuildSummarySortedAlphabetically:
|
|
"""test_build_summary_sorted_alphabetically — meals appear in alphabetical order."""
|
|
|
|
def test_build_summary_sorted_alphabetically(self, handler_module):
|
|
orders = [
|
|
_make_order(
|
|
"Alice",
|
|
"a@x.com",
|
|
[
|
|
_make_item("Ziti", quantity=1, price=10.00),
|
|
_make_item("Apple Pie", quantity=1, price=5.00),
|
|
_make_item("Meatloaf", quantity=1, price=12.00),
|
|
],
|
|
),
|
|
]
|
|
summary = handler_module.build_summary(orders, "2026-W20")
|
|
|
|
meal_names = [m["meal"] for m in summary["meals"]]
|
|
assert meal_names == ["Apple Pie", "Meatloaf", "Ziti"], (
|
|
"Meals should be sorted alphabetically"
|
|
)
|
|
|
|
|
|
class TestBuildSummaryGrandTotalVsEmployeeTotal:
|
|
"""test_build_summary_grand_total_vs_employee_total — grand_total uses bulk_price, employee_total uses price."""
|
|
|
|
def test_build_summary_grand_total_vs_employee_total(self, handler_module):
|
|
orders = [
|
|
_make_order(
|
|
"Alice",
|
|
"a@x.com",
|
|
[
|
|
_make_item("Steak", quantity=2, price=15.00, bulk_price=11.00),
|
|
],
|
|
),
|
|
_make_order(
|
|
"Bob",
|
|
"b@x.com",
|
|
[
|
|
_make_item("Pasta", quantity=1, price=9.00, bulk_price=7.00),
|
|
],
|
|
),
|
|
]
|
|
summary = handler_module.build_summary(orders, "2026-W20")
|
|
|
|
# Steak: bulk 11*2=22, employee 15*2=30
|
|
# Pasta: bulk 7*1=7, employee 9*1=9
|
|
assert summary["grand_total"] == 29.00, (
|
|
"grand_total should use bulk_price (22 + 7)"
|
|
)
|
|
assert summary["employee_total"] == 39.00, (
|
|
"employee_total should use employee_price (30 + 9)"
|
|
)
|
|
assert summary["grand_total"] != summary["employee_total"], (
|
|
"grand_total and employee_total must differ when bulk != employee price"
|
|
)
|
|
|
|
|
|
class TestBuildSummaryRounding:
|
|
"""test_build_summary_rounding — totals rounded to 2 decimal places."""
|
|
|
|
def test_build_summary_rounding(self, handler_module):
|
|
# Use prices that produce repeating decimals when multiplied
|
|
orders = [
|
|
_make_order(
|
|
"Alice",
|
|
"a@x.com",
|
|
[
|
|
_make_item("Soup", quantity=3, price=3.33, bulk_price=2.77),
|
|
],
|
|
),
|
|
]
|
|
summary = handler_module.build_summary(orders, "2026-W20")
|
|
|
|
soup = summary["meals"][0]
|
|
# 2.77 * 3 = 8.31 (rounded)
|
|
assert soup["line_total"] == 8.31, "line_total should be rounded to 2 decimals"
|
|
# 3.33 * 3 = 9.99
|
|
assert soup["employee_line_total"] == 9.99, (
|
|
"employee_line_total should be rounded to 2 decimals"
|
|
)
|
|
assert summary["grand_total"] == 8.31
|
|
assert summary["employee_total"] == 9.99
|
|
|
|
# Verify they are actually rounded (no extra decimal digits)
|
|
assert summary["grand_total"] == round(summary["grand_total"], 2)
|
|
assert summary["employee_total"] == round(summary["employee_total"], 2)
|
|
|
|
|
|
# ===================================================================
|
|
# CSV Generation (High)
|
|
# ===================================================================
|
|
|
|
|
|
class TestBuildOrderSummaryCsv:
|
|
"""test_build_order_summary_csv — correct header, meal rows, total row format."""
|
|
|
|
def test_build_order_summary_csv(self, handler_module):
|
|
orders = [
|
|
_make_order(
|
|
"Alice",
|
|
"a@x.com",
|
|
[
|
|
_make_item("Burger", quantity=2, price=10.00, bulk_price=8.00),
|
|
_make_item("Fries", quantity=1, price=5.00, bulk_price=4.00),
|
|
],
|
|
),
|
|
]
|
|
summary = handler_module.build_summary(orders, "2026-W20")
|
|
csv_str = handler_module.build_order_summary_csv(summary)
|
|
|
|
reader = csv.reader(io.StringIO(csv_str))
|
|
rows = list(reader)
|
|
|
|
# Header
|
|
assert rows[0] == ["Meal", "Quantity", "Unit Price", "Line Total"], (
|
|
"First row should be the header"
|
|
)
|
|
|
|
# Meal rows (alphabetical: Burger, Fries)
|
|
assert rows[1][0] == "Burger"
|
|
assert rows[1][1] == "2"
|
|
assert rows[1][2] == "$8.00"
|
|
assert rows[1][3] == "$16.00"
|
|
|
|
assert rows[2][0] == "Fries"
|
|
assert rows[2][1] == "1"
|
|
assert rows[2][2] == "$4.00"
|
|
assert rows[2][3] == "$4.00"
|
|
|
|
# Empty separator row then total row
|
|
assert rows[3] == [], "Separator should be an empty row"
|
|
assert rows[4][0] == "TOTAL"
|
|
assert rows[4][1] == "3", "Total quantity should be 3"
|
|
assert rows[4][3] == "$20.00", "Total should be grand_total"
|
|
|
|
|
|
class TestBuildPayrollCsv:
|
|
"""test_build_payroll_csv — correct header, sorted employees, item format, total deduction."""
|
|
|
|
def test_build_payroll_csv(self, handler_module):
|
|
orders = [
|
|
_make_order(
|
|
"Zara Adams",
|
|
"zara@x.com",
|
|
[
|
|
_make_item("Pasta", quantity=2, price=9.00, subtotal=18.00),
|
|
],
|
|
total=18.00,
|
|
),
|
|
_make_order(
|
|
"Alice Brown",
|
|
"alice@x.com",
|
|
[
|
|
_make_item("Burger", quantity=1, price=10.00, subtotal=10.00),
|
|
_make_item("Fries", quantity=2, price=5.00, subtotal=10.00),
|
|
],
|
|
total=20.00,
|
|
),
|
|
]
|
|
csv_str = handler_module.build_payroll_csv(orders)
|
|
|
|
reader = csv.reader(io.StringIO(csv_str))
|
|
rows = list(reader)
|
|
|
|
# Header
|
|
assert rows[0] == [
|
|
"Employee Name",
|
|
"Employee Email",
|
|
"Items Ordered",
|
|
"Total Deduction",
|
|
]
|
|
|
|
# Sorted alphabetically by employee_name: Alice Brown before Zara Adams
|
|
assert rows[1][0] == "Alice Brown", "Employees should be sorted by name"
|
|
assert rows[1][1] == "alice@x.com"
|
|
assert "Burger x1 ($10.00)" in rows[1][2]
|
|
assert "Fries x2 ($10.00)" in rows[1][2]
|
|
assert "; " in rows[1][2], "Items should be separated by '; '"
|
|
assert rows[1][3] == "$20.00"
|
|
|
|
assert rows[2][0] == "Zara Adams"
|
|
assert rows[2][1] == "zara@x.com"
|
|
assert "Pasta x2 ($18.00)" in rows[2][2]
|
|
assert rows[2][3] == "$18.00"
|
|
|
|
|
|
class TestBuildPayrollCsvSubtotalFallback:
|
|
"""test_build_payroll_csv_subtotal_fallback — when item lacks 'subtotal', falls back to price*quantity."""
|
|
|
|
def test_build_payroll_csv_subtotal_fallback(self, handler_module):
|
|
orders = [
|
|
_make_order(
|
|
"Alice Brown",
|
|
"alice@x.com",
|
|
[
|
|
_make_item("Burger", quantity=3, price=10.00), # no subtotal key
|
|
],
|
|
total=30.00,
|
|
),
|
|
]
|
|
csv_str = handler_module.build_payroll_csv(orders)
|
|
|
|
reader = csv.reader(io.StringIO(csv_str))
|
|
rows = list(reader)
|
|
|
|
# price(10) * quantity(3) = 30.00
|
|
assert "Burger x3 ($30.00)" in rows[1][2], (
|
|
"Without 'subtotal' key, should fall back to price * quantity"
|
|
)
|
|
|
|
|
|
# ===================================================================
|
|
# Lambda Handler Flow (High)
|
|
# ===================================================================
|
|
|
|
|
|
class TestAggregateAlreadyAggregated:
|
|
"""test_aggregate_already_aggregated — summary exists, returns early, no S3 upload."""
|
|
|
|
@patch("server.jobs.aggregate.get_summary")
|
|
@patch("server.jobs.aggregate.get_orders")
|
|
@patch("server.jobs.aggregate.current_week", return_value="2026-W20")
|
|
def test_aggregate_already_aggregated(
|
|
self, mock_week, mock_orders, mock_summary, handler_module
|
|
):
|
|
mock_summary.return_value = {"week": "2026-W20", "meals": []}
|
|
|
|
result = handler_module.lambda_handler({}, None)
|
|
|
|
assert result["status"] == "already_aggregated", (
|
|
"Should return already_aggregated status"
|
|
)
|
|
assert result["week"] == "2026-W20"
|
|
handler_module._s3.put_object.assert_not_called()
|
|
mock_orders.assert_not_called()
|
|
handler_module._sqs.send_message.assert_not_called()
|
|
|
|
|
|
class TestAggregateNoOrders:
|
|
"""test_aggregate_no_orders — no orders returns no_orders status."""
|
|
|
|
@patch("server.jobs.aggregate.get_summary")
|
|
@patch("server.jobs.aggregate.get_orders")
|
|
@patch("server.jobs.aggregate.current_week", return_value="2026-W20")
|
|
def test_aggregate_no_orders(
|
|
self, mock_week, mock_orders, mock_summary, handler_module
|
|
):
|
|
mock_summary.return_value = None
|
|
mock_orders.return_value = []
|
|
|
|
result = handler_module.lambda_handler({}, None)
|
|
|
|
assert result["status"] == "no_orders", (
|
|
"Should return no_orders when order list is empty"
|
|
)
|
|
assert result["week"] == "2026-W20"
|
|
handler_module._s3.put_object.assert_not_called()
|
|
handler_module._sqs.send_message.assert_not_called()
|
|
|
|
|
|
class TestAggregateHappyPath:
|
|
"""test_aggregate_happy_path — orders exist, builds summary, uploads CSVs, saves, triggers Slack."""
|
|
|
|
@patch("server.jobs.aggregate.put_summary")
|
|
@patch("server.jobs.aggregate.get_summary")
|
|
@patch("server.jobs.aggregate.get_orders")
|
|
@patch("server.jobs.aggregate.current_week", return_value="2026-W20")
|
|
def test_aggregate_happy_path(
|
|
self, mock_week, mock_orders, mock_get_summary, mock_put_summary, handler_module
|
|
):
|
|
mock_get_summary.return_value = None
|
|
mock_orders.return_value = [
|
|
_make_order(
|
|
"Alice",
|
|
"alice@x.com",
|
|
[
|
|
_make_item(
|
|
"Burger",
|
|
quantity=1,
|
|
price=10.00,
|
|
bulk_price=8.00,
|
|
subtotal=10.00,
|
|
),
|
|
],
|
|
total=10.00,
|
|
),
|
|
_make_order(
|
|
"Bob",
|
|
"bob@x.com",
|
|
[
|
|
_make_item(
|
|
"Pasta", quantity=2, price=9.00, bulk_price=7.00, subtotal=18.00
|
|
),
|
|
],
|
|
total=18.00,
|
|
),
|
|
]
|
|
|
|
result = handler_module.lambda_handler({}, None)
|
|
|
|
# Verify return
|
|
assert result["status"] == "aggregated"
|
|
assert result["week"] == "2026-W20"
|
|
assert result["total_employees"] == 2
|
|
|
|
# Verify 3 S3 uploads (order summary CSV + payroll CSV + weekly PDF)
|
|
s3_calls = handler_module._s3.put_object.call_args_list
|
|
assert len(s3_calls) == 3, "Should upload 2 CSVs + 1 PDF to S3"
|
|
|
|
by_key = {call.kwargs["Key"]: call.kwargs for call in s3_calls}
|
|
assert "reports/2026-W20/order-summary.csv" in by_key
|
|
assert "reports/2026-W20/payroll-deductions.csv" in by_key
|
|
assert "reports/2026-W20/weekly-summary-2026-W20.pdf" in by_key
|
|
|
|
for kwargs in by_key.values():
|
|
assert kwargs["Bucket"] == "test-bucket"
|
|
|
|
# CSVs are text/csv; the PDF is application/pdf with real PDF bytes
|
|
assert by_key["reports/2026-W20/order-summary.csv"]["ContentType"] == "text/csv"
|
|
assert (
|
|
by_key["reports/2026-W20/payroll-deductions.csv"]["ContentType"]
|
|
== "text/csv"
|
|
)
|
|
pdf_kwargs = by_key["reports/2026-W20/weekly-summary-2026-W20.pdf"]
|
|
assert pdf_kwargs["ContentType"] == "application/pdf"
|
|
assert pdf_kwargs["Body"][:5] == b"%PDF-"
|
|
|
|
# Verify summary saved to DynamoDB
|
|
mock_put_summary.assert_called_once()
|
|
saved_summary = mock_put_summary.call_args[0][1]
|
|
assert saved_summary["week"] == "2026-W20"
|
|
assert "order_csv_s3_key" in saved_summary
|
|
assert "payroll_csv_s3_key" in saved_summary
|
|
assert (
|
|
saved_summary["weekly_summary_pdf_s3_key"]
|
|
== "reports/2026-W20/weekly-summary-2026-W20.pdf"
|
|
)
|
|
|
|
# Verify Slack notifier job is dispatched
|
|
handler_module._dispatch_job.assert_called_once_with(
|
|
{"event": "orders_aggregated", "week": "2026-W20"}
|
|
)
|
|
|
|
handler_module._sqs.send_message.assert_called_once()
|
|
send_kwargs = handler_module._sqs.send_message.call_args.kwargs
|
|
assert send_kwargs["QueueUrl"] == os.environ["CHECKCOMPONENTS_QUEUE_URL"]
|
|
body = json.loads(send_kwargs["MessageBody"])
|
|
assert body["type"] == "checkcomponents"
|
|
assert body["kind"] == "meal_deduction"
|
|
assert body["week"] == "WEEK#2026-W20"
|
|
assert "payPeriodId" not in body
|
|
emails = {line["employee_email"] for line in body["lines"]}
|
|
assert emails == {"alice@x.com", "bob@x.com"}
|
|
|
|
|
|
class TestCheckcomponentsPayload:
|
|
def test_prefixes_week_and_omits_flex_fields(self, handler_module):
|
|
orders = [
|
|
_make_order("Alice", "alice@x.com", [_make_item("Burger")], total=10.5),
|
|
_make_order("Skip", "", [_make_item("Pasta")], total=9),
|
|
]
|
|
payload = handler_module.build_checkcomponents_payload("2026-W20", orders)
|
|
assert payload == {
|
|
"type": "checkcomponents",
|
|
"kind": "meal_deduction",
|
|
"week": "WEEK#2026-W20",
|
|
"lines": [{"employee_email": "alice@x.com", "amount": "10.50"}],
|
|
}
|
|
|
|
def test_keeps_existing_week_hash_prefix(self, handler_module):
|
|
payload = handler_module.build_checkcomponents_payload(
|
|
"WEEK#2026-W20",
|
|
[_make_order("Alice", "alice@x.com", [_make_item("Burger")], total=1)],
|
|
)
|
|
assert payload["week"] == "WEEK#2026-W20"
|
|
|
|
def test_rounds_amount_half_up(self, handler_module):
|
|
payload = handler_module.build_checkcomponents_payload(
|
|
"2026-W20",
|
|
[
|
|
_make_order(
|
|
"Alice",
|
|
"alice@x.com",
|
|
[_make_item("Burger")],
|
|
total=Decimal("10.025"),
|
|
)
|
|
],
|
|
)
|
|
assert payload["lines"][0]["amount"] == "10.03"
|
|
|
|
|
|
class TestCheckcomponentsSend:
|
|
@patch("server.jobs.aggregate.put_summary")
|
|
@patch("server.jobs.aggregate.get_summary")
|
|
@patch("server.jobs.aggregate.get_orders")
|
|
@patch("server.jobs.aggregate.current_week", return_value="2026-W20")
|
|
def test_sqs_failure_still_aggregates_and_notifies(
|
|
self, mock_week, mock_orders, mock_get_summary, mock_put_summary, handler_module
|
|
):
|
|
mock_get_summary.return_value = None
|
|
mock_orders.return_value = [
|
|
_make_order("Alice", "alice@x.com", [_make_item("Burger")], total=10)
|
|
]
|
|
handler_module._sqs.send_message.side_effect = Exception("AccessDenied")
|
|
|
|
result = handler_module.lambda_handler({}, None)
|
|
|
|
assert result["status"] == "aggregated"
|
|
mock_put_summary.assert_called_once()
|
|
handler_module._dispatch_job.assert_called_once()
|
|
|
|
@patch("server.jobs.aggregate.put_summary")
|
|
@patch("server.jobs.aggregate.get_summary")
|
|
@patch("server.jobs.aggregate.get_orders")
|
|
@patch("server.jobs.aggregate.current_week", return_value="2026-W20")
|
|
def test_empty_queue_url_skips_send(
|
|
self,
|
|
mock_week,
|
|
mock_orders,
|
|
mock_get_summary,
|
|
mock_put_summary,
|
|
handler_module,
|
|
monkeypatch,
|
|
):
|
|
monkeypatch.setenv("CHECKCOMPONENTS_QUEUE_URL", "")
|
|
mock_get_summary.return_value = None
|
|
mock_orders.return_value = [
|
|
_make_order("Alice", "alice@x.com", [_make_item("Burger")], total=10)
|
|
]
|
|
|
|
result = handler_module.lambda_handler({}, None)
|
|
|
|
assert result["status"] == "aggregated"
|
|
handler_module._sqs.send_message.assert_not_called()
|
|
handler_module._dispatch_job.assert_called_once()
|