264 lines
10 KiB
Go
264 lines
10 KiB
Go
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), "<redacted>") {
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|