package audita import ( "context" "errors" "os" "path/filepath" "reflect" "strings" "testing" "time" configkit "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess" ) func auditaTestRequest(t *testing.T) PolishRequest { t.Helper() dir := t.TempDir() return PolishRequest{ MergedTranscriptPath: filepath.Join(dir, "merged.json"), GlossaryPath: filepath.Join(dir, "glossary.yml"), OutputProcessedPath: filepath.Join(dir, "polished.json"), GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"), StdoutLogPath: filepath.Join(dir, "stdout.log"), StderrLogPath: filepath.Join(dir, "stderr.log"), } } func auditaTestRunner(t *testing.T, cfg SubprocessRunnerConfig) *SubprocessRunner { t.Helper() if cfg.Binary == "" { cfg.Binary = "audita" } if cfg.Timeout == 0 { cfg.Timeout = time.Second } runner, err := NewSubprocessRunner(cfg) if err != nil { t.Fatal(err) } return runner } func auditaTestSecret(t *testing.T, name, value string) configkit.Secret { t.Helper() t.Setenv(name, value) type loaded struct { Credential configkit.Secret `yaml:"credential"` } result, err := configkit.Load(configkit.Bytes("credential.yml", []byte("credential: "+name+"\n")), configkit.Options[loaded]{}) if err != nil { t.Fatal(err) } return result.Config.Credential } func writeAuditaTestFile(t *testing.T, path, value string) { t.Helper() if err := os.WriteFile(path, []byte(value), 0o600); err != nil { t.Fatal(err) } } func TestSubprocessRunnerUsesV2InvocationAndCapturedCredentials(t *testing.T) { const first = "NARRATIO_AUDITA_CUSTOM_ONE" const second = "NARRATIO_AUDITA_CUSTOM_TWO" firstSecret := auditaTestSecret(t, first, "loaded-one") secondSecret := auditaTestSecret(t, second, "loaded-two") t.Setenv(first, "parent-changed") t.Setenv(second, "parent-changed-too") t.Setenv("NARRATIO_AUDITA_UNSELECTED", "not-forwarded") runner := auditaTestRunner(t, SubprocessRunnerConfig{ Credentials: []configkit.Secret{firstSecret, secondSecret}, Modules: []string{"grammar", "glossary", "grammar"}, Overlay: "testing", ProposalProfile: "proposal", ValidationProfile: "validation", TranscriptDescription: "campaign", ConfigPath: "/etc/audita/config.yml", OutputSchema: "audita-v1", }) req := auditaTestRequest(t) var captured subprocess.RunRequest runner.run = func(_ context.Context, processReq subprocess.RunRequest) (subprocess.RunResult, error) { captured = processReq writeAuditaTestFile(t, req.OutputProcessedPath, `{"segments":[]}`) for _, name := range []string{"errors.yml", "recoveries.yml", "report.md"} { writeAuditaTestFile(t, filepath.Join(filepath.Dir(req.OutputProcessedPath), name), "recoverable diagnostic\n") } return subprocess.RunResult{}, nil } result, err := runner.Run(context.Background(), req) if err != nil { t.Fatal(err) } want := []string{"process", req.MergedTranscriptPath, "--glossary", req.GlossaryPath, "--output", req.OutputProcessedPath, "--modules", "grammar,glossary,grammar", "--config", "/etc/audita/config.yml", "--overlay", "testing", "--proposal-profile", "proposal", "--validation-profile", "validation", "--transcript-description", "campaign", "--output-schema", "audita-v1"} if !reflect.DeepEqual(captured.Args, want) { t.Fatalf("args = %#v, want %#v", captured.Args, want) } if len(captured.EnvOverrides) != 2 || captured.EnvOverrides[first] != "loaded-one" || captured.EnvOverrides[second] != "loaded-two" || captured.EnvOverrides["NARRATIO_AUDITA_UNSELECTED"] != "" { t.Fatalf("credential forwarding = %#v", captured.EnvOverrides) } if !reflect.DeepEqual(captured.SensitiveEnvNames, []string{first, second}) { t.Fatalf("sensitive names = %#v", captured.SensitiveEnvNames) } if result.Reports.ErrorsPath == "" || result.Reports.RecoveriesPath == "" || result.Reports.OverviewPath == "" { t.Fatalf("reports = %#v", result.Reports) } generated, err := os.ReadFile(req.GeneratedConfigPath) if err != nil { t.Fatal(err) } if !strings.Contains(string(generated), "audita.generated.v2") || !strings.Contains(string(generated), first) || strings.Contains(string(generated), "loaded-one") { t.Fatalf("unsafe or incomplete generated invocation: %s", generated) } } func TestSubprocessRunnerModulePresenceAndReportAbsence(t *testing.T) { for _, test := range []struct { name string configured []string requested []string wantFlag bool wantValue string }{ {"omitted", nil, nil, false, ""}, {"configured", []string{"grammar", "grammar"}, nil, true, "grammar,grammar"}, {"empty override", []string{"grammar"}, []string{}, true, ""}, } { t.Run(test.name, func(t *testing.T) { runner := auditaTestRunner(t, SubprocessRunnerConfig{Modules: test.configured}) req := auditaTestRequest(t) req.Modules = test.requested runner.run = func(_ context.Context, processReq subprocess.RunRequest) (subprocess.RunResult, error) { index := -1 for i, arg := range processReq.Args { if arg == "--modules" { index = i } if strings.HasPrefix(arg, "--report") || arg == "--debug" || arg == "--work-dir" { t.Fatalf("retired or unwanted flag %q", arg) } } if (index >= 0) != test.wantFlag || (index >= 0 && processReq.Args[index+1] != test.wantValue) { t.Fatalf("args = %#v", processReq.Args) } writeAuditaTestFile(t, req.OutputProcessedPath, `{"segments":[]}`) return subprocess.RunResult{}, nil } result, err := runner.Run(context.Background(), req) if err != nil || result.Reports != (ReportBundle{}) { t.Fatalf("result = %#v, error = %v", result, err) } }) } } func TestSubprocessRunnerRejectsMissingOrMalformedTranscript(t *testing.T) { for _, output := range []string{"", `{"segments":"wrong"}`} { t.Run(output, func(t *testing.T) { runner := auditaTestRunner(t, SubprocessRunnerConfig{}) req := auditaTestRequest(t) runner.run = func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error) { if output != "" { writeAuditaTestFile(t, req.OutputProcessedPath, output) } return subprocess.RunResult{}, nil } if _, err := runner.Run(context.Background(), req); err == nil { t.Fatal("missing or malformed transcript was accepted") } }) } } func TestSubprocessRunnerPreservesFailureAndAvailableReports(t *testing.T) { runner := auditaTestRunner(t, SubprocessRunnerConfig{}) req := auditaTestRequest(t) processFailure := errors.New("audita exited after writing output") runner.run = func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error) { writeAuditaTestFile(t, req.OutputProcessedPath, `{"segments":[]}`) writeAuditaTestFile(t, filepath.Join(filepath.Dir(req.OutputProcessedPath), "errors.yml"), "status: failed\n") return subprocess.RunResult{ExitCode: 7}, processFailure } result, err := runner.Run(context.Background(), req) if !errors.Is(err, processFailure) || !strings.Contains(err.Error(), filepath.Dir(req.OutputProcessedPath)) { t.Fatalf("error = %v", err) } if result.Reports.ErrorsPath == "" || result.Reports.RecoveriesPath != "" || result.Reports.OverviewPath != "" { t.Fatalf("reports = %#v", result.Reports) } } func TestSubprocessRunnerDoesNotMaskProcessFailureWithReportError(t *testing.T) { runner := auditaTestRunner(t, SubprocessRunnerConfig{}) req := auditaTestRequest(t) processFailure := errors.New("audita process failed") runner.run = func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error) { path := filepath.Join(filepath.Dir(req.OutputProcessedPath), "errors.yml") if err := os.WriteFile(path, make([]byte, MaxReportOutputBytes+1), 0o600); err != nil { t.Fatal(err) } return subprocess.RunResult{ExitCode: 7}, processFailure } if _, err := runner.Run(context.Background(), req); !errors.Is(err, processFailure) { t.Fatalf("error = %v, want original process error", err) } } func TestSubprocessRunnerBoundsAdjacentReports(t *testing.T) { runner := auditaTestRunner(t, SubprocessRunnerConfig{}) req := auditaTestRequest(t) runner.run = func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error) { writeAuditaTestFile(t, req.OutputProcessedPath, `{"segments":[]}`) path := filepath.Join(filepath.Dir(req.OutputProcessedPath), "errors.yml") if err := os.WriteFile(path, make([]byte, MaxReportOutputBytes+1), 0o600); err != nil { t.Fatal(err) } return subprocess.RunResult{}, nil } if _, err := runner.Run(context.Background(), req); err == nil || !strings.Contains(err.Error(), "errors.yml") || !strings.Contains(err.Error(), "16777216") { t.Fatalf("error = %v, want named report limit", err) } } func TestSubprocessRunnerRedactsCustomCredentialStreams(t *testing.T) { const first = "NARRATIO_AUDITA_STREAM_ONE" const second = "NARRATIO_AUDITA_STREAM_TWO" for _, fail := range []bool{false, true} { t.Run(map[bool]string{false: "success", true: "failure"}[fail], func(t *testing.T) { firstSecret := auditaTestSecret(t, first, "audita-first-secret") secondSecret := auditaTestSecret(t, second, "audita-second-secret") req := auditaTestRequest(t) body := "#!/bin/sh\nprintf '%s\\n' \"$" + first + "\"\nprintf '%s\\n' \"$" + second + "\" >&2\n" if fail { body += "exit 7\n" } else { body += "printf '{\"segments\":[]}' > \"" + req.OutputProcessedPath + "\"\n" } binary := filepath.Join(t.TempDir(), "audita-helper") writeAuditaTestFile(t, binary, body) if err := os.Chmod(binary, 0o700); err != nil { t.Fatal(err) } runner := auditaTestRunner(t, SubprocessRunnerConfig{Binary: binary, Credentials: []configkit.Secret{firstSecret, secondSecret}}) _, err := runner.Run(context.Background(), req) if (err != nil) != fail { t.Fatalf("error = %v, fail = %v", err, fail) } for _, path := range []string{req.StdoutLogPath, req.StderrLogPath} { data, readErr := os.ReadFile(path) if readErr != nil { t.Fatal(readErr) } if strings.Contains(string(data), "audita-first-secret") || strings.Contains(string(data), "audita-second-secret") || !strings.Contains(string(data), "") { t.Fatalf("unredacted stream %q: %s", path, data) } } if fail && (strings.Contains(err.Error(), "audita-first-secret") || strings.Contains(err.Error(), "audita-second-secret")) { t.Fatalf("unredacted failure: %v", err) } }) } }