rdev/internal/adapter/templates/provider_test.go
jordan 8282d60c69 feat: implement composable monorepo template system with component architecture
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>
2026-01-31 19:11:42 -07:00

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)
}