mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 05:23:13 +00:00
feat(api): add OpenAPI Redocly contract and VPC outputs (DEV-289) (#206)
* feat(infra): export attached VPC ids and lock prod to afterhours (DEV-289)
Prod must keep existing_vpc_id pointed at the afterhours VPC. Outputs
expose the resolved vpc_id and public subnet IDs.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* feat(api): add OpenAPI 3.1 and Redocly lint in CI (DEV-289)
Same extends: recommended ruleset and @redocly/cli 2.52.1 as
internal-portal. Documents current { error: string } JSON errors.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(api): document 4xx and reject invalid form-status weeks (DEV-289)
Health, form-status, and roster document 400. form-status now maps
current and returns 400 for a week that is not current or YYYY-WNN.
Redocly treats 302 as a success response, matching the portal.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* style(test): format VPC contract assertions for ruff (DEV-289)
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(api): fail Redocly on missing 4xx and 2xx/3xx (DEV-289)
Promote operation-4xx-response and the 2xx-or-3xx success rule to error.
Replace unused health and roster 400s with 403, matching portal health.
Form-status keeps its real 400 for invalid week.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(api): split week params and allow live menu nulls (DEV-289)
Menu and form-status take current or YYYY-WNN. Orders take YYYY-WNN or a
calendar date and reject current. Menu payloads may emit null menu_url,
calories, protein, and image_url.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(infra): fail prod apply without the afterhours VPC (DEV-289)
Prod never creates the 10.60 fallback VPC. A terraform_data precondition
fails plan and apply when existing_vpc_id is empty, instead of a check
block that only warns.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
This commit is contained in:
parent
fb3a181e0c
commit
d7ad49d00f
15 changed files with 881 additions and 16 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
|
@ -34,6 +34,9 @@ jobs:
|
||||||
- name: Check template JavaScript
|
- name: Check template JavaScript
|
||||||
run: npm run check:templates
|
run: npm run check:templates
|
||||||
|
|
||||||
|
- name: Lint OpenAPI
|
||||||
|
run: npm run openapi:lint
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
|
||||||
32
.redocly.yaml
Normal file
32
.redocly.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Same Redocly recommended ruleset as internal-portal (DEV-223 / DEV-289).
|
||||||
|
# Recommended operation-2xx-response does not count 302. Login-style redirects
|
||||||
|
# succeed with 302, so that rule is replaced by operation-2xx-or-3xx-response
|
||||||
|
# at error. operation-4xx-response is promoted to error so missing 4xx fails CI.
|
||||||
|
extends:
|
||||||
|
- recommended
|
||||||
|
|
||||||
|
rules:
|
||||||
|
operation-2xx-response: off
|
||||||
|
operation-4xx-response: error
|
||||||
|
rule/operation-2xx-or-3xx-response:
|
||||||
|
subject:
|
||||||
|
type: Responses
|
||||||
|
message: Operation must define a 2XX or 3XX response.
|
||||||
|
severity: error
|
||||||
|
assertions:
|
||||||
|
requireAny:
|
||||||
|
- "200"
|
||||||
|
- "201"
|
||||||
|
- "202"
|
||||||
|
- "204"
|
||||||
|
- "301"
|
||||||
|
- "302"
|
||||||
|
- "303"
|
||||||
|
- "307"
|
||||||
|
- "308"
|
||||||
|
- "2XX"
|
||||||
|
- "3XX"
|
||||||
|
|
||||||
|
apis:
|
||||||
|
meals@v1:
|
||||||
|
root: openapi.yaml
|
||||||
|
|
@ -74,6 +74,8 @@ Publish routes are omitted from CloudFront. The generated form uses relative
|
||||||
Workspace: `meal-order-manager-prod` / `meal-order-manager-dev` (us-east-1)
|
Workspace: `meal-order-manager-prod` / `meal-order-manager-dev` (us-east-1)
|
||||||
|
|
||||||
- **ECS Fargate** — Flask + gunicorn + SQS job consumer. Desired count 2 in prod, 1 in dev.
|
- **ECS Fargate** — Flask + gunicorn + SQS job consumer. Desired count 2 in prod, 1 in dev.
|
||||||
|
- **VPC** — Prod attaches to the After Hours VPC (`existing_vpc_id` / `existing_public_subnet_ids` from afterhours-shift-manager outputs). The 10.60 CIDR is unused fallback.
|
||||||
|
- **HTTP contract** — `openapi.yaml`, linted in CI with `npm run openapi:lint` (Redocly `extends: recommended`, same as internal-portal and afterhours-shift-manager).
|
||||||
- **ALB** — origin for CloudFront `/api` behaviors and weekly-menu HMAC publish. Idle timeout 120s.
|
- **ALB** — origin for CloudFront `/api` behaviors and weekly-menu HMAC publish. Idle timeout 120s.
|
||||||
- **ECR** — API image. GitHub Actions `deploy-api.yaml` owns the image; Terraform ignores `container_definitions`.
|
- **ECR** — API image. GitHub Actions `deploy-api.yaml` owns the image; Terraform ignores `container_definitions`.
|
||||||
- **S3** — `meal-order-manager-form-*` (static form hosting), `meal-order-manager-reports-*` (CSV reports + weekly summary PDF)
|
- **S3** — `meal-order-manager-form-*` (static form hosting), `meal-order-manager-reports-*` (CSV reports + weekly summary PDF)
|
||||||
|
|
@ -186,9 +188,11 @@ meal-order-manager/
|
||||||
├── .github/workflows/
|
├── .github/workflows/
|
||||||
│ ├── weekly-menu.yml # Monday cron: scrape + HMAC publish + notify
|
│ ├── weekly-menu.yml # Monday cron: scrape + HMAC publish + notify
|
||||||
│ ├── deploy-api.yaml # Image CD to Fargate
|
│ ├── deploy-api.yaml # Image CD to Fargate
|
||||||
│ ├── ci.yml # PR checks
|
│ ├── ci.yml # PR checks (pytest, template JS, OpenAPI lint)
|
||||||
│ └── ci-terraform.yaml # terraform fmt / validate
|
│ └── ci-terraform.yaml # terraform fmt / validate
|
||||||
├── terraform/ # HCP Terraform (cluster, ALB, ECR, jobs queue)
|
├── terraform/ # HCP Terraform (cluster, ALB, ECR, jobs queue)
|
||||||
|
├── openapi.yaml # Employee HTTP contract (Redocly recommended)
|
||||||
|
├── .redocly.yaml
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── scraper/ # Playwright menu scraper
|
│ ├── scraper/ # Playwright menu scraper
|
||||||
|
|
|
||||||
665
openapi.yaml
Normal file
665
openapi.yaml
Normal file
|
|
@ -0,0 +1,665 @@
|
||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Meal Order Manager
|
||||||
|
version: 0.1.0
|
||||||
|
description: >
|
||||||
|
Flask HTTP API on ECS Fargate behind orders.seahaven.com. The internal
|
||||||
|
portal SPA calls menu, submit, and admin with a Cognito ID token from
|
||||||
|
GET /api/auth/meals-token. Weekly-menu GitHub Actions uses HMAC publish
|
||||||
|
routes that CloudFront does not expose. JSON errors are currently
|
||||||
|
`{ error: string }`. Health matches the portal BFF `{ stage, sha }`.
|
||||||
|
Lint with the same Redocly `extends: recommended` config as
|
||||||
|
internal-portal and afterhours-shift-manager.
|
||||||
|
contact:
|
||||||
|
name: Sea Haven Engineering
|
||||||
|
license:
|
||||||
|
name: Proprietary
|
||||||
|
identifier: LicenseRef-SeaHaven
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: /
|
||||||
|
description: orders.seahaven.com CloudFront / local Flask :5050
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Runtime
|
||||||
|
description: Unauthenticated health
|
||||||
|
- name: Menu
|
||||||
|
description: Public weekly menu and form status
|
||||||
|
- name: Orders
|
||||||
|
description: Employee submit and own-order lookup
|
||||||
|
- name: Admin
|
||||||
|
description: Admin order edit and summary PDF
|
||||||
|
- name: Publish
|
||||||
|
description: Weekly-menu HMAC publish (not on CloudFront)
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/api/health:
|
||||||
|
get:
|
||||||
|
operationId: getHealth
|
||||||
|
tags: [Runtime]
|
||||||
|
summary: Runtime health
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Process is up
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Health"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
/api/menu/{week}:
|
||||||
|
get:
|
||||||
|
operationId: getMenu
|
||||||
|
tags: [Menu]
|
||||||
|
summary: Published menu for a week
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/MenuWeekPath"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Menu payload
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/MenuPayload"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
/api/form-status/{week}:
|
||||||
|
get:
|
||||||
|
operationId: getFormStatus
|
||||||
|
tags: [Menu]
|
||||||
|
summary: Open or closed for a week
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/MenuWeekPath"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Form status
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/FormStatus"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
/api/submit-order:
|
||||||
|
post:
|
||||||
|
operationId: submitOrder
|
||||||
|
tags: [Orders]
|
||||||
|
summary: Place or replace this week's order
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/SubmitBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Order saved
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/SubmitResult"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"410":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"429":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"503":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
/api/orders/{week}:
|
||||||
|
get:
|
||||||
|
operationId: getMyOrder
|
||||||
|
tags: [Orders]
|
||||||
|
summary: Caller's order only
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/OrderWeekPath"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Empty list when the caller has no order
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/MyOrders"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"503":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
/api/admin/orders:
|
||||||
|
get:
|
||||||
|
operationId: adminListOrders
|
||||||
|
tags: [Admin]
|
||||||
|
summary: List weeks or one week's orders
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- name: week
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Weeks list or week detail
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/AdminOrdersResponse"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
put:
|
||||||
|
operationId: adminUpdateOrder
|
||||||
|
tags: [Admin]
|
||||||
|
summary: Recalculate and replace an order
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/AdminUpdateBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Updated
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/AdminMutation"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"503":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
delete:
|
||||||
|
operationId: adminDeleteOrder
|
||||||
|
tags: [Admin]
|
||||||
|
summary: Delete an order
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- name: week
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: email
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Deleted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/AdminMutation"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
/api/admin/summary-pdf:
|
||||||
|
get:
|
||||||
|
operationId: adminSummaryPdf
|
||||||
|
tags: [Admin]
|
||||||
|
summary: Presigned summary PDF URL
|
||||||
|
security:
|
||||||
|
- portalCognito: []
|
||||||
|
parameters:
|
||||||
|
- name: week
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Short-lived URL
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/SummaryPdf"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"500":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
/api/roster:
|
||||||
|
get:
|
||||||
|
operationId: getRoster
|
||||||
|
tags: [Orders]
|
||||||
|
summary: Name and email list used by the static form
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Roster
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/FormRoster"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
/api/publish/settings:
|
||||||
|
get:
|
||||||
|
operationId: publishSettings
|
||||||
|
tags: [Publish]
|
||||||
|
summary: Bulk discount and subsidy for scrape
|
||||||
|
security:
|
||||||
|
- publishKey: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Pricing fields only
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PublishSettings"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
/api/publish/menu:
|
||||||
|
post:
|
||||||
|
operationId: publishMenu
|
||||||
|
tags: [Publish]
|
||||||
|
summary: Write this week's scraped menu
|
||||||
|
security:
|
||||||
|
- publishKey: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PublishMenuBody"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Published
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PublishMenuResult"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/StringError"
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
portalCognito:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
description: Portal Cognito ID token. GIS google_id_token is also accepted on submit until cutover.
|
||||||
|
publishKey:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-Meals-Publish-Key
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
MenuWeekPath:
|
||||||
|
name: week
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: "`current` or `YYYY-WNN`. Other values are 400."
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
OrderWeekPath:
|
||||||
|
name: week
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: "`YYYY-WNN` or `YYYY-MM-DD`. `current` is 400."
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
|
||||||
|
responses:
|
||||||
|
StringError:
|
||||||
|
description: Current meals JSON error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/StringErrorBody"
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
Health:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [stage, sha]
|
||||||
|
properties:
|
||||||
|
stage:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
description: Workspace stage (`dev`, `prod`, or `local`)
|
||||||
|
sha:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
description: Git SHA or `unknown` locally
|
||||||
|
|
||||||
|
StringErrorBody:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [error]
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
|
||||||
|
Meal:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
required: [name, price]
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
price:
|
||||||
|
type: number
|
||||||
|
calories:
|
||||||
|
type: [string, number, "null"]
|
||||||
|
protein:
|
||||||
|
type: [string, number, "null"]
|
||||||
|
description:
|
||||||
|
type: [string, "null"]
|
||||||
|
dietary_tags:
|
||||||
|
type: [array, "null"]
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
image_url:
|
||||||
|
type: [string, "null"]
|
||||||
|
is_new:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
MenuPayload:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- week
|
||||||
|
- form_status
|
||||||
|
- meal_count
|
||||||
|
- meals
|
||||||
|
- bulk_discount_percent
|
||||||
|
- company_subsidy_percent
|
||||||
|
properties:
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
form_status:
|
||||||
|
type: string
|
||||||
|
scraped_at:
|
||||||
|
type: [string, "null"]
|
||||||
|
menu_url:
|
||||||
|
type: [string, "null"]
|
||||||
|
meal_count:
|
||||||
|
type: integer
|
||||||
|
meals:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Meal"
|
||||||
|
bulk_discount_percent:
|
||||||
|
type: number
|
||||||
|
company_subsidy_percent:
|
||||||
|
type: number
|
||||||
|
order_deadline:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
FormStatus:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [week, status]
|
||||||
|
properties:
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
reopen_at:
|
||||||
|
type: integer
|
||||||
|
|
||||||
|
SubmitItem:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
required: [name, quantity]
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
quantity:
|
||||||
|
type: number
|
||||||
|
retail_price:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
SubmitBody:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
required: [items]
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/SubmitItem"
|
||||||
|
google_id_token:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
SubmitResult:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [status]
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
total:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
MyOrders:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [week, orders]
|
||||||
|
properties:
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
orders:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [employee_email]
|
||||||
|
properties:
|
||||||
|
employee_email:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
AdminWeek:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
required: [week]
|
||||||
|
properties:
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
form_status:
|
||||||
|
type: string
|
||||||
|
meal_count:
|
||||||
|
type: integer
|
||||||
|
order_count:
|
||||||
|
type: integer
|
||||||
|
|
||||||
|
AdminOrderItem:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [name, quantity, retail_price, price, subtotal]
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
quantity:
|
||||||
|
type: integer
|
||||||
|
retail_price:
|
||||||
|
type: number
|
||||||
|
price:
|
||||||
|
type: number
|
||||||
|
subtotal:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
AdminOrder:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [employee_name, employee_email, items, total, submitted_at]
|
||||||
|
properties:
|
||||||
|
employee_name:
|
||||||
|
type: string
|
||||||
|
employee_email:
|
||||||
|
type: string
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/AdminOrderItem"
|
||||||
|
total:
|
||||||
|
type: number
|
||||||
|
submitted_at:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
AdminWeeks:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [weeks]
|
||||||
|
properties:
|
||||||
|
weeks:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/AdminWeek"
|
||||||
|
|
||||||
|
AdminWeekOrders:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [week, orders, total_employees, grand_total]
|
||||||
|
properties:
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
orders:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/AdminOrder"
|
||||||
|
total_employees:
|
||||||
|
type: integer
|
||||||
|
grand_total:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
AdminOrdersResponse:
|
||||||
|
oneOf:
|
||||||
|
- $ref: "#/components/schemas/AdminWeeks"
|
||||||
|
- $ref: "#/components/schemas/AdminWeekOrders"
|
||||||
|
|
||||||
|
AdminUpdateBody:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [week, email, items]
|
||||||
|
properties:
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/SubmitItem"
|
||||||
|
|
||||||
|
AdminMutation:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [status, week]
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
total:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
SummaryPdf:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [week, url]
|
||||||
|
properties:
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
|
||||||
|
FormRoster:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [employees]
|
||||||
|
properties:
|
||||||
|
employees:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [name, email]
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
PublishSettings:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [bulk_discount_percent, company_subsidy_percent]
|
||||||
|
properties:
|
||||||
|
bulk_discount_percent:
|
||||||
|
type: number
|
||||||
|
company_subsidy_percent:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
PublishMenuBody:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
required: [meals]
|
||||||
|
properties:
|
||||||
|
meals:
|
||||||
|
type: array
|
||||||
|
minItems: 1
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Meal"
|
||||||
|
scraped_at:
|
||||||
|
type: string
|
||||||
|
menu_url:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
PublishMenuResult:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [status, week, meal_count]
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
week:
|
||||||
|
type: string
|
||||||
|
meal_count:
|
||||||
|
type: integer
|
||||||
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
|
"@redocly/cli": "2.52.1",
|
||||||
"eslint": "10.10.0",
|
"eslint": "10.10.0",
|
||||||
"globals": "17.12.0",
|
"globals": "17.12.0",
|
||||||
"prettier": "3.9.8"
|
"prettier": "3.9.8"
|
||||||
|
|
@ -256,6 +257,21 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@redocly/cli": {
|
||||||
|
"version": "2.52.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.52.1.tgz",
|
||||||
|
"integrity": "sha512-gwfx1WelVDHU/cw+GkpLV9+xksOI3obJGgL1nUVrAFUyXNDxM+aifXDUZk7BfbOIpF69Sea2ant0cOqbQfg/KQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"openapi": "bin/cli.js",
|
||||||
|
"redocly": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0 || >=20.19.0 <21.0.0",
|
||||||
|
"npm": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/esrecurse": {
|
"node_modules/@types/esrecurse": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@
|
||||||
"check:templates": "npm run lint:templates && npm run format:templates",
|
"check:templates": "npm run lint:templates && npm run format:templates",
|
||||||
"format:templates": "prettier --check \"src/server/templates/*.js\"",
|
"format:templates": "prettier --check \"src/server/templates/*.js\"",
|
||||||
"format:templates:write": "prettier --write \"src/server/templates/*.js\"",
|
"format:templates:write": "prettier --write \"src/server/templates/*.js\"",
|
||||||
"lint:templates": "eslint \"src/server/templates/*.js\""
|
"lint:templates": "eslint \"src/server/templates/*.js\"",
|
||||||
|
"openapi:lint": "redocly lint --config .redocly.yaml openapi.yaml"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
|
"@redocly/cli": "2.52.1",
|
||||||
"eslint": "10.10.0",
|
"eslint": "10.10.0",
|
||||||
"globals": "17.12.0",
|
"globals": "17.12.0",
|
||||||
"prettier": "3.9.8"
|
"prettier": "3.9.8"
|
||||||
|
|
|
||||||
|
|
@ -279,15 +279,20 @@ def lambda_handler(event, context):
|
||||||
return response(405, {"error": "Method not allowed"})
|
return response(405, {"error": "Method not allowed"})
|
||||||
|
|
||||||
|
|
||||||
def handle_menu(event):
|
def _week_from_path(event):
|
||||||
"""Return the published menu in the portal's public MenuPayload shape."""
|
|
||||||
requested_week = (event.get("pathParameters") or {}).get("week", "")
|
requested_week = (event.get("pathParameters") or {}).get("week", "")
|
||||||
if requested_week == "current":
|
if requested_week == "current":
|
||||||
week = current_week()
|
return current_week(), None
|
||||||
elif re.fullmatch(r"\d{4}-W\d{2}", requested_week):
|
if re.fullmatch(r"\d{4}-W\d{2}", requested_week):
|
||||||
week = requested_week
|
return requested_week, None
|
||||||
else:
|
return None, response(400, {"error": "week path param must be current or YYYY-WNN"})
|
||||||
return response(400, {"error": "week path param must be current or YYYY-WNN"})
|
|
||||||
|
|
||||||
|
def handle_menu(event):
|
||||||
|
"""Return the published menu in the portal's public MenuPayload shape."""
|
||||||
|
week, error = _week_from_path(event)
|
||||||
|
if error is not None:
|
||||||
|
return error
|
||||||
|
|
||||||
menu = get_menu(week)
|
menu = get_menu(week)
|
||||||
if menu is None:
|
if menu is None:
|
||||||
|
|
@ -623,7 +628,9 @@ def handle_my_orders(event):
|
||||||
|
|
||||||
|
|
||||||
def handle_form_status(event):
|
def handle_form_status(event):
|
||||||
week = event.get("pathParameters", {}).get("week", current_week())
|
week, error = _week_from_path(event)
|
||||||
|
if error is not None:
|
||||||
|
return error
|
||||||
status = get_form_status(week)
|
status = get_form_status(week)
|
||||||
result = {"week": week, "status": status}
|
result = {"week": week, "status": status}
|
||||||
if status == "closed":
|
if status == "closed":
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,13 @@ check "dev_has_no_custom_domain" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check "prod_reuses_afterhours_vpc" {
|
||||||
|
assert {
|
||||||
|
condition = !local.is_prod || var.existing_vpc_id != ""
|
||||||
|
error_message = "Prod must set existing_vpc_id to the afterhours VPC. Do not mint 10.60."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
check "existing_vpc_pair" {
|
check "existing_vpc_pair" {
|
||||||
assert {
|
assert {
|
||||||
condition = (var.existing_vpc_id == "") == (length(var.existing_public_subnet_ids) == 0)
|
condition = (var.existing_vpc_id == "") == (length(var.existing_public_subnet_ids) == 0)
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ locals {
|
||||||
|
|
||||||
github_oidc_provider_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com"
|
github_oidc_provider_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com"
|
||||||
|
|
||||||
# Created only when existing_vpc_id is empty. 10.60 is unused in 011934824531.
|
# Created only when existing_vpc_id is empty. Prod attaches to afterhours 10.70.
|
||||||
manage_vpc = var.existing_vpc_id == ""
|
# 10.60 is the unused fallback CIDR, not a second prod VPC.
|
||||||
|
manage_vpc = var.existing_vpc_id == "" && !local.is_prod
|
||||||
vpc_cidr = "10.60.0.0/16"
|
vpc_cidr = "10.60.0.0/16"
|
||||||
public_subnet_cidrs = ["10.60.0.0/24", "10.60.1.0/24"]
|
public_subnet_cidrs = ["10.60.0.0/24", "10.60.1.0/24"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,3 +57,13 @@ output "ecs_task_role_arn" {
|
||||||
description = "ECS task role; paychex-checkcomponents queue policy must allow this ARN."
|
description = "ECS task role; paychex-checkcomponents queue policy must allow this ARN."
|
||||||
value = aws_iam_role.ecs_task.arn
|
value = aws_iam_role.ecs_task.arn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "vpc_id" {
|
||||||
|
description = "VPC the ALB and Fargate tasks run in. Prod attaches to afterhours."
|
||||||
|
value = local.vpc_id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "public_subnet_ids" {
|
||||||
|
description = "Public subnet IDs for the ALB and Fargate tasks."
|
||||||
|
value = local.public_subnet_ids
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ variable "checkcomponents_queue_arn" {
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "existing_vpc_id" {
|
variable "existing_vpc_id" {
|
||||||
description = "When set, place the ALB and Fargate tasks in this VPC instead of creating one."
|
description = "When set, place the ALB and Fargate tasks in this VPC instead of creating one. Prod attaches to the afterhours VPC."
|
||||||
type = string
|
type = string
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ data "aws_availability_zones" "available" {
|
||||||
}
|
}
|
||||||
|
|
||||||
data "aws_vpc" "existing" {
|
data "aws_vpc" "existing" {
|
||||||
count = local.manage_vpc ? 0 : 1
|
count = var.existing_vpc_id == "" ? 0 : 1
|
||||||
id = var.existing_vpc_id
|
id = var.existing_vpc_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -13,6 +13,17 @@ data "aws_subnet" "existing_public" {
|
||||||
id = each.value
|
id = each.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "terraform_data" "prod_requires_afterhours_vpc" {
|
||||||
|
input = var.existing_vpc_id
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = !local.is_prod || var.existing_vpc_id != ""
|
||||||
|
error_message = "Prod must set existing_vpc_id to the afterhours VPC. Do not mint 10.60."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resource "aws_vpc" "this" {
|
resource "aws_vpc" "this" {
|
||||||
count = local.manage_vpc ? 1 : 0
|
count = local.manage_vpc ? 1 : 0
|
||||||
|
|
||||||
|
|
@ -24,6 +35,13 @@ resource "aws_vpc" "this" {
|
||||||
Name = "${local.project}-vpc"
|
Name = "${local.project}-vpc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = !local.is_prod
|
||||||
|
error_message = "Prod must set existing_vpc_id to the afterhours VPC. Do not mint 10.60."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# First apply updates the live hcptf apply role before CreateVpc.
|
# First apply updates the live hcptf apply role before CreateVpc.
|
||||||
depends_on = [
|
depends_on = [
|
||||||
aws_iam_role_policy_attachments_exclusive.hcptf_apply,
|
aws_iam_role_policy_attachments_exclusive.hcptf_apply,
|
||||||
|
|
@ -80,7 +98,7 @@ resource "aws_route_table_association" "public" {
|
||||||
}
|
}
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
vpc_id = local.manage_vpc ? aws_vpc.this[0].id : data.aws_vpc.existing[0].id
|
vpc_id = local.manage_vpc ? aws_vpc.this[0].id : try(data.aws_vpc.existing[0].id, var.existing_vpc_id)
|
||||||
public_subnet_ids = local.manage_vpc ? aws_subnet.public[*].id : var.existing_public_subnet_ids
|
public_subnet_ids = local.manage_vpc ? aws_subnet.public[*].id : var.existing_public_subnet_ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
45
tests/test_openapi_contract.py
Normal file
45
tests/test_openapi_contract.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""OpenAPI 3.1 + Redocly recommended, matching internal-portal (DEV-289)."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_openapi_uses_redocly_recommended():
|
||||||
|
redocly = (ROOT / ".redocly.yaml").read_text()
|
||||||
|
package = (ROOT / "package.json").read_text()
|
||||||
|
spec = (ROOT / "openapi.yaml").read_text()
|
||||||
|
ci = (ROOT / ".github" / "workflows" / "ci.yml").read_text()
|
||||||
|
assert "extends:" in redocly
|
||||||
|
assert "- recommended" in redocly
|
||||||
|
assert "operation-2xx-response: off" in redocly
|
||||||
|
assert "operation-4xx-response: error" in redocly
|
||||||
|
assert "rule/operation-2xx-or-3xx-response" in redocly
|
||||||
|
assert "severity: error" in redocly
|
||||||
|
health = spec.split("/api/health:", 1)[1].split("\n /", 1)[0]
|
||||||
|
assert '"403":' in health
|
||||||
|
assert '"400":' not in health
|
||||||
|
roster = spec.split("/api/roster:", 1)[1].split("\n /", 1)[0]
|
||||||
|
assert '"403":' in roster
|
||||||
|
assert '"400":' not in roster
|
||||||
|
form_status = spec.split("/api/form-status/{week}:", 1)[1].split("\n /", 1)[0]
|
||||||
|
assert '"400":' in form_status
|
||||||
|
menu = spec.split("/api/menu/{week}:", 1)[1].split("\n /", 1)[0]
|
||||||
|
orders = spec.split("/api/orders/{week}:", 1)[1].split("\n /", 1)[0]
|
||||||
|
assert "MenuWeekPath" in menu
|
||||||
|
assert "MenuWeekPath" in form_status
|
||||||
|
assert "OrderWeekPath" in orders
|
||||||
|
assert "WeekPath" not in spec.split("components:", 1)[1].split("MenuWeekPath", 1)[0]
|
||||||
|
assert "`current` or `YYYY-WNN`" in spec
|
||||||
|
assert "`YYYY-WNN` or `YYYY-MM-DD`" in spec
|
||||||
|
meal = spec.split(" Meal:", 1)[1].split("\n MenuPayload:", 1)[0]
|
||||||
|
assert 'type: [string, number, "null"]' in meal
|
||||||
|
assert 'type: [string, "null"]' in meal
|
||||||
|
assert 'menu_url:\n type: [string, "null"]' in spec
|
||||||
|
assert "root: openapi.yaml" in redocly
|
||||||
|
assert '"openapi:lint"' in package
|
||||||
|
assert '"@redocly/cli": "2.52.1"' in package
|
||||||
|
assert "npm run openapi:lint" in ci
|
||||||
|
assert "openapi: 3.1.0" in spec
|
||||||
|
assert "required: [stage, sha]" in spec
|
||||||
|
assert "{ error: string }" in spec or "`{ error: string }`" in spec
|
||||||
|
|
@ -1446,6 +1446,33 @@ def test_form_status_open(mock_week, mock_status):
|
||||||
assert "reopen_at" not in body, "reopen_at should NOT be present when form is open"
|
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="open")
|
||||||
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||||
|
def test_form_status_current_maps_to_current_week(mock_week, mock_status):
|
||||||
|
from submit_order_handler import lambda_handler
|
||||||
|
|
||||||
|
event = _make_event(
|
||||||
|
method="GET", path="/form-status/current", path_parameters={"week": "current"}
|
||||||
|
)
|
||||||
|
status, body = _parse_response(lambda_handler(event, None))
|
||||||
|
assert status == 200, f"Expected 200, got {status}: {body}"
|
||||||
|
assert body["week"] == "2026-W20"
|
||||||
|
mock_status.assert_called_once_with("2026-W20")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("submit_order_handler.get_form_status")
|
||||||
|
def test_form_status_rejects_invalid_week(mock_status):
|
||||||
|
from submit_order_handler import lambda_handler
|
||||||
|
|
||||||
|
event = _make_event(
|
||||||
|
method="GET", path="/form-status/nope", path_parameters={"week": "nope"}
|
||||||
|
)
|
||||||
|
status, body = _parse_response(lambda_handler(event, None))
|
||||||
|
assert status == 400, f"Expected 400, got {status}: {body}"
|
||||||
|
assert "week path param" in body["error"]
|
||||||
|
mock_status.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@patch("submit_order_handler.get_form_status", return_value="closed")
|
@patch("submit_order_handler.get_form_status", return_value="closed")
|
||||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||||
def test_form_status_closed_reopen_at(mock_week, mock_status):
|
def test_form_status_closed_reopen_at(mock_week, mock_status):
|
||||||
|
|
@ -2137,6 +2164,24 @@ def test_my_orders_empty_when_none(mock_looks_like, mock_portal, mock_get):
|
||||||
assert body == {"week": "2026-W36", "orders": []}
|
assert body == {"week": "2026-W36", "orders": []}
|
||||||
|
|
||||||
|
|
||||||
|
@patch("submit_order_handler.get_order")
|
||||||
|
@patch(
|
||||||
|
"submit_order_handler._verify_portal_token",
|
||||||
|
return_value={
|
||||||
|
"name": "Portal Employee",
|
||||||
|
"email": "portal.employee@seahavenind.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@patch("submit_order_handler.looks_like_cognito_token", return_value=True)
|
||||||
|
def test_my_orders_rejects_current_week(mock_looks_like, mock_portal, mock_get):
|
||||||
|
status, body = _parse_response(
|
||||||
|
submit_order_handler.lambda_handler(_orders_event(week="current"), None)
|
||||||
|
)
|
||||||
|
assert status == 400
|
||||||
|
assert "YYYY-WNN" in body["error"]
|
||||||
|
mock_get.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_my_orders_requires_bearer():
|
def test_my_orders_requires_bearer():
|
||||||
status, body = _parse_response(
|
status, body = _parse_response(
|
||||||
submit_order_handler.lambda_handler(_orders_event(token=""), None)
|
submit_order_handler.lambda_handler(_orders_event(token=""), None)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ def test_meals_owns_a_vpc_instead_of_looking_up_default():
|
||||||
assert "count = local.manage_vpc ? 1 : 0" in vpc
|
assert "count = local.manage_vpc ? 1 : 0" in vpc
|
||||||
assert 'variable "existing_vpc_id"' in variables
|
assert 'variable "existing_vpc_id"' in variables
|
||||||
assert 'variable "existing_public_subnet_ids"' in variables
|
assert 'variable "existing_public_subnet_ids"' in variables
|
||||||
assert 'manage_vpc = var.existing_vpc_id == ""' in locals_tf
|
assert (
|
||||||
|
'manage_vpc = var.existing_vpc_id == "" && !local.is_prod' in locals_tf
|
||||||
|
)
|
||||||
assert 'data "aws_vpc" "default"' not in ecs
|
assert 'data "aws_vpc" "default"' not in ecs
|
||||||
assert "data.aws_vpc.default" not in ecs
|
assert "data.aws_vpc.default" not in ecs
|
||||||
assert "data.aws_subnets.default" not in ecs
|
assert "data.aws_subnets.default" not in ecs
|
||||||
|
|
@ -33,3 +35,11 @@ def test_meals_owns_a_vpc_instead_of_looking_up_default():
|
||||||
assert 'check "existing_subnets_in_vpc"' in data
|
assert 'check "existing_subnets_in_vpc"' in data
|
||||||
assert "from = aws_vpc.this" in vpc
|
assert "from = aws_vpc.this" in vpc
|
||||||
assert "to = aws_vpc.this[0]" in vpc
|
assert "to = aws_vpc.this[0]" in vpc
|
||||||
|
outputs = _read("outputs.tf")
|
||||||
|
assert 'output "vpc_id"' in outputs
|
||||||
|
assert "value = local.vpc_id" in outputs
|
||||||
|
assert 'output "public_subnet_ids"' in outputs
|
||||||
|
assert 'check "prod_reuses_afterhours_vpc"' in data
|
||||||
|
assert "prod_requires_afterhours_vpc" in vpc
|
||||||
|
assert "Do not mint 10.60." in vpc
|
||||||
|
assert 'count = var.existing_vpc_id == "" ? 0 : 1' in vpc
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue