Files
coder/coderd/templatebuilder/compose_internal_test.go
T
Jeremy Ruppel 6f0b81cfbc feat(coderd/templatebuilder): validation extraction, static file bundling, Windows OS (#26633)
Extract validation helpers to a dedicated file, add static file bundling
for base templates, and add Windows OS support.

**Commit 1: Extract validation to `validate.go`**
Move `validateVariableValue`, `validateStringValue`,
`validateNumberValue`, `validateBoolValue`, `toHCLLiteral`, `hclQuote`,
and `isSimpleJSONValue` from `compose.go` into `validate.go`.
Corresponding tests move to `validate_internal_test.go`. This keeps
`compose.go` focused on the compose pipeline.

**Commit 2: Static file bundling**
Add `StaticFiles` field to `ComposeResult` and a `collectStaticFiles`
helper that walks the base template FS to collect non-template files
(e.g. cloud-init `.tftpl` inputs). `BundleTar` now writes these files
into the output archive in sorted order for deterministic output. This
fixes `aws-linux`, whose cloud-init files were embedded but never
included in the tar.

**Commit 3: Windows OS support**
Add `BaseOSWindows` constant and register `"windows"` in `validBaseOS`
so that base templates with `os="windows"` can be loaded and used for
module compatibility filtering.

> [!NOTE]
> This PR was authored by Coder Agents on behalf of @jeremyruppel.
2026-06-30 18:08:02 -04:00

158 lines
4.6 KiB
Go

package templatebuilder
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestMergeModuleVariables(t *testing.T) {
t.Parallel()
manifest := ModuleManifest{
Variables: []ModuleVariable{
{Name: "agent_id", Type: "string", Computed: true},
{Name: "api_key", Type: "string", Sensitive: true},
{Name: "port", Type: "number", Default: json.RawMessage(`13337`)},
{Name: "enabled", Type: "bool", Default: json.RawMessage(`false`)},
{Name: "optional_no_default", Type: "string", Required: false},
{Name: "required_no_default", Type: "string", Required: true},
},
}
requiredVars := map[string]string{
"required_no_default": "value",
}
t.Run("DefaultsApplied", func(t *testing.T) {
t.Parallel()
merged, err := mergeModuleVariables(manifest, requiredVars)
require.NoError(t, err)
require.Equal(t, "13337", merged["port"])
require.Equal(t, "false", merged["enabled"])
})
t.Run("ComputedAndSensitiveSkipped", func(t *testing.T) {
t.Parallel()
merged, err := mergeModuleVariables(manifest, requiredVars)
require.NoError(t, err)
require.NotContains(t, merged, "agent_id")
require.NotContains(t, merged, "api_key")
})
t.Run("NonRequiredWithoutDefaultGetsNull", func(t *testing.T) {
t.Parallel()
merged, err := mergeModuleVariables(manifest, requiredVars)
require.NoError(t, err)
require.Equal(t, "null", merged["optional_no_default"])
})
t.Run("RequiredWithoutDefaultIsRequired", func(t *testing.T) {
t.Parallel()
_, err := mergeModuleVariables(manifest, nil)
require.Error(t, err)
require.Contains(t, err.Error(), `variable "required_no_default"`)
require.Contains(t, err.Error(), "is required")
})
t.Run("CallerOverridesDefault", func(t *testing.T) {
t.Parallel()
merged, err := mergeModuleVariables(manifest, map[string]string{
"port": "9999",
"required_no_default": "value",
})
require.NoError(t, err)
require.Equal(t, "9999", merged["port"])
})
t.Run("CallerProvidesRequired", func(t *testing.T) {
t.Parallel()
merged, err := mergeModuleVariables(manifest, map[string]string{
"required_no_default": "value",
})
require.NoError(t, err)
require.Equal(t, `"value"`, merged["required_no_default"])
})
t.Run("UnknownKeyRejected", func(t *testing.T) {
t.Parallel()
_, err := mergeModuleVariables(manifest, map[string]string{
"nonexistent": `"val"`,
})
require.Error(t, err)
require.Contains(t, err.Error(), `unknown variable "nonexistent"`)
})
t.Run("ComputedKeyRejected", func(t *testing.T) {
t.Parallel()
_, err := mergeModuleVariables(manifest, map[string]string{
"agent_id": `"injected"`,
})
require.Error(t, err)
require.Contains(t, err.Error(), `unknown variable "agent_id"`)
})
t.Run("SensitiveKeyRejected", func(t *testing.T) {
t.Parallel()
_, err := mergeModuleVariables(manifest, map[string]string{
"api_key": `"secret"`,
})
require.Error(t, err)
require.Contains(t, err.Error(), `unknown variable "api_key"`)
})
t.Run("InvalidNumberValueRejected", func(t *testing.T) {
t.Parallel()
_, err := mergeModuleVariables(manifest, map[string]string{
"port": "abc",
})
require.Error(t, err)
require.Contains(t, err.Error(), `variable "port"`)
require.Contains(t, err.Error(), "invalid number value")
})
t.Run("InvalidBoolValueRejected", func(t *testing.T) {
t.Parallel()
_, err := mergeModuleVariables(manifest, map[string]string{
"enabled": "yes",
})
require.Error(t, err)
require.Contains(t, err.Error(), `variable "enabled"`)
require.Contains(t, err.Error(), "invalid bool value")
})
t.Run("InvalidStringValueRejected", func(t *testing.T) {
t.Parallel()
_, err := mergeModuleVariables(manifest, map[string]string{
"optional_no_default": "${var.foo}",
})
require.Error(t, err)
require.Contains(t, err.Error(), `variable "optional_no_default"`)
require.Contains(t, err.Error(), "interpolation")
})
t.Run("NullAcceptedForAnyType", func(t *testing.T) {
t.Parallel()
merged, err := mergeModuleVariables(manifest, map[string]string{
"port": "null",
"enabled": "null",
"optional_no_default": "null",
"required_no_default": "null",
})
require.NoError(t, err)
require.Equal(t, "null", merged["port"])
require.Equal(t, "null", merged["enabled"])
require.Equal(t, "null", merged["optional_no_default"])
})
t.Run("EmptyCallerVarsUsesDefaults", func(t *testing.T) {
t.Parallel()
merged, err := mergeModuleVariables(manifest, map[string]string{
"required_no_default": "value",
})
require.NoError(t, err)
require.Equal(t, "13337", merged["port"])
})
}