diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b2b4d1e9..44ac9ab01 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ The Coder Registry is a collection of Terraform modules and templates for Coder ### Install Dependencies -Install Bun: +Install Bun (for formatting and scripts): ```bash curl -fsSL https://bun.sh/install | bash @@ -124,19 +124,23 @@ This script generates: - Accurate description and usage examples - Correct icon path (usually `../../../../.icons/your-icon.svg`) - Proper tags that describe your module -3. **Create `main.test.ts`** to test your module +3. **Create at least one `.tftest.hcl`** to test your module with `terraform test` 4. **Add any scripts** or additional files your module needs ### 4. Test and Submit ```bash -# Test your module -bun test -t 'module-name' +# Test your module (from the module directory) +terraform init -upgrade +terraform test -verbose + +# Or run all tests in the repo +./scripts/terraform_test_all.sh # Format code -bun fmt +bun run fmt -# Commit and create PR +# Commit and create PR (do not push to main directly) git add . git commit -m "Add [module-name] module" git push origin your-branch @@ -335,11 +339,12 @@ coder templates push test-[template-name] -d . ### 2. Test Your Changes ```bash -# Test a specific module -bun test -t 'module-name' +# Test a specific module (from the module directory) +terraform init -upgrade +terraform test -verbose # Test all modules -bun test +./scripts/terraform_test_all.sh ``` ### 3. Maintain Backward Compatibility @@ -388,7 +393,7 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template= ### Every Module Must Have - `main.tf` - Terraform code -- `main.test.ts` - Working tests +- One or more `.tftest.hcl` files - Working tests with `terraform test` - `README.md` - Documentation with frontmatter ### Every Template Must Have @@ -488,6 +493,6 @@ When reporting bugs, include: 2. **No tests** or broken tests 3. **Hardcoded values** instead of variables 4. **Breaking changes** without defaults -5. **Not running** `bun fmt` before submitting +5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting Happy contributing! 🚀 diff --git a/MAINTAINER.md b/MAINTAINER.md index 223265746..9959e59c8 100644 --- a/MAINTAINER.md +++ b/MAINTAINER.md @@ -18,9 +18,9 @@ sudo apt install golang-go Check that PRs have: -- [ ] All required files (`main.tf`, `main.test.ts`, `README.md`) +- [ ] All required files (`main.tf`, `README.md`, at least one `.tftest.hcl`) - [ ] Proper frontmatter in README -- [ ] Working tests (`bun test`) +- [ ] Working tests (`terraform test`) - [ ] Formatted code (`bun run fmt`) - [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`) diff --git a/examples/modules/MODULE_NAME.tftest.hcl b/examples/modules/MODULE_NAME.tftest.hcl new file mode 100644 index 000000000..6f11666b5 --- /dev/null +++ b/examples/modules/MODULE_NAME.tftest.hcl @@ -0,0 +1,21 @@ +run "plan_with_required_vars" { + command = plan + + variables { + agent_id = "example-agent-id" + } +} + +run "app_url_uses_port" { + command = plan + + variables { + agent_id = "example-agent-id" + port = 19999 + } + + assert { + condition = resource.coder_app.MODULE_NAME.url == "http://localhost:19999" + error_message = "Expected MODULE_NAME app URL to include configured port" + } +} diff --git a/package.json b/package.json index 7ca9f2ec2..c2f9ff694 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "fmt": "bun x prettier --write **/*.sh **/*.ts **/*.md *.md && terraform fmt -recursive -diff", "fmt:ci": "bun x prettier --check **/*.sh **/*.ts **/*.md *.md && terraform fmt -check -recursive -diff", "terraform-validate": "./scripts/terraform_validate.sh", - "test": "bun test", + "test": "./scripts/terraform_test_all.sh", "update-version": "./update-version.sh" }, "devDependencies": { diff --git a/registry/coder/modules/code-server/code-server.tftest.hcl b/registry/coder/modules/code-server/code-server.tftest.hcl new file mode 100644 index 000000000..ebbb71755 --- /dev/null +++ b/registry/coder/modules/code-server/code-server.tftest.hcl @@ -0,0 +1,50 @@ +run "required_vars" { + command = plan + + variables { + agent_id = "foo" + } +} + +run "offline_and_use_cached_conflict" { + command = plan + + variables { + agent_id = "foo" + use_cached = true + offline = true + } + + expect_failures = [ + resource.coder_script.code-server + ] +} + +run "offline_disallows_extensions" { + command = plan + + variables { + agent_id = "foo" + offline = true + extensions = ["ms-python.python", "golang.go"] + } + + expect_failures = [ + resource.coder_script.code-server + ] +} + +run "url_with_folder_query" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder/project" + port = 13337 + } + + assert { + condition = resource.coder_app.code-server.url == "http://localhost:13337/?folder=%2Fhome%2Fcoder%2Fproject" + error_message = "coder_app URL must include encoded folder query param" + } +} diff --git a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl new file mode 100644 index 000000000..e5c00a78e --- /dev/null +++ b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl @@ -0,0 +1,131 @@ +run "requires_agent_and_folder" { + command = plan + + # Setting both required vars should plan + variables { + agent_id = "foo" + folder = "/home/coder" + } +} + +run "creates_parameter_when_default_empty_latest" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + major_version = "latest" + } + + # When default is empty, a coder_parameter should be created + assert { + condition = can(data.coder_parameter.jetbrains_ides[0].type) + error_message = "Expected data.coder_parameter.jetbrains_ides to exist when default is empty" + } +} + +run "no_apps_when_default_empty" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + } + + assert { + condition = length(resource.coder_app.jetbrains) == 0 + error_message = "Expected no coder_app resources when default is empty" + } +} + +run "single_app_when_default_GO" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + } + + assert { + condition = length(resource.coder_app.jetbrains) == 1 + error_message = "Expected exactly one coder_app when default contains GO" + } +} + +run "url_contains_required_params" { + command = apply + + variables { + agent_id = "test-agent-123" + folder = "/custom/project/path" + default = ["GO"] + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("jetbrains://gateway/coder", app.url)) > 0]) + error_message = "URL must contain jetbrains scheme" + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("&folder=/custom/project/path", app.url)) > 0]) + error_message = "URL must include folder path" + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("ide_product_code=GO", app.url)) > 0]) + error_message = "URL must include product code" + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("ide_build_number=", app.url)) > 0]) + error_message = "URL must include build number" + } +} + +run "includes_agent_name_when_set" { + command = apply + + variables { + agent_id = "test-agent-123" + agent_name = "main-agent" + folder = "/custom/project/path" + default = ["GO"] + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("&agent_name=main-agent", app.url)) > 0]) + error_message = "URL must include agent_name when provided" + } +} + +run "parameter_order_when_default_empty" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + coder_parameter_order = 5 + } + + assert { + condition = data.coder_parameter.jetbrains_ides[0].order == 5 + error_message = "Expected coder_parameter order to be set to 5" + } +} + +run "app_order_when_default_not_empty" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + coder_app_order = 10 + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.order == 10]) + error_message = "Expected coder_app order to be set to 10" + } +} diff --git a/registry/coder/modules/zed/zed.tftest.hcl b/registry/coder/modules/zed/zed.tftest.hcl new file mode 100644 index 000000000..508b65503 --- /dev/null +++ b/registry/coder/modules/zed/zed.tftest.hcl @@ -0,0 +1,40 @@ +run "default_output" { + command = apply + + variables { + agent_id = "foo" + } + + assert { + condition = output.zed_url == "zed://ssh/default.coder" + error_message = "zed_url did not match expected default URL" + } +} + +run "adds_folder" { + command = apply + + variables { + agent_id = "foo" + folder = "/foo/bar" + } + + assert { + condition = output.zed_url == "zed://ssh/default.coder/foo/bar" + error_message = "zed_url did not include provided folder path" + } +} + +run "adds_agent_name" { + command = apply + + variables { + agent_id = "foo" + agent_name = "myagent" + } + + assert { + condition = output.zed_url == "zed://ssh/myagent.default.default.coder" + error_message = "zed_url did not include agent_name in hostname" + } +} diff --git a/scripts/terraform_test_all.sh b/scripts/terraform_test_all.sh new file mode 100755 index 000000000..01258904d --- /dev/null +++ b/scripts/terraform_test_all.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Find all directories that contain any .tftest.hcl files and run terraform test in each + +run_dir() { + local dir="$1" + echo "==> Running terraform test in $dir" + (cd "$dir" && terraform init -upgrade -input=false -no-color > /dev/null && terraform test -no-color -verbose) +} + +mapfile -t test_dirs < <(find . -type f -name "*.tftest.hcl" -print0 | xargs -0 -I{} dirname {} | sort -u) + +if [[ ${#test_dirs[@]} -eq 0 ]]; then + echo "No .tftest.hcl tests found." + exit 0 +fi + +status=0 +for d in "${test_dirs[@]}"; do + if ! run_dir "$d"; then + status=1 + fi +done + +exit $status