Skip to content

Commit 51eae3b

Browse files
sir-sigurdclaude
andcommitted
Add terraform test coverage for vpc + quilt modules
Plan-only, provider-mocked tests — no AWS credentials, no infrastructure: - modules/vpc: characterize the new-vs-existing network input validation — valid new/existing configs plan cleanly; contradictory configs trip the configuration_error precondition. - modules/quilt: smoke-test the public module end-to-end (vpc + db + search + CloudFormation) through a wrapper root that re-exposes only the non-sensitive stack name. Run both in a new CI `test` job; bump CI Terraform 1.5.0 -> 1.10.0 (mock_provider requires >= 1.7). No module behavior changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 984239c commit 51eae3b

6 files changed

Lines changed: 322 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- uses: actions/checkout@v6
1616
- uses: hashicorp/setup-terraform@v4
1717
with:
18-
terraform_version: "1.5.0"
18+
terraform_version: "1.10.0"
1919
terraform_wrapper: false
2020
- run: terraform fmt -check -recursive -diff
2121

@@ -37,8 +37,29 @@ jobs:
3737
- uses: actions/checkout@v6
3838
- uses: hashicorp/setup-terraform@v4
3939
with:
40-
terraform_version: "1.5.0"
40+
terraform_version: "1.10.0"
4141
terraform_wrapper: false
4242
# -backend=false: validate needs init for provider/module schemas but no backend or cloud credentials.
4343
- run: terraform init -backend=false
4444
- run: terraform validate -no-color
45+
46+
test:
47+
runs-on: ubuntu-latest
48+
strategy:
49+
fail-fast: false
50+
matrix:
51+
dir:
52+
- modules/vpc
53+
- modules/quilt/tests/smoke
54+
defaults:
55+
run:
56+
working-directory: ${{ matrix.dir }}
57+
steps:
58+
- uses: actions/checkout@v6
59+
- uses: hashicorp/setup-terraform@v4
60+
with:
61+
terraform_version: "1.10.0"
62+
terraform_wrapper: false
63+
# The tests mock the AWS provider, so no backend or cloud credentials are needed.
64+
- run: terraform init -backend=false
65+
- run: terraform test -no-color

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.DS_Store
22
.terraform
3+
.terraform.lock.hcl
34
tfplan
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
AWSTemplateFormatVersion: "2010-09-09"
2+
Description: >-
3+
Test fixture for terraform test smoke runs. Stands in for the real Quilt
4+
CloudFormation template so plan-time references (template_file / filemd5)
5+
resolve. Not a deployable Quilt stack.
6+
Resources:
7+
Placeholder:
8+
Type: AWS::CloudFormation::WaitConditionHandle

