package app import ( "bytes" "context" "encoding/json" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/distributor/internal/testutil" ) func TestInspectPrintsBundleSummary(t *testing.T) { var stdout bytes.Buffer err := Inspect(context.Background(), InspectOptions{ Path: filepath.Join("..", "bundle", "testdata", "valid_bundle"), Stdout: &stdout, }) if err != nil { t.Fatalf("Inspect() error = %v", err) } output := stdout.String() for _, want := range []string{ "Bundles: 1", "path=.", "id=weather.daily.brentwood.2026-05-30", "created=2026-05-30T11:10:00Z", "report.md size=16", "summary.txt size=8", } { if !strings.Contains(output, want) { t.Fatalf("Inspect() output = %q, want substring %q", output, want) } } } func TestInspectConfiguredLocalSource(t *testing.T) { sourceRoot := t.TempDir() destinationRoot := t.TempDir() testutil.WriteSourceBundle(t, sourceRoot, "daily", testutil.BundleOptions{ID: "reports.daily"}) var stdout bytes.Buffer err := Inspect(context.Background(), InspectOptions{ ConfigPath: testutil.WriteMinimalLocalConfig(t, sourceRoot, destinationRoot), PipelineID: "reports", Stdout: &stdout, }) if err != nil { t.Fatalf("Inspect() configured source error = %v", err) } output := stdout.String() for _, want := range []string{ "Pipeline: reports", "Source: local", "Bundles: 1", "path=daily", "id=reports.daily", } { if !strings.Contains(output, want) { t.Fatalf("Inspect() output = %q, want substring %q", output, want) } } } func TestInspectConfiguredSourceJSON(t *testing.T) { sourceRoot := t.TempDir() destinationRoot := t.TempDir() testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{ID: "reports.json"}) var stdout bytes.Buffer err := Inspect(context.Background(), InspectOptions{ ConfigPath: testutil.WriteMinimalLocalConfig(t, sourceRoot, destinationRoot), PipelineID: "reports", Stdout: &stdout, OutputFormat: OutputFormatJSON, }) if err != nil { t.Fatalf("Inspect() configured JSON error = %v", err) } result := decodeAppResult(t, stdout.String()) if result["pipeline_id"] != "reports" || result["source_backend"] != "local" || result["bundle_count"] != float64(1) { t.Fatalf("result = %#v, want configured inspect metadata", result) } } func TestInspectJSONPreservesCreatedOffsetAndFileMetadata(t *testing.T) { sourceRoot := t.TempDir() created := time.Date(2026, 6, 1, 6, 30, 0, 0, time.FixedZone("CDT", -5*60*60)) testutil.WriteSourceBundle(t, sourceRoot, "daily", testutil.BundleOptions{ ID: "reports.offset", Created: created, Files: []testutil.SourceFile{ {Path: "report.md", Data: "# Report\n"}, }, }) var stdout bytes.Buffer err := Inspect(context.Background(), InspectOptions{ Path: sourceRoot, Stdout: &stdout, OutputFormat: OutputFormatJSON, }) if err != nil { t.Fatalf("Inspect() error = %v", err) } result := decodeAppResult(t, stdout.String()) bundles, ok := result["bundles"].([]any) if !ok || len(bundles) != 1 { t.Fatalf("bundles = %#v, want one bundle", result["bundles"]) } bundle, ok := bundles[0].(map[string]any) if !ok { t.Fatalf("bundle = %#v, want object", bundles[0]) } if bundle["created"] != "2026-06-01T06:30:00-05:00" || bundle["file_count"] != float64(1) { t.Fatalf("bundle = %#v, want offset timestamp and file count", bundle) } files, ok := bundle["files"].([]any) if !ok || len(files) != 1 { t.Fatalf("files = %#v, want one file", bundle["files"]) } file, ok := files[0].(map[string]any) if !ok || file["path"] != "report.md" || file["sha256"] == "" || file["size"] != float64(9) { t.Fatalf("file = %#v, want projected file metadata", file) } } func TestInspectConfiguredSourceJSONIncludesSecretConflictWarningWithoutValues(t *testing.T) { name := "DISTRIBUTOR_TEST_INSPECT_SECRET" t.Setenv(name, "process-value") sourceRoot := t.TempDir() destinationRoot := t.TempDir() secretsRoot := t.TempDir() if err := os.WriteFile(filepath.Join(secretsRoot, name), []byte("secret-value\n"), 0o600); err != nil { t.Fatalf("write secret: %v", err) } testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{ID: "reports.json"}) configPath := writeConfigFile(t, ` secrets: directory: `+secretsRoot+` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive backend: local path: `+destinationRoot+` `) var stdout bytes.Buffer err := Inspect(context.Background(), InspectOptions{ ConfigPath: configPath, PipelineID: "reports", Stdout: &stdout, OutputFormat: OutputFormatJSON, }) if err != nil { t.Fatalf("Inspect() error = %v", err) } var envelope struct { Warnings []OutputWarning `json:"warnings"` } if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil { t.Fatalf("decode output: %v; output = %q", err, stdout.String()) } if len(envelope.Warnings) != 1 || !strings.Contains(envelope.Warnings[0].Message, "secret "+name+" ignored") { t.Fatalf("warnings = %#v, want secret conflict warning", envelope.Warnings) } output := stdout.String() if strings.Contains(output, "process-value") || strings.Contains(output, "secret-value") { t.Fatalf("stdout exposed secret values: %q", output) } } func TestInspectRequiresPath(t *testing.T) { err := Inspect(context.Background(), InspectOptions{}) if err == nil || !strings.Contains(err.Error(), "requires a path") { t.Fatalf("Inspect() error = %v, want required path", err) } }