package cli import ( "bytes" "context" "os" "path/filepath" "strings" "testing" "gitea.maximumdirect.net/eric/distributor/internal/storage" ) func TestExecuteRootHelp(t *testing.T) { var stdout, stderr bytes.Buffer code := Execute(context.Background(), []string{"--help"}, &stdout, &stderr) if code != exitOK { t.Fatalf("exit code = %d, want %d", code, exitOK) } if !strings.Contains(stdout.String(), "Usage:") { t.Fatalf("stdout = %q, want help text", stdout.String()) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } func TestExecuteVersion(t *testing.T) { var stdout, stderr bytes.Buffer code := Execute(context.Background(), []string{"version"}, &stdout, &stderr) if code != exitOK { t.Fatalf("exit code = %d, want %d", code, exitOK) } if got, want := stdout.String(), "distributor dev\n"; got != want { t.Fatalf("stdout = %q, want %q", got, want) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } func TestExecuteValidate(t *testing.T) { var stdout, stderr bytes.Buffer code := Execute(context.Background(), []string{"validate", filepath.Join("..", "bundle", "testdata", "valid_bundle")}, &stdout, &stderr) if code != exitOK { t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String()) } if got, want := stdout.String(), "Validated 1 bundle(s)\n"; got != want { t.Fatalf("stdout = %q, want %q", got, want) } } func TestExecuteValidateArgs(t *testing.T) { validPath := filepath.Join("..", "bundle", "testdata", "valid_bundle") tests := []struct { name string args []string wantCode int wantStdout string wantStderr string }{ { name: "zero args", args: []string{"validate"}, wantCode: exitError, wantStderr: "requires a path", }, { name: "one arg", args: []string{"validate", validPath}, wantCode: exitOK, wantStdout: "Validated 1 bundle(s)", }, { name: "two args", args: []string{"validate", validPath, validPath}, wantCode: exitUsage, wantStderr: "accepts at most one path", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var stdout, stderr bytes.Buffer code := Execute(context.Background(), tt.args, &stdout, &stderr) if code != tt.wantCode { t.Fatalf("exit code = %d, want %d; stderr = %q", code, tt.wantCode, stderr.String()) } if tt.wantStdout != "" && !strings.Contains(stdout.String(), tt.wantStdout) { t.Fatalf("stdout = %q, want substring %q", stdout.String(), tt.wantStdout) } if tt.wantStderr != "" && !strings.Contains(stderr.String(), tt.wantStderr) { t.Fatalf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr) } }) } } func TestExecuteInspect(t *testing.T) { var stdout, stderr bytes.Buffer code := Execute(context.Background(), []string{"inspect", filepath.Join("..", "bundle", "testdata", "valid_bundle")}, &stdout, &stderr) if code != exitOK { t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String()) } if !strings.Contains(stdout.String(), "id=weather.daily.brentwood.2026-05-30") { t.Fatalf("stdout = %q, want bundle summary", stdout.String()) } } func TestExecuteInspectArgs(t *testing.T) { validPath := filepath.Join("..", "bundle", "testdata", "valid_bundle") tests := []struct { name string args []string wantCode int wantStdout string wantStderr string }{ { name: "zero args", args: []string{"inspect"}, wantCode: exitError, wantStderr: "requires a path", }, { name: "one arg", args: []string{"inspect", validPath}, wantCode: exitOK, wantStdout: "id=weather.daily.brentwood.2026-05-30", }, { name: "two args", args: []string{"inspect", validPath, validPath}, wantCode: exitUsage, wantStderr: "accepts at most one path", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var stdout, stderr bytes.Buffer code := Execute(context.Background(), tt.args, &stdout, &stderr) if code != tt.wantCode { t.Fatalf("exit code = %d, want %d; stderr = %q", code, tt.wantCode, stderr.String()) } if tt.wantStdout != "" && !strings.Contains(stdout.String(), tt.wantStdout) { t.Fatalf("stdout = %q, want substring %q", stdout.String(), tt.wantStdout) } if tt.wantStderr != "" && !strings.Contains(stderr.String(), tt.wantStderr) { t.Fatalf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr) } }) } } func TestExecuteRunDryRun(t *testing.T) { sourceRoot := t.TempDir() writeCLIBundle(t, sourceRoot) configPath := filepath.Join(t.TempDir(), "config.yml") err := os.WriteFile(configPath, []byte(` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive backend: local path: `+t.TempDir()+` `), 0o600) if err != nil { t.Fatalf("write config: %v", err) } var stdout, stderr bytes.Buffer code := Execute(context.Background(), []string{"run", "--config", configPath, "--dry-run"}, &stdout, &stderr) if code != exitOK { t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String()) } if !strings.Contains(stdout.String(), "action=publish_new") { t.Fatalf("stdout = %q, want config summary", stdout.String()) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } func TestExecuteRunRejectsExtraPositionalArgs(t *testing.T) { var stdout, stderr bytes.Buffer code := Execute(context.Background(), []string{"run", "--config", "config.yml", "extra"}, &stdout, &stderr) if code != exitUsage { t.Fatalf("exit code = %d, want %d", code, exitUsage) } if !strings.Contains(stderr.String(), "does not accept positional arguments") { t.Fatalf("stderr = %q, want positional argument error", stderr.String()) } } func TestExecuteRunPublishes(t *testing.T) { sourceRoot := t.TempDir() destinationRoot := t.TempDir() writeCLIBundle(t, sourceRoot) configPath := filepath.Join(t.TempDir(), "config.yml") err := os.WriteFile(configPath, []byte(` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive backend: local path: `+destinationRoot+` `), 0o600) if err != nil { t.Fatalf("write config: %v", err) } var stdout, stderr bytes.Buffer code := Execute(context.Background(), []string{"run", "--config", configPath}, &stdout, &stderr) if code != exitOK { t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String()) } if _, err := os.Stat(filepath.Join(destinationRoot, storage.StateFileName)); err != nil { t.Fatalf("state stat error = %v", err) } } func TestUnknownCommandIsUsageError(t *testing.T) { var stdout, stderr bytes.Buffer code := Execute(context.Background(), []string{"nope"}, &stdout, &stderr) if code != exitUsage { t.Fatalf("exit code = %d, want %d", code, exitUsage) } if !strings.Contains(stderr.String(), "unknown command") { t.Fatalf("stderr = %q, want unknown command error", stderr.String()) } } func writeCLIBundle(t *testing.T, root string) { t.Helper() for _, file := range []struct { path string data string }{ {"manifest.json", `{ "schema_version": 1, "id": "weather.daily.brentwood.2026-05-30", "digest": "sha256:099b205780d2b050024868399961b05731729a548d5d6329c7b06a6740dd75fe", "created": "2026-05-30T11:10:00Z", "files": [ { "path": "report.md", "sha256": "sha256:3640fd37140ee4d2e0e93e78834f232ea67a50e7bc6279203690cc7de1975fa6", "size": 16 }, { "path": "summary.txt", "sha256": "sha256:3cbb36aca330b3bd113955dfbada0adb7a5f95ad9f678bd61f175406c6a37e95", "size": 8 } ] } `}, {"report.md", "# Report\nSunny.\n"}, {"summary.txt", "Summary\n"}, } { if err := os.WriteFile(filepath.Join(root, file.path), []byte(file.data), 0o600); err != nil { t.Fatalf("write bundle file: %v", err) } } }