Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 243 additions & 2 deletions cmd/wfctl/config_migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ func TestConfigMigrate_NoArgs_PrintsUsage(t *testing.T) {
// TestConfigMigrate_DefaultWriterIsStderr verifies that the deprecation banner
// defaults to os.Stderr, so future changes that accidentally redirect it are caught.
func TestConfigMigrate_DefaultWriterIsStderr(t *testing.T) {
if migrateDeprecationWriter != os.Stderr {
t.Errorf("migrateDeprecationWriter default should be os.Stderr, got %T", migrateDeprecationWriter)
if migrateDeprecationWriter != defaultMigrateDeprecationWriter {
t.Errorf("migrateDeprecationWriter default should be defaultMigrateDeprecationWriter, got %T", migrateDeprecationWriter)
}
}

Expand Down Expand Up @@ -118,6 +118,19 @@ func TestConfigCommand_DispatchesMigrate(t *testing.T) {
}
}

func TestConfigCommand_EmbeddedCLIWiresConfigCommand(t *testing.T) {
embedded := string(wfctlConfigBytes)
for _, want := range []string{
"name: config",
"cmd-config:",
"command: config",
} {
if !strings.Contains(embedded, want) {
t.Fatalf("embedded wfctl config must wire config command, missing %q", want)
}
}
}

// TestConfigCommand_UnknownSubcommand verifies that wfctl config with an
// unknown sub-subcommand returns a clear error.
func TestConfigCommand_UnknownSubcommand(t *testing.T) {
Expand All @@ -129,3 +142,231 @@ func TestConfigCommand_UnknownSubcommand(t *testing.T) {
t.Errorf("expected 'unknown' in error, got: %v", err)
}
}

func TestConfigValidateAcceptsWfctlManifestAndLockfile(t *testing.T) {
dir := t.TempDir()
manifest := filepath.Join(dir, "wfctl.yaml")
lockfile := filepath.Join(dir, ".wfctl-lock.yaml")
if err := os.WriteFile(manifest, []byte(`version: 1
plugins:
- name: workflow-plugin-digitalocean
version: v1.0.13
source: github.com/GoCodeAlone/workflow-plugin-digitalocean
`), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(lockfile, []byte(`version: 1
generated_at: 2026-05-14T00:00:00Z
plugins:
workflow-plugin-digitalocean:
version: v1.0.13
source: github.com/GoCodeAlone/workflow-plugin-digitalocean
platforms:
darwin/arm64:
url: https://example.invalid/workflow-plugin-digitalocean_Darwin_arm64.tar.gz
sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
`), 0o600); err != nil {
t.Fatal(err)
}
if err := runConfig([]string{"validate", "--manifest", manifest, "--lock-file", lockfile}); err != nil {
t.Fatalf("wfctl config validate failed: %v", err)
}
}

func TestConfigValidateAcceptsPositionalManifestAndWarnsOnMissingDefaultLock(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
if err := os.WriteFile("wfctl.yaml", []byte(`version: 1
plugins:
- name: workflow-plugin-digitalocean
version: v1.0.13
`), 0o600); err != nil {
t.Fatal(err)
}
stderr := captureConfigValidateStderr(t, func() {
if err := runConfigValidate([]string{"wfctl.yaml"}); err != nil {
t.Fatalf("config validate: %v", err)
}
})
if !strings.Contains(stderr, "lockfile not found") {
t.Fatalf("stderr = %q, want missing lockfile warning", stderr)
}
}

func TestConfigValidateRejectsTooManyPositionals(t *testing.T) {
if err := runConfigValidate([]string{"one.yaml", "two.yaml"}); err == nil {
t.Fatal("expected too many positional args to fail")
}
}

func TestConfigValidateRejectsRuntimeWorkflowConfig(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "workflow.yaml")
if err := os.WriteFile(path, []byte(minimalConfig), 0o600); err != nil {
t.Fatal(err)
}
err := runConfig([]string{"validate", path, "--skip-lock"})
if err == nil {
t.Fatal("expected workflow runtime config to be rejected by wfctl config validate")
}
if !strings.Contains(err.Error(), "not a wfctl project manifest") {
t.Fatalf("error = %v, want project manifest guidance", err)
}
}

func TestValidateWfctlManifestFileReportsPluginProblems(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "wfctl.yaml")
if err := os.WriteFile(path, []byte(`version: 2
plugins:
- name: workflow-plugin-foo
version: ""
auth: {}
verify: {}
- name: workflow-plugin-foo
version: v1.0.0
- version: v1.0.0
`), 0o600); err != nil {
t.Fatal(err)
}
err := validateWfctlManifestFile(path)
if err == nil {
t.Fatal("expected invalid manifest to fail")
}
for _, want := range []string{"version: got 2 want 1", "duplicated", "version is required", "auth.env", "verify.identity", "name is required"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("error = %v, missing %q", err, want)
}
}
}

