Files
coder/coderd/templatebuilder/validate_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

170 lines
3.8 KiB
Go

package templatebuilder
import (
"encoding/json"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestIsSimpleJSONValue(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw json.RawMessage
want bool
}{
{"String", json.RawMessage(`"hello"`), true},
{"EmptyString", json.RawMessage(`""`), true},
{"True", json.RawMessage(`true`), true},
{"False", json.RawMessage(`false`), true},
{"Null", json.RawMessage(`null`), true},
{"PositiveInt", json.RawMessage(`42`), true},
{"NegativeInt", json.RawMessage(`-1`), true},
{"Float", json.RawMessage(`3.14`), true},
{"Array", json.RawMessage(`[1,2]`), false},
{"Object", json.RawMessage(`{"a":1}`), false},
{"Empty", json.RawMessage(``), false},
{"Nil", nil, false},
{"MalformedString", json.RawMessage(`"unclosed`), false},
{"MalformedBool", json.RawMessage(`truesomething`), false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := isSimpleJSONValue(tc.raw)
require.Equal(t, tc.want, got)
})
}
}
func TestValidateStringValue(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value string
wantErr string
}{
{"ValidEmpty", "", ""},
{"ValidSimple", "hello", ""},
{"ValidPath", "/home/coder", ""},
{"ValidURL", "https://github.com/coder/coder", ""},
{"ValidWithQuotes", `say "hi"`, ""},
{"ValidWithNewlines", "line\nbreak", ""},
{"ValidWithBackslash", `path\to\file`, ""},
{"RejectedHCLInterpolation", "${var.foo}", "interpolation"},
{"RejectedHCLDirective", "%{if true}yes%{endif}", "interpolation"},
{"RejectedOverlong", strings.Repeat("a", maxStringValueLen+1), "maximum length"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := validateStringValue(tc.value)
if tc.wantErr == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.wantErr)
}
})
}
}
func TestValidateNumberValue(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value string
wantErr bool
}{
{"Zero", "0", false},
{"Positive", "42", false},
{"Negative", "-1", false},
{"Decimal", "3.14", false},
{"NegativeDecimal", "-0.5", false},
{"Scientific", "1e10", true},
{"Hex", "0x1F", true},
{"Underscore", "1_000", true},
{"Expression", "1 + 1", true},
{"Empty", "", true},
{"Letters", "abc", true},
{"TrailingDot", "1.", true},
{"LeadingDot", ".5", true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := validateNumberValue(tc.value)
if tc.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestValidateBoolValue(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value string
wantErr bool
}{
{"True", "true", false},
{"False", "false", false},
{"UpperTrue", "True", true},
{"UpperFALSE", "FALSE", true},
{"QuotedTrue", `"true"`, true},
{"One", "1", true},
{"Yes", "yes", true},
{"Empty", "", true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := validateBoolValue(tc.value)
if tc.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestValidateVariableValue(t *testing.T) {
t.Parallel()
t.Run("NullAcceptedForAllTypes", func(t *testing.T) {
t.Parallel()
for _, typ := range []string{"string", "number", "bool"} {
t.Run(typ, func(t *testing.T) {
t.Parallel()
v := ModuleVariable{Name: "test", Type: typ}
require.NoError(t, validateVariableValue(v, "null"))
})
}
})
t.Run("UnsupportedTypeRejected", func(t *testing.T) {
t.Parallel()
v := ModuleVariable{Name: "test", Type: "list"}
err := validateVariableValue(v, "val")
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported variable type")
})
}