Adds the composable monorepo template system that generates project skeletons with pluggable components (service, worker, app-react, app-astro, cli). Key changes: - Monorepo skeleton templates with shared pkg/, scripts/, and git hooks - Component templates (service, worker, app-react, app-astro, cli) with Dockerfiles, CI steps, and component.yaml manifests - Component domain model with validation and dependency resolution - Component handler endpoints for CRUD and composition - Template provider extended with BuildComposableProject and component assembly - Deployer extended with composable project deployment support - Handler timeout constants (TimeoutFastLookup through TimeoutLongRunning) - envutil package for centralized env var reads with defaults - api.DecodeJSON helper for standardized request body decoding - Standardized response helpers (WriteBadRequest, WriteNotFound, etc.) - Replaced fullstack-app cookbook with composable-app cookbook - Hardened handler timeouts, logging, and error responses across all handlers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
189 lines
6.8 KiB
Go
189 lines
6.8 KiB
Go
package templates
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestInterpolateVars(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
vars map[string]string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "single variable",
|
|
content: "Hello {{PROJECT_NAME}}!",
|
|
vars: map[string]string{
|
|
"PROJECT_NAME": "myapp",
|
|
},
|
|
expected: "Hello myapp!",
|
|
},
|
|
{
|
|
name: "multiple variables",
|
|
content: "Project {{PROJECT_NAME}} at {{DOMAIN}} from {{GIT_URL}}",
|
|
vars: map[string]string{
|
|
"PROJECT_NAME": "myapp",
|
|
"DOMAIN": "myapp.threesix.ai",
|
|
"GIT_URL": "https://git.example.com/org/myapp.git",
|
|
},
|
|
expected: "Project myapp at myapp.threesix.ai from https://git.example.com/org/myapp.git",
|
|
},
|
|
{
|
|
name: "no variables",
|
|
content: "Static content with no placeholders",
|
|
vars: map[string]string{},
|
|
expected: "Static content with no placeholders",
|
|
},
|
|
{
|
|
name: "variable not in map",
|
|
content: "Hello {{UNKNOWN_VAR}}!",
|
|
vars: map[string]string{
|
|
"PROJECT_NAME": "myapp",
|
|
},
|
|
expected: "Hello {{UNKNOWN_VAR}}!",
|
|
},
|
|
{
|
|
name: "empty vars map",
|
|
content: "Hello {{PROJECT_NAME}}!",
|
|
vars: nil,
|
|
expected: "Hello {{PROJECT_NAME}}!",
|
|
},
|
|
{
|
|
name: "repeated variable",
|
|
content: "{{NAME}} and {{NAME}} again",
|
|
vars: map[string]string{
|
|
"NAME": "test",
|
|
},
|
|
expected: "test and test again",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := interpolateVars(tt.content, tt.vars)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidTemplateName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"valid simple", "default", true},
|
|
{"valid with dash", "astro-landing", true},
|
|
{"valid with numbers", "go-api-v2", true},
|
|
{"empty", "", false},
|
|
{"starts with number", "123template", false},
|
|
{"starts with dash", "-invalid", false},
|
|
{"contains uppercase", "MyTemplate", false},
|
|
{"contains underscore", "my_template", false},
|
|
{"contains dot", "my.template", false},
|
|
{"contains slash", "my/template", false},
|
|
{"path traversal attempt", "../evil", false},
|
|
{"too long", "a" + string(make([]byte, 64)), false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isValidTemplateName(tt.input)
|
|
assert.Equal(t, tt.expected, result, "isValidTemplateName(%q)", tt.input)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListTemplateFiles(t *testing.T) {
|
|
// Test that default template exists and has expected files
|
|
files, err := listTemplateFiles("default")
|
|
require.NoError(t, err)
|
|
|
|
// Should have at least these core files
|
|
assert.Contains(t, files, ".woodpecker.yml", "default template should have .woodpecker.yml")
|
|
assert.Contains(t, files, "Dockerfile", "default template should have Dockerfile")
|
|
assert.Contains(t, files, "README.md", "default template should have README.md")
|
|
}
|
|
|
|
func TestListTemplateFiles_InvalidTemplate(t *testing.T) {
|
|
_, err := listTemplateFiles("nonexistent-template")
|
|
assert.Error(t, err, "should error for nonexistent template")
|
|
}
|
|
|
|
func TestAvailableTemplates(t *testing.T) {
|
|
// Verify all declared templates actually exist
|
|
for _, tmpl := range availableTemplates {
|
|
t.Run(tmpl.Name, func(t *testing.T) {
|
|
files, err := listTemplateFiles(tmpl.Name)
|
|
require.NoError(t, err, "template %s should exist", tmpl.Name)
|
|
assert.NotEmpty(t, files, "template %s should have files", tmpl.Name)
|
|
|
|
// Every template should have these core files
|
|
assert.Contains(t, files, ".woodpecker.yml", "template %s should have .woodpecker.yml", tmpl.Name)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListTemplateFiles_TmplExtensionStripped(t *testing.T) {
|
|
// go-api template has go.mod.tmpl which should be listed as go.mod
|
|
files, err := listTemplateFiles("go-api")
|
|
require.NoError(t, err)
|
|
|
|
// Should contain go.mod (not go.mod.tmpl)
|
|
assert.Contains(t, files, "go.mod", "go.mod.tmpl should be listed as go.mod")
|
|
|
|
// Should NOT contain the .tmpl extension
|
|
for _, f := range files {
|
|
assert.NotContains(t, f, ".tmpl", "file %s should not have .tmpl extension", f)
|
|
}
|
|
}
|
|
|
|
func TestSkeletonTemplate(t *testing.T) {
|
|
// Test that skeleton template exists and has expected files
|
|
files, err := listTemplateFiles("skeleton")
|
|
require.NoError(t, err)
|
|
|
|
// Should have the core monorepo files
|
|
assert.Contains(t, files, "CLAUDE.md", "skeleton template should have CLAUDE.md")
|
|
assert.Contains(t, files, "README.md", "skeleton template should have README.md")
|
|
assert.Contains(t, files, ".woodpecker.yml", "skeleton template should have .woodpecker.yml")
|
|
assert.Contains(t, files, "docker-compose.yml", "skeleton template should have docker-compose.yml")
|
|
assert.Contains(t, files, "go.work", "skeleton template should have go.work")
|
|
assert.Contains(t, files, "Procfile", "skeleton template should have Procfile")
|
|
assert.Contains(t, files, ".gitignore", "skeleton template should have .gitignore")
|
|
assert.Contains(t, files, ".golangci.yml", "skeleton template should have .golangci.yml")
|
|
|
|
// Should have scripts
|
|
assert.Contains(t, files, "scripts/dev.sh", "skeleton template should have scripts/dev.sh")
|
|
assert.Contains(t, files, "scripts/install.sh", "skeleton template should have scripts/install.sh")
|
|
assert.Contains(t, files, "scripts/quality.sh", "skeleton template should have scripts/quality.sh")
|
|
assert.Contains(t, files, "scripts/discover.sh", "skeleton template should have scripts/discover.sh")
|
|
|
|
// Should have .claude structure
|
|
assert.Contains(t, files, ".claude/settings.local.json", "skeleton template should have .claude/settings.local.json")
|
|
assert.Contains(t, files, ".claude/guides/local/setup.md", "skeleton template should have .claude/guides/local/setup.md")
|
|
assert.Contains(t, files, ".claude/guides/ops/deploying.md", "skeleton template should have .claude/guides/ops/deploying.md")
|
|
assert.Contains(t, files, ".claude/skills/code-review/SKILL.md", "skeleton template should have .claude/skills/code-review/SKILL.md")
|
|
|
|
// Should have component directory placeholders
|
|
assert.Contains(t, files, "services/.gitkeep", "skeleton template should have services/.gitkeep")
|
|
assert.Contains(t, files, "workers/.gitkeep", "skeleton template should have workers/.gitkeep")
|
|
assert.Contains(t, files, "apps/.gitkeep", "skeleton template should have apps/.gitkeep")
|
|
assert.Contains(t, files, "cli/.gitkeep", "skeleton template should have cli/.gitkeep")
|
|
|
|
// Should have pkg directory files
|
|
assert.Contains(t, files, "pkg/go.mod", "skeleton template should have pkg/go.mod")
|
|
assert.Contains(t, files, "pkg/README.md", "skeleton template should have pkg/README.md")
|
|
}
|
|
|
|
func TestSkeletonTemplateInfo(t *testing.T) {
|
|
// Verify skeleton template metadata
|
|
assert.Equal(t, "skeleton", skeletonTemplate.Name)
|
|
assert.Equal(t, "monorepo", skeletonTemplate.Stack)
|
|
assert.NotEmpty(t, skeletonTemplate.Description)
|
|
}
|