modules/quilt/tests/smoke/main.tf

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Wrapper root for the `quilt` module smoke tests.
2+
#
3+
# The smoke tests run against this wrapper rather than modules/quilt directly
4+
# because the quilt module's `stack` output embeds sensitive values (DB + admin
5+
# passwords); as a root module under test that trips a sensitive-output error.
6+
# The wrapper re-exposes only the non-sensitive stack name.
7+
#
8+
# The required_providers block below is also load-bearing for the tests:
9+
# declaring the provider requirement at the root is what lets the test's
10+
# mock_provider engage for the whole module tree. Without a direct provider
11+
# reference at the root, the mock never attaches and the child modules fall
12+
# back to real AWS credentials.
13+
terraform {
14+
required_providers {
15+
aws = {
16+
source = "hashicorp/aws"
17+
}
18+
}
19+
}
20+
21+
variable "create_new_vpc" {
22+
type = bool
23+
}
24+
25+
variable "internal" {
26+
type = bool
27+
default = false
28+
}
29+
30+
variable "vpc_id" {
31+
type = string
32+
default = null
33+
}
34+
35+
variable "intra_subnets" {
36+
type = list(string)
37+
default = null
38+
}
39+
40+
variable "private_subnets" {
41+
type = list(string)
42+
default = null
43+
}
44+
45+
variable "public_subnets" {
46+
type = list(string)
47+
default = null
48+
}
49+
50+
variable "user_security_group" {
51+
type = string
52+
default = null
53+
}
54+
55+
module "quilt" {
56+
source = "../../"
57+
58+
name = "quilt-test"
59+
parameters = {}
60+
template_file = "${path.module}/fixtures/quilt.yaml"
61+
62+
create_new_vpc = var.create_new_vpc
63+
internal = var.internal
64+
vpc_id = var.vpc_id
65+
intra_subnets = var.intra_subnets
66+
private_subnets = var.private_subnets
67+
public_subnets = var.public_subnets
68+
user_security_group = var.user_security_group
69+
}
70+
71+
output "stack_name" {
72+
value = module.quilt.stack.name
73+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Smoke tests for the public `quilt` module, run against the wrapper root in
2+
# this directory (see main.tf).
3+
#
4+
# Plan-only with the AWS provider mocked: no credentials, no infrastructure.
5+
# Exercises the full wiring (vpc + db + search + the CloudFormation stack), so
6+
# a change that breaks the public boundary or the vpc pass-through fails in CI.
7+
#
8+
# Assertions reference known inputs (the stack name), not mocked computed
9+
# attributes, whose generated values are intentionally arbitrary.
10+
11+
mock_provider "aws" {
12+
# slice(..., 0, 2) and the cidrsubnet math in the vpc submodule need at
13+
# least two AZ names; mocked collections are otherwise empty.
14+
mock_data "aws_availability_zones" {
15+
defaults = {
16+
names = ["us-east-1a", "us-east-1b", "us-east-1c"]
17+
}
18+
}
19+
}
20+
21+
run "new_vpc_plans" {
22+
command = plan
23+
variables {
24+
create_new_vpc = true
25+
internal = false
26+
}
27+
assert {
28+
condition = output.stack_name == "quilt-test"
29+
error_message = "The CloudFormation stack must be named after var.name"
30+
}
31+
}
32+
33+
run "existing_vpc_plans" {
34+
command = plan
35+
variables {
36+
create_new_vpc = false
37+
internal = false
38+
vpc_id = "vpc-00000000000000000"
39+
intra_subnets = ["subnet-intra-a", "subnet-intra-b"]
40+
private_subnets = ["subnet-priv-a", "subnet-priv-b"]
41+
public_subnets = ["subnet-pub-a", "subnet-pub-b"]
42+
user_security_group = "sg-00000000000000000"
43+
}
44+
assert {
45+
condition = output.stack_name == "quilt-test"
46+
error_message = "The CloudFormation stack must be named after var.name"
47+
}
48+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Characterization tests for the new-vs-existing VPC input validation.
2+
#
3+
# These run `terraform plan` with the AWS provider mocked, so they need no AWS
4+
# credentials and create no infrastructure. They pin the behavior of the
5+
# `configuration_error` precondition (see outputs.tf): valid input combinations
6+
# must plan cleanly, and contradictory combinations must fail fast.
7+
#
8+
# On a valid config the precondition passes and the `configuration_error`
9+
# output renders the requirement checklist with every line marked ✅; an
10+
# unmet requirement shows ❌ and a contradictory config trips the precondition
11+
# during plan.
12+
13+
mock_provider "aws" {
14+
# slice(..., 0, 2) and the cidrsubnet math need at least two AZ names.
15+
mock_data "aws_availability_zones" {
16+
defaults = {
17+
names = ["us-east-1a", "us-east-1b", "us-east-1c"]
18+
}
19+
}
20+
}
21+
22+
variables {
23+
name = "quilt-test"
24+
cidr = "10.0.0.0/16"
25+
}
26+
27+
# --- Valid: create a new VPC -------------------------------------------------
28+
29+
run "new_vpc_external_alb" {
30+
command = plan
31+
32+
variables {
33+
create_new_vpc = true
34+
internal = false
35+
existing_vpc_id = null
36+
existing_api_endpoint = null
37+
existing_intra_subnets = null
38+
existing_private_subnets = null
39+
existing_public_subnets = null
40+
existing_user_security_group = null
41+
existing_user_subnets = null
42+
}
43+
44+
assert {
45+
condition = strcontains(output.configuration_error, "create a new VPC")
46+
error_message = "New-VPC config must be checked against the new-network requirements"
47+
}
48+
49+
assert {
50+
condition = !strcontains(output.configuration_error, "")
51+
error_message = "A valid new-VPC config (internal = false) must satisfy every requirement"
52+
}
53+
}
54+
55+
run "new_vpc_internal_alb" {
56+
command = plan
57+
58+
variables {
59+
create_new_vpc = true
60+
internal = true
61+
existing_vpc_id = null
62+
existing_api_endpoint = null
63+
existing_intra_subnets = null
64+
existing_private_subnets = null
65+
existing_public_subnets = null
66+
existing_user_security_group = null
67+
existing_user_subnets = null
68+
}
69+
70+
assert {
71+
condition = !strcontains(output.configuration_error, "")
72+
error_message = "A valid new-VPC config (internal = true) must satisfy every requirement"
73+
}
74+
}
75+
76+
# --- Valid: use an existing VPC ----------------------------------------------
77+
78+
run "existing_vpc_external_alb" {
79+
command = plan
80+
81+
variables {
82+
create_new_vpc = false
83+
internal = false
84+
existing_vpc_id = "vpc-00000000000000000"
85+
existing_api_endpoint = null
86+
existing_intra_subnets = ["subnet-intra-a", "subnet-intra-b"]
87+
existing_private_subnets = ["subnet-priv-a", "subnet-priv-b"]
88+
existing_public_subnets = ["subnet-pub-a", "subnet-pub-b"]
89+
existing_user_security_group = "sg-00000000000000000"
90+
existing_user_subnets = null
91+
}
92+
93+
assert {
94+
condition = strcontains(output.configuration_error, "use an existing VPC")
95+
error_message = "Existing-VPC config must be checked against the existing-network requirements"
96+
}
97+
98+
assert {
99+
condition = !strcontains(output.configuration_error, "")
100+
error_message = "A valid existing-VPC config (internal = false) must satisfy every requirement"
101+
}
102+
103+
assert {
104+
condition = output.vpc_id == "vpc-00000000000000000"
105+
error_message = "Existing-VPC mode must surface the supplied existing_vpc_id"
106+
}
107+
}
108+
109+
run "existing_vpc_internal_alb" {
110+
command = plan
111+
112+
variables {
113+
create_new_vpc = false
114+
internal = true
115+
existing_vpc_id = "vpc-00000000000000000"
116+
existing_api_endpoint = "vpce-00000000000000000"
117+
existing_intra_subnets = ["subnet-intra-a", "subnet-intra-b"]
118+
existing_private_subnets = ["subnet-priv-a", "subnet-priv-b"]
119+
existing_public_subnets = null
120+
existing_user_security_group = "sg-00000000000000000"
121+
existing_user_subnets = ["subnet-user-a", "subnet-user-b"]
122+
}
123+
124+
assert {
125+
condition = !strcontains(output.configuration_error, "")
126+
error_message = "A valid existing-VPC config (internal = true) must satisfy every requirement"
127+
}
128+
}
129+
130+
# --- Invalid: contradictory input must fail fast -----------------------------
131+
132+
run "new_vpc_with_existing_id_is_rejected" {
133+
command = plan
134+
135+
variables {
136+
create_new_vpc = true
137+
internal = false
138+
existing_vpc_id = "vpc-00000000000000000"
139+
existing_api_endpoint = null
140+
existing_intra_subnets = null
141+
existing_private_subnets = null
142+
existing_public_subnets = null
143+
existing_user_security_group = null
144+
existing_user_subnets = null
145+
}
146+
147+
# create_new_vpc = true while supplying an existing VPC id satisfies neither
148+
# the new-network nor the existing-network requirement set.
149+
expect_failures = [output.configuration_error]
150+
}
151+
152+
run "existing_vpc_missing_inputs_is_rejected" {
153+
command = plan
154+
155+
variables {
156+
create_new_vpc = false
157+
internal = false
158+
existing_vpc_id = "vpc-00000000000000000"
159+
existing_api_endpoint = null
160+
existing_intra_subnets = null
161+
existing_private_subnets = null
162+
existing_public_subnets = null
163+
existing_user_security_group = null
164+
existing_user_subnets = null
165+
}
166+
167+
# create_new_vpc = false without the required existing_* inputs is incomplete.
168+
expect_failures = [output.configuration_error]
169+
}

0 commit comments

Comments
 (0)