rdev/internal/service/component_updates_test.go
jordan 9f957d6e75
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix(templates): harden component CI steps and compile regexes
- Add --connect-timeout 10 and --max-time 15 to all verify step curl
  calls to prevent hanging on registry health checks
- Fix cli template: depends_on [deps] -> [preflight] for consistency
- Add cross-reference comment to service template about verify logic
  being replicated across all 5 component templates
- Document component CI step rules in composable-monorepo.md
- Compile regexes at package level instead of per-call in
  component_updates.go
- Add component_updates_test.go

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 19:36:23 -07:00

278 lines
6.9 KiB
Go

package service
import (
"strings"
"testing"
)
func TestExtractDeployStepName(t *testing.T) {
tests := []struct {
name string
stepYaml string
want string
}{
{
name: "service template with deploy step",
stepYaml: `build-studio-api:
depends_on: [preflight]
image: woodpeckerci/plugin-kaniko
deploy-studio-api:
depends_on: [verify-studio-api]
image: bitnami/kubectl:latest`,
want: "deploy-studio-api",
},
{
name: "worker template with deploy step",
stepYaml: `build-processor:
depends_on: [preflight]
deploy-processor:
image: bitnami/kubectl:latest`,
want: "deploy-processor",
},
{
name: "CLI template without deploy step",
stepYaml: `build-mytool:
depends_on: [deps]
image: golang:1.25-alpine
commands:
- go build ./cmd`,
want: "",
},
{
name: "empty input",
stepYaml: "",
want: "",
},
{
name: "hyphenated component name",
stepYaml: `deploy-my-cool-service:
image: bitnami/kubectl:latest`,
want: "deploy-my-cool-service",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := extractDeployStepName(tc.stepYaml)
if got != tc.want {
t.Errorf("extractDeployStepName() = %q, want %q", got, tc.want)
}
})
}
}
func TestAddBuildCompleteDep(t *testing.T) {
baseYml := `steps:
preflight:
depends_on: [deps]
image: alpine/curl
# COMPONENT_STEPS_BELOW
build-complete:
depends_on: [preflight] # BUILD_COMPLETE_DEPS
image: alpine:3.19
commands:
- echo "All component builds complete"`
tests := []struct {
name string
yml string
stepName string
wantDeps string
}{
{
name: "first component added",
yml: baseYml,
stepName: "deploy-studio-api",
wantDeps: "depends_on: [preflight, deploy-studio-api] # BUILD_COMPLETE_DEPS",
},
{
name: "second component added",
yml: strings.Replace(baseYml,
"depends_on: [preflight] # BUILD_COMPLETE_DEPS",
"depends_on: [preflight, deploy-studio-api] # BUILD_COMPLETE_DEPS", 1),
stepName: "deploy-studio-ui",
wantDeps: "depends_on: [preflight, deploy-studio-api, deploy-studio-ui] # BUILD_COMPLETE_DEPS",
},
{
name: "duplicate detection",
yml: strings.Replace(baseYml, "depends_on: [preflight] # BUILD_COMPLETE_DEPS", "depends_on: [preflight, deploy-studio-api] # BUILD_COMPLETE_DEPS", 1),
stepName: "deploy-studio-api",
wantDeps: "depends_on: [preflight, deploy-studio-api] # BUILD_COMPLETE_DEPS",
},
{
name: "missing marker returns unchanged",
yml: "steps:\n build-complete:\n image: alpine:3.19",
stepName: "deploy-foo",
wantDeps: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := addBuildCompleteDep(tc.yml, tc.stepName)
if tc.wantDeps == "" {
// Expect no change
if got != tc.yml {
t.Errorf("expected unchanged yml, but got modification")
}
return
}
if !strings.Contains(got, tc.wantDeps) {
t.Errorf("result does not contain expected depends_on line\nwant: %s\ngot:\n%s", tc.wantDeps, got)
}
})
}
}
func TestAddBuildCompleteDep_PreservesIndentation(t *testing.T) {
yml := `steps:
build-complete:
depends_on: [preflight] # BUILD_COMPLETE_DEPS
image: alpine:3.19`
got := addBuildCompleteDep(yml, "deploy-foo")
// The indentation (8 spaces) should be preserved
want := " depends_on: [preflight, deploy-foo] # BUILD_COMPLETE_DEPS"
if !strings.Contains(got, want) {
t.Errorf("indentation not preserved\nwant line: %q\ngot:\n%s", want, got)
}
}
func TestUpdateWoodpeckerYml_WiresBuildCompleteDeps(t *testing.T) {
skeleton := `steps:
preflight:
depends_on: [deps]
image: alpine/curl
# COMPONENT_STEPS_BELOW
# Do not remove the marker above
build-complete:
depends_on: [preflight] # BUILD_COMPLETE_DEPS
image: alpine:3.19
commands:
- echo "All component builds complete"`
stepYaml := `build-studio-api:
depends_on: [preflight]
image: woodpeckerci/plugin-kaniko
verify-studio-api:
depends_on: [build-studio-api]
image: alpine/curl
deploy-studio-api:
depends_on: [verify-studio-api]
image: bitnami/kubectl:latest`
svc := &ComponentService{}
result := svc.updateWoodpeckerYml(skeleton, stepYaml)
// Verify step YAML was inserted after marker
if !strings.Contains(result, " build-studio-api:") {
t.Error("component build step not inserted")
}
if !strings.Contains(result, " verify-studio-api:") {
t.Error("component verify step not inserted")
}
if !strings.Contains(result, " deploy-studio-api:") {
t.Error("component deploy step not inserted")
}
// Verify build-complete depends_on was updated
if !strings.Contains(result, "depends_on: [preflight, deploy-studio-api] # BUILD_COMPLETE_DEPS") {
t.Errorf("build-complete depends_on not updated\ngot:\n%s", result)
}
// Verify marker is preserved for future components
if !strings.Contains(result, "# COMPONENT_STEPS_BELOW") {
t.Error("COMPONENT_STEPS_BELOW marker was removed")
}
}
func TestUpdateWoodpeckerYml_TwoComponents(t *testing.T) {
skeleton := `steps:
preflight:
depends_on: [deps]
# COMPONENT_STEPS_BELOW
build-complete:
depends_on: [preflight] # BUILD_COMPLETE_DEPS
image: alpine:3.19`
step1 := `build-studio-api:
depends_on: [preflight]
verify-studio-api:
depends_on: [build-studio-api]
deploy-studio-api:
depends_on: [verify-studio-api]`
step2 := `build-studio-ui:
depends_on: [preflight]
verify-studio-ui:
depends_on: [build-studio-ui]
deploy-studio-ui:
depends_on: [verify-studio-ui]`
svc := &ComponentService{}
// Add first component
result := svc.updateWoodpeckerYml(skeleton, step1)
// Add second component
result = svc.updateWoodpeckerYml(result, step2)
// Both deploy steps should be in build-complete depends_on
if !strings.Contains(result, "depends_on: [preflight, deploy-studio-api, deploy-studio-ui] # BUILD_COMPLETE_DEPS") {
t.Errorf("build-complete doesn't depend on both deploy steps\ngot:\n%s", result)
}
}
func TestUpdateWoodpeckerYml_CLISkipsBuildCompleteDep(t *testing.T) {
skeleton := `steps:
# COMPONENT_STEPS_BELOW
build-complete:
depends_on: [preflight] # BUILD_COMPLETE_DEPS
image: alpine:3.19`
cliStep := `build-mytool:
depends_on: [deps]
image: golang:1.25-alpine
commands:
- go build ./cmd`
svc := &ComponentService{}
result := svc.updateWoodpeckerYml(skeleton, cliStep)
// CLI has no deploy step, so build-complete should stay unchanged
if !strings.Contains(result, "depends_on: [preflight] # BUILD_COMPLETE_DEPS") {
t.Errorf("build-complete was modified for CLI component (no deploy step)\ngot:\n%s", result)
}
}
func TestUpdateWoodpeckerYml_MissingMarker(t *testing.T) {
noMarker := `steps:
build:
image: golang:1.25`
svc := &ComponentService{}
result := svc.updateWoodpeckerYml(noMarker, "deploy-foo:\n image: test")
if result != noMarker {
t.Error("expected unchanged yml when marker is missing")
}
}