func TestValidateWfctlLockfileReportsPlatformProblems(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, ".wfctl-lock.yaml")
if err := os.WriteFile(path, []byte(`version: 2
plugins:
workflow-plugin-foo:
version: ""
platforms:
"":
url: not a url
sha256: abc
`), 0o600); err != nil {
t.Fatal(err)
}
err := validateWfctlLockfile(path)
if err == nil {
t.Fatal("expected invalid lockfile to fail")
}
for _, want := range []string{"version: got 2 want 1", "version is required", "empty platform", "url is invalid", "got length 3 want 64"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("error = %v, missing %q", err, want)
}
}
}

func TestValidateWfctlLockfilePreservesExplicitMissingLockError(t *testing.T) {
err := validateWfctlLockfile(filepath.Join(t.TempDir(), ".wfctl-lock.yaml"))
if !os.IsNotExist(err) {
t.Fatalf("error = %v, want os.ErrNotExist", err)
}
}

func TestRunValidateRejectsWfctlManifestWithGuidance(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "wfctl.yaml")
if err := os.WriteFile(path, []byte(`version: 1
plugins:
- name: workflow-plugin-digitalocean
version: v1.0.13
`), 0o600); err != nil {
t.Fatal(err)
}
err := runValidate([]string{path})
if err == nil {
t.Fatal("expected wfctl validate to reject wfctl.yaml")
}
if !strings.Contains(err.Error(), "wfctl config validate") {
t.Fatalf("error = %v, want guidance to wfctl config validate", err)
}
}

func TestIsLikelyWfctlProjectManifestClassifiesOnlyCanonicalProjectFiles(t *testing.T) {
dir := t.TempDir()
wfctl := filepath.Join(dir, ".wfctl.yaml")
if err := os.WriteFile(wfctl, []byte(`version: 1
plugins: []
`), 0o600); err != nil {
t.Fatal(err)
}
if !isLikelyWfctlProjectManifest(wfctl) {
t.Fatal(".wfctl.yaml with plugins should be classified as wfctl project manifest")
}
runtime := filepath.Join(dir, "wfctl.yaml")
if err := os.WriteFile(runtime, []byte(`version: 1
plugins: []
modules: []
`), 0o600); err != nil {
t.Fatal(err)
}
if isLikelyWfctlProjectManifest(runtime) {
t.Fatal("wfctl.yaml with runtime keys must not be classified as project manifest")
}
other := filepath.Join(dir, "other.yaml")
if err := os.WriteFile(other, []byte(`version: 1
plugins: []
`), 0o600); err != nil {
t.Fatal(err)
}
if isLikelyWfctlProjectManifest(other) {
t.Fatal("non-canonical filename must not be classified as project manifest")
}
bad := filepath.Join(dir, "bad.wfctl.yaml")
if err := os.WriteFile(bad, []byte("plugins: ["), 0o600); err != nil {
t.Fatal(err)
}
if isLikelyWfctlProjectManifest(bad) {
t.Fatal("malformed yaml must not be classified as project manifest")
}
}

func TestRunValidateDoesNotMisclassifyOtherPluginsYAML(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "buf.gen.yaml")
if err := os.WriteFile(path, []byte(`version: v2
plugins:
- local: protoc-gen-go
out: gen/go
`), 0o600); err != nil {
t.Fatal(err)
}
err := runValidate([]string{path})
if err == nil {
t.Fatal("expected non-workflow plugins YAML to fail workflow validation")
}
if strings.Contains(err.Error(), "wfctl config validate") {
t.Fatalf("error = %v, should not classify buf.gen.yaml as wfctl project manifest", err)
}
}

func captureConfigValidateStderr(t *testing.T, fn func()) string {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
orig := os.Stderr
os.Stderr = w
defer func() {
os.Stderr = orig
}()
fn()
if err := w.Close(); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
t.Fatal(err)
}
return buf.String()
}
Loading
Loading