mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
Scaffolds the `coderd/templatebuilder` package for the guided template builder ([DEVEX-272](https://linear.app/codercom/issue/DEVEX-272), [RFC](https://www.notion.so/coderhq/RFC-Guided-Template-Creation-Workflow-342d579be59280dfbf8eea2e5006dbda)). Adds the module catalog types and `go:embed` wiring that the template builder endpoints will use: - `codersdk.TemplateBuilderModule`, `TemplateBuilderModuleVariable`, and related types matching the RFC schema - Internal `ModuleManifest` type with `go:embed` wiring to bundle `module.json` files from `coderd/templatebuilder/modules/` - `LoadModules()` with defensive copy, unexported `parseModulesFromFS(fs.FS)` for test isolation, `ToSDK()` conversion - Real `code-server` module manifest as the first catalog entry - Strict validation: ID uniqueness, version non-empty, variable type/name validation, `DisallowUnknownFields`, and requiring `module.json` in every module directory - Tests via internal `catalog_internal_test.go` (for `parseModulesFromFS` with `fstest.MapFS` fixtures) and external `catalog_test.go` (for `LoadModules` and `ToSDK`), covering multi-module parsing, all variable types, validation errors, nil-slice normalization, and full SDK field assertions > [!NOTE] > Generated with [Coder Agents](https://coder.com/agents) by @jeremyruppel --------- Co-authored-by: McKayla はな <mckayla@hey.com>
264 lines
6.2 KiB
Go
264 lines
6.2 KiB
Go
package templatebuilder
|
|
|
|
import (
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestParseModulesFromFS(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("ValidManifest", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/mymod/module.json": &fstest.MapFile{
|
|
Data: []byte(`{
|
|
"id": "mymod",
|
|
"display_name": "My Module",
|
|
"description": "A test module.",
|
|
"icon": "/icons/mymod.svg",
|
|
"category": "IDE",
|
|
"tags": ["ide"],
|
|
"compatible_os": ["linux"],
|
|
"conflicts_with": ["other"],
|
|
"pinned_version": "1.2.3",
|
|
"variables": [
|
|
{
|
|
"name": "agent_id",
|
|
"type": "string",
|
|
"description": "The Coder agent ID.",
|
|
"required": true,
|
|
"sensitive": false,
|
|
"computed": true
|
|
},
|
|
{
|
|
"name": "port",
|
|
"type": "number",
|
|
"description": "Port number.",
|
|
"default": 8080,
|
|
"required": false,
|
|
"sensitive": false,
|
|
"computed": false
|
|
},
|
|
{
|
|
"name": "enable_debug",
|
|
"type": "bool",
|
|
"description": "Enable debug mode.",
|
|
"required": false,
|
|
"sensitive": false,
|
|
"computed": false
|
|
},
|
|
{
|
|
"name": "api_key",
|
|
"type": "string",
|
|
"description": "Secret API key.",
|
|
"required": true,
|
|
"sensitive": true,
|
|
"computed": false
|
|
}
|
|
]
|
|
}`),
|
|
},
|
|
}
|
|
|
|
modules, err := parseModulesFromFS(fsys)
|
|
require.NoError(t, err)
|
|
require.Len(t, modules, 1)
|
|
|
|
m := modules[0]
|
|
require.Equal(t, "mymod", m.ID)
|
|
require.Equal(t, "My Module", m.DisplayName)
|
|
require.Equal(t, "A test module.", m.Description)
|
|
require.Equal(t, "/icons/mymod.svg", m.Icon)
|
|
require.Equal(t, "IDE", m.Category)
|
|
require.Equal(t, []string{"ide"}, m.Tags)
|
|
require.Equal(t, []string{"linux"}, m.CompatibleOS)
|
|
require.Equal(t, []string{"other"}, m.ConflictsWith)
|
|
require.Equal(t, "1.2.3", m.PinnedVersion)
|
|
require.Len(t, m.Variables, 4)
|
|
|
|
// Verify variable types parsed correctly.
|
|
require.Equal(t, "string", m.Variables[0].Type)
|
|
require.Equal(t, "number", m.Variables[1].Type)
|
|
require.Equal(t, "bool", m.Variables[2].Type)
|
|
|
|
// Verify computed and sensitive fields.
|
|
require.True(t, m.Variables[0].Computed)
|
|
require.True(t, m.Variables[3].Sensitive)
|
|
|
|
// Verify default pointer.
|
|
require.Nil(t, m.Variables[0].Default)
|
|
require.NotNil(t, m.Variables[1].Default)
|
|
require.JSONEq(t, "8080", string(m.Variables[1].Default))
|
|
})
|
|
|
|
t.Run("MultipleModules", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/alpha/module.json": &fstest.MapFile{
|
|
Data: []byte(`{"id": "alpha", "pinned_version": "1.0.0"}`),
|
|
},
|
|
"modules/beta/module.json": &fstest.MapFile{
|
|
Data: []byte(`{"id": "beta", "pinned_version": "2.0.0"}`),
|
|
},
|
|
}
|
|
|
|
modules, err := parseModulesFromFS(fsys)
|
|
require.NoError(t, err)
|
|
require.Len(t, modules, 2)
|
|
|
|
ids := []string{modules[0].ID, modules[1].ID}
|
|
require.Contains(t, ids, "alpha")
|
|
require.Contains(t, ids, "beta")
|
|
})
|
|
|
|
t.Run("EmptyCatalog", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/.keep": &fstest.MapFile{Data: []byte{}},
|
|
}
|
|
|
|
modules, err := parseModulesFromFS(fsys)
|
|
require.NoError(t, err)
|
|
require.Empty(t, modules)
|
|
})
|
|
|
|
t.Run("RejectsDirWithoutManifest", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/nomod/readme.txt": &fstest.MapFile{Data: []byte("hi")},
|
|
}
|
|
|
|
_, err := parseModulesFromFS(fsys)
|
|
require.ErrorContains(t, err, "read nomod/module.json")
|
|
})
|
|
|
|
t.Run("RejectsEmptyID", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/bad/module.json": &fstest.MapFile{
|
|
Data: []byte(`{"id": "", "pinned_version": "1.0.0"}`),
|
|
},
|
|
}
|
|
|
|
_, err := parseModulesFromFS(fsys)
|
|
require.ErrorContains(t, err, "empty id")
|
|
})
|
|
|
|
t.Run("RejectsEmptyPinnedVersion", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/bad/module.json": &fstest.MapFile{
|
|
Data: []byte(`{"id": "bad", "pinned_version": ""}`),
|
|
},
|
|
}
|
|
|
|
_, err := parseModulesFromFS(fsys)
|
|
require.ErrorContains(t, err, "empty pinned_version")
|
|
})
|
|
|
|
t.Run("RejectsDuplicateID", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/a/module.json": &fstest.MapFile{
|
|
Data: []byte(`{"id": "dupe", "pinned_version": "1.0.0"}`),
|
|
},
|
|
"modules/b/module.json": &fstest.MapFile{
|
|
Data: []byte(`{"id": "dupe", "pinned_version": "2.0.0"}`),
|
|
},
|
|
}
|
|
|
|
_, err := parseModulesFromFS(fsys)
|
|
require.ErrorContains(t, err, "duplicate module id")
|
|
})
|
|
|
|
t.Run("RejectsUnknownVariableType", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/bad/module.json": &fstest.MapFile{
|
|
Data: []byte(`{
|
|
"id": "bad",
|
|
"pinned_version": "1.0.0",
|
|
"variables": [{"name": "x", "type": "list"}]
|
|
}`),
|
|
},
|
|
}
|
|
|
|
_, err := parseModulesFromFS(fsys)
|
|
require.ErrorContains(t, err, `unknown type "list"`)
|
|
})
|
|
|
|
t.Run("RejectsUnknownField", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/bad/module.json": &fstest.MapFile{
|
|
Data: []byte(`{"id": "bad", "pinned_version": "1.0.0", "dispaly_name": "typo"}`),
|
|
},
|
|
}
|
|
|
|
_, err := parseModulesFromFS(fsys)
|
|
require.ErrorContains(t, err, "decode")
|
|
})
|
|
|
|
t.Run("RejectsEmptyVariableName", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/bad/module.json": &fstest.MapFile{
|
|
Data: []byte(`{
|
|
"id": "bad",
|
|
"pinned_version": "1.0.0",
|
|
"variables": [{"name": "", "type": "string"}]
|
|
}`),
|
|
},
|
|
}
|
|
|
|
_, err := parseModulesFromFS(fsys)
|
|
require.ErrorContains(t, err, "empty name")
|
|
})
|
|
|
|
t.Run("RejectsDuplicateVariableName", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/bad/module.json": &fstest.MapFile{
|
|
Data: []byte(`{
|
|
"id": "bad",
|
|
"pinned_version": "1.0.0",
|
|
"variables": [
|
|
{"name": "x", "type": "string"},
|
|
{"name": "x", "type": "number"}
|
|
]
|
|
}`),
|
|
},
|
|
}
|
|
|
|
_, err := parseModulesFromFS(fsys)
|
|
require.ErrorContains(t, err, "duplicate variable name")
|
|
})
|
|
|
|
t.Run("RejectsInvalidJSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fsys := fstest.MapFS{
|
|
"modules/bad/module.json": &fstest.MapFile{
|
|
Data: []byte(`{not json`),
|
|
},
|
|
}
|
|
|
|
_, err := parseModulesFromFS(fsys)
|
|
require.ErrorContains(t, err, "decode")
|
|
})
|
|
}
|