mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 05:33:12 +00:00
* feat(infra): export vpc_id and public subnet outputs (DEV-289) Portal Fargate and meals already attach to this VPC. These outputs are the HCP existing_vpc_id / existing_public_subnet_ids values. 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. Covers health, roster, and portal /api/shifts. Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * fix(api): document 4xx and treat 302 as success in Redocly (DEV-289) Health and CORS preflight document 400. Recommended only counted 2XX, so login-style 302s use a shared 2XX-or-3XX rule. 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. Drop unused 400s on health and CORS OPTIONS. Health documents 403 like the portal. CORS stays in Flask and is not part of the employee contract. 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>
221 lines
8.4 KiB
Python
221 lines
8.4 KiB
Python
"""Contracts for the HCP Terraform seam (PLAT-74 / PLAT-216)."""
|
|
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
TERRAFORM = ROOT / "terraform"
|
|
HCP_IAM = (TERRAFORM / "hcp_iam.tf").read_text()
|
|
CI = (ROOT / ".github" / "workflows" / "ci.yaml").read_text()
|
|
LOCALS = (TERRAFORM / "locals.tf").read_text()
|
|
VARIABLES = (TERRAFORM / "variables.tf").read_text()
|
|
|
|
|
|
def _tf_without_comments(text: str) -> str:
|
|
return "\n".join(line.split("#", 1)[0] for line in text.splitlines())
|
|
|
|
|
|
def test_sam_template_removed():
|
|
assert not (ROOT / "template.yaml").exists()
|
|
assert not (ROOT / "samconfig.toml.example").exists()
|
|
|
|
|
|
def test_zip_cd_removed():
|
|
assert not (ROOT / ".github" / "workflows" / "deploy.yaml").exists()
|
|
for path in TERRAFORM.glob("*.tf"):
|
|
assert 'resource "aws_lambda_function"' not in path.read_text()
|
|
assert not (TERRAFORM / "apigateway.tf").exists()
|
|
assert not (TERRAFORM / "events.tf").exists()
|
|
|
|
|
|
def test_schedules_disabled_by_default():
|
|
chunk = (
|
|
(TERRAFORM / "variables.tf")
|
|
.read_text()
|
|
.split('variable "schedules_enabled"')[1]
|
|
)
|
|
chunk = chunk.split("variable ")[0]
|
|
assert "default = false" in chunk or "default = false" in chunk
|
|
|
|
|
|
def test_ecs_schedules_disabled_by_default():
|
|
chunk = (
|
|
(TERRAFORM / "variables.tf")
|
|
.read_text()
|
|
.split('variable "ecs_schedules_enabled"')[1]
|
|
)
|
|
chunk = chunk.split("variable ")[0]
|
|
assert "default = false" in chunk or "default = false" in chunk
|
|
|
|
|
|
def test_workspaces_use_app_tag():
|
|
versions = (TERRAFORM / "versions.tf").read_text()
|
|
assert 'tags = ["app:afterhours-shift-manager"]' in versions
|
|
assert 'name = "afterhours-shift-manager-prod"' not in versions
|
|
assert 'hcp_workspace = "${local.project}-${var.environment}"' in LOCALS
|
|
assert "seahaven-${var.environment}" in LOCALS
|
|
|
|
|
|
def test_ecs_ignore_changes_and_task_size():
|
|
ecs = (TERRAFORM / "ecs.tf").read_text()
|
|
assert "ignore_changes = [container_definitions]" in ecs
|
|
assert "ignore_changes = [task_definition, desired_count]" in ecs
|
|
assert 'cpu = "512"' in ecs
|
|
assert 'memory = "1024"' in ecs
|
|
assert 'cpu_architecture = "ARM64"' in ecs
|
|
assert 'path = "/api/health"' in ecs
|
|
|
|
|
|
def test_stack_owns_a_vpc_instead_of_looking_up_default():
|
|
vpc = (TERRAFORM / "vpc.tf").read_text()
|
|
ecs = (TERRAFORM / "ecs.tf").read_text()
|
|
assert 'resource "aws_vpc" "this"' in vpc
|
|
assert "cidr_block = local.vpc_cidr" in vpc
|
|
assert 'vpc_cidr = "10.70.0.0/16"' in LOCALS
|
|
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 "aws_vpc.this.id" in ecs
|
|
assert "aws_subnet.public[*].id" in ecs
|
|
assert "ec2:CreateVpc" in HCP_IAM
|
|
assert "sid = \"RefreshVpc\"" in HCP_IAM
|
|
assert "afterhours-shift-manager-ecs" in HCP_IAM
|
|
assert "hcptf_apply_ecs" in HCP_IAM
|
|
assert 'resource "aws_iam_policy" "hcptf_apply_ecs"' in HCP_IAM
|
|
assert 'resource "aws_iam_role_policy" "hcptf_apply_ecs"' not in HCP_IAM
|
|
assert "aws_iam_policy.hcptf_apply_ecs.arn" in HCP_IAM
|
|
assert "sid = \"CreateElbAndEcsServiceLinkedRoles\"" in HCP_IAM
|
|
assert "iam:CreateServiceLinkedRole" in HCP_IAM
|
|
|
|
|
|
def test_ecs_task_boundary_uses_static_arns():
|
|
iam = (TERRAFORM / "iam.tf").read_text()
|
|
chunk = iam.split('data "aws_iam_policy_document" "ecs_task_boundary"')[1]
|
|
chunk = chunk.split("resource ")[0]
|
|
assert "aws_dynamodb_table.shifts" not in chunk
|
|
assert "aws_sqs_queue.jobs" not in chunk
|
|
assert "aws_ecr_repository.api" not in chunk
|
|
assert "aws_cloudwatch_log_group.api" not in chunk
|
|
assert "table/${local.table_name}" in chunk
|
|
assert "log-group:/ecs/${local.project}" in chunk
|
|
|
|
|
|
def test_deploy_api_workflow_exists():
|
|
deploy_api = (ROOT / ".github" / "workflows" / "deploy-api.yaml").read_text()
|
|
assert "environment: ${{ needs.target.outputs.environment }}" in deploy_api
|
|
assert "/afterhours-shift-manager/deploy/cluster" in deploy_api
|
|
assert "linux/arm64" in deploy_api
|
|
assert "gh release create" in deploy_api
|
|
|
|
|
|
def test_in_repo_hcptf_roles():
|
|
assert 'apply_role = "hcptf-afterhours-shift-manager"' in LOCALS
|
|
assert 'plan_role = "hcptf-afterhours-shift-manager-plan"' in LOCALS
|
|
assert "hcptf_apply" in HCP_IAM
|
|
assert "DenyCreatePolicy" in HCP_IAM
|
|
|
|
|
|
def test_ci_runs_pytest_and_terraform_validate():
|
|
assert "ci-python-sam" not in CI
|
|
assert "pytest" in CI
|
|
assert "terraform fmt -check" in CI
|
|
assert "terraform init -backend=false" in CI
|
|
assert "terraform validate" in CI
|
|
assert "openapi:lint" in CI
|
|
assert "npm ci" in CI
|
|
|
|
|
|
def test_openapi_uses_redocly_recommended():
|
|
redocly = (ROOT / ".redocly.yaml").read_text()
|
|
package = (ROOT / "package.json").read_text()
|
|
spec = (ROOT / "openapi.yaml").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
|
|
assert "optionsShifts" not in spec
|
|
health = spec.split("/api/health:", 1)[1].split("\n /", 1)[0]
|
|
assert '"403":' in health
|
|
assert '"400":' not in health
|
|
assert 'root: openapi.yaml' in redocly
|
|
assert '"openapi:lint"' in package
|
|
assert '"@redocly/cli": "2.52.1"' in package
|
|
assert "openapi: 3.1.0" in spec
|
|
assert "required: [stage, sha]" in spec
|
|
|
|
|
|
def test_checkcomponents_queue_arn_variable_matches_iam_references():
|
|
assert 'variable "checkcomponents_queue_arn"' in VARIABLES
|
|
iam = (TERRAFORM / "iam.tf").read_text()
|
|
assert "var.checkcomponents_queue_arn" in iam
|
|
boundary = (TERRAFORM / "lambda_boundary.tf").read_text()
|
|
assert "var.checkcomponents_queue_arn" in boundary
|
|
data_tf = (TERRAFORM / "data.tf").read_text()
|
|
assert "dev_has_no_paychex" in data_tf
|
|
assert "checkcomponents_pair" in data_tf
|
|
|
|
|
|
def test_leftover_lambda_iam_roles_removed():
|
|
for path in TERRAFORM.glob("*.tf"):
|
|
body = _tf_without_comments(path.read_text())
|
|
assert 'resource "aws_iam_role" "lambda"' not in body
|
|
assert 'resource "aws_iam_role_policy" "lambda"' not in body
|
|
assert 'resource "aws_iam_role_policy_attachment" "lambda_basic"' not in body
|
|
assert 'data "aws_iam_policy_document" "lambda_assume"' not in body
|
|
|
|
|
|
def test_lambda_boundary_policy_remains():
|
|
boundary = (TERRAFORM / "lambda_boundary.tf").read_text()
|
|
assert 'resource "aws_iam_policy" "lambda_boundary"' in boundary
|
|
assert "afterhours-shift-manager-lambda-boundary" in boundary
|
|
|
|
|
|
def test_local_functions_still_lists_packaging_keys():
|
|
for name in (
|
|
"afterhours-shift-manager",
|
|
"afterhours-weekly-post",
|
|
"afterhours-roster-sync",
|
|
"afterhours-roster-api",
|
|
"afterhours-ring-scheduler",
|
|
"afterhours-holiday-router",
|
|
"afterhours-portal-api",
|
|
):
|
|
assert name in LOCALS
|
|
assert "afterhours-release-notifier" not in LOCALS
|
|
assert "weekly_post" in LOCALS
|
|
|
|
|
|
def test_github_deploy_trust_covers_image_only():
|
|
iam = (TERRAFORM / "iam_github_deploy.tf").read_text()
|
|
assert "environment:prod" in iam or "environment:prod" in LOCALS
|
|
assert "environment:dev" in iam or "environment:dev" in LOCALS
|
|
assert "deploy.yaml@" not in iam
|
|
assert "deploy-api.yaml@refs/heads/${var.github_deploy_branch}" in iam
|
|
assert "deploy-api.yaml@refs/tags/v*" in iam
|
|
assert "ecs:ListTasks" in iam
|
|
|
|
|
|
def test_plan_refresh_includes_provider6_s3_gets():
|
|
assert "s3:GetLifecycleConfiguration" in HCP_IAM
|
|
assert "s3:GetReplicationConfiguration" in HCP_IAM
|
|
assert "s3:GetBucketReplication" in HCP_IAM
|
|
|
|
|
|
def test_origins_use_fargate_url():
|
|
outputs = (TERRAFORM / "outputs.tf").read_text()
|
|
assert "aws_apigatewayv2_api.http" not in outputs
|
|
assert '${local.api_url}/slack/events' in outputs
|
|
assert "value = local.api_url" in outputs
|
|
assert "output \"vpc_id\"" in outputs
|
|
assert "output \"public_subnet_ids\"" in outputs
|
|
assert "aws_vpc.this.id" in outputs
|
|
|
|
|
|
def test_alb_alarms_remain():
|
|
alarms = (TERRAFORM / "alarms.tf").read_text()
|
|
assert "ALB-5xx-" in alarms
|
|
assert "ALB-Latency-" in alarms
|
|
assert "ALB-UnhealthyHost-" in alarms
|
|
assert "ApiGateway-" not in alarms
|
|
assert "Lambda-" not in alarms
|