package cli import ( "bytes" "context" "os" "path/filepath" "strings" "testing" "gitea.maximumdirect.net/eric/distributor/internal/storage" "gitea.maximumdirect.net/eric/distributor/internal/testutil" ) 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() testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{}) configPath := testutil.WriteMinimalLocalConfig(t, sourceRoot, t.TempDir()) 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 TestExecuteRunForceDryRunReportsWithoutWriting(t *testing.T) { sourceRoot := t.TempDir() destinationRoot := t.TempDir() testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{}) if err := os.WriteFile(filepath.Join(destinationRoot, "unmanaged.txt"), []byte("old"), 0o600); err != nil { t.Fatalf("write unmanaged file: %v", err) } configPath := testutil.WriteMinimalLocalConfig(t, sourceRoot, destinationRoot) var stdout, stderr bytes.Buffer code := Execute(context.Background(), []string{"run", "--config", configPath, "--force", "--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=force_replace") { t.Fatalf("stdout = %q, want force_replace", stdout.String()) } if _, err := os.Stat(filepath.Join(destinationRoot, "unmanaged.txt")); err != nil { t.Fatalf("unmanaged file stat error = %v", err) } if _, err := os.Stat(filepath.Join(destinationRoot, storage.StateFileName)); !os.IsNotExist(err) { t.Fatalf("state stat error = %v, want not exist", err) } } 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() testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{}) configPath := testutil.WriteMinimalLocalConfig(t, sourceRoot, destinationRoot) 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()) } }