Harden Go CLI subprocess behavior

This commit is contained in:
2026-05-10 23:33:13 +00:00
parent 08b7531149
commit 95fe8c32fa
4 changed files with 234 additions and 4 deletions

View File

@@ -0,0 +1,230 @@
package main
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
"gitea.maximumdirect.net/eric/audita/internal/cli"
)
func TestHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
sep := -1
for i, arg := range os.Args {
if arg == "--" {
sep = i
break
}
}
if sep == -1 {
os.Exit(2)
}
code := cli.Run(os.Args[sep+1:], os.Stdout, os.Stderr)
os.Exit(code)
}
func TestProcessHelpSubprocess(t *testing.T) {
result := runCLISubprocess(t, "process", "--help")
if result.exitCode != 0 {
t.Fatalf("expected exit 0, got %d stderr=%q", result.exitCode, result.stderr)
}
if !strings.Contains(result.stdout, "Usage:") || !strings.Contains(result.stdout, "--glossary") {
t.Fatalf("unexpected help stdout: %q", result.stdout)
}
if result.stderr != "" {
t.Fatalf("expected empty stderr, got %q", result.stderr)
}
}
func TestProcessSuccessWithOutputSubprocess(t *testing.T) {
outputPath := filepath.Join(t.TempDir(), "corrected.json")
result := runCLISubprocess(
t,
"process",
fixturePath("tiny_transcript.json"),
"--glossary",
fixturePath("tiny_glossary.yaml"),
"--output",
outputPath,
)
if result.exitCode != 0 {
t.Fatalf("expected exit 0, got %d stderr=%q", result.exitCode, result.stderr)
}
if result.stdout != "" {
t.Fatalf("expected empty stdout when --output is set, got %q", result.stdout)
}
if result.stderr != "" {
t.Fatalf("expected empty stderr on success, got %q", result.stderr)
}
inputBytes := readFile(t, fixturePath("tiny_transcript.json"))
outputBytes := readFile(t, outputPath)
assertJSONSemanticallyEqual(t, inputBytes, outputBytes)
}
func TestProcessSuccessWithoutOutputSubprocess(t *testing.T) {
result := runCLISubprocess(
t,
"process",
fixturePath("tiny_transcript.json"),
"--glossary",
fixturePath("tiny_glossary.yaml"),
)
if result.exitCode != 0 {
t.Fatalf("expected exit 0, got %d stderr=%q", result.exitCode, result.stderr)
}
if result.stderr != "" {
t.Fatalf("expected empty stderr on success, got %q", result.stderr)
}
inputBytes := readFile(t, fixturePath("tiny_transcript.json"))
assertJSONSemanticallyEqual(t, inputBytes, []byte(result.stdout))
}
func TestProcessFailureMissingTranscriptSubprocess(t *testing.T) {
result := runCLISubprocess(t, "process", "--glossary", fixturePath("tiny_glossary.yaml"))
if result.exitCode == 0 {
t.Fatalf("expected nonzero exit code")
}
if result.stdout != "" {
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
}
if !strings.Contains(result.stderr, "expected exactly 1 transcript JSON path argument") {
t.Fatalf("expected actionable missing transcript error, got %q", result.stderr)
}
}
func TestProcessFailureMalformedJSONSubprocess(t *testing.T) {
result := runCLISubprocess(
t,
"process",
fixturePath("malformed_transcript.json"),
"--glossary",
fixturePath("tiny_glossary.yaml"),
)
if result.exitCode == 0 {
t.Fatalf("expected nonzero exit code")
}
if result.stdout != "" {
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
}
if !strings.Contains(result.stderr, "is not valid JSON") {
t.Fatalf("expected malformed JSON error, got %q", result.stderr)
}
}
func TestProcessFailureUnwritableOutputSubprocess(t *testing.T) {
outputDir := t.TempDir()
result := runCLISubprocess(
t,
"process",
fixturePath("tiny_transcript.json"),
"--glossary",
fixturePath("tiny_glossary.yaml"),
"--output",
outputDir,
)
if result.exitCode == 0 {
t.Fatalf("expected nonzero exit code")
}
if result.stdout != "" {
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
}
if !strings.Contains(result.stderr, "failed to write output file") {
t.Fatalf("expected write failure message, got %q", result.stderr)
}
}
type subprocessResult struct {
stdout string
stderr string
exitCode int
}
func runCLISubprocess(t *testing.T, args ...string) subprocessResult {
t.Helper()
cmdArgs := append([]string{"-test.run=TestHelperProcess", "--"}, args...)
cmd := exec.Command(os.Args[0], cmdArgs...)
cmd.Env = append(filterAuditaEnv(os.Environ()), "GO_WANT_HELPER_PROCESS=1")
var stdoutBuf bytes.Buffer
var stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
err := cmd.Run()
result := subprocessResult{
stdout: stdoutBuf.String(),
stderr: stderrBuf.String(),
}
if err == nil {
return result
}
if exitErr, ok := err.(*exec.ExitError); ok {
result.exitCode = exitErr.ExitCode()
return result
}
t.Fatalf("subprocess execution failed: %v", err)
return subprocessResult{}
}
func filterAuditaEnv(env []string) []string {
filtered := make([]string, 0, len(env))
for _, entry := range env {
key := entry
if idx := strings.IndexByte(entry, '='); idx >= 0 {
key = entry[:idx]
}
if strings.HasPrefix(key, "AUDITA_") || key == "OPENROUTER_API_KEY" {
continue
}
filtered = append(filtered, entry)
}
return filtered
}
func fixturePath(name string) string {
return filepath.Join("..", "..", "internal", "cli", "testdata", name)
}
func readFile(t *testing.T, path string) []byte {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read file %q: %v", path, err)
}
return data
}
func assertJSONSemanticallyEqual(t *testing.T, expected []byte, actual []byte) {
t.Helper()
if !json.Valid(actual) {
t.Fatalf("actual output is not valid JSON: %q", string(actual))
}
var expectedValue any
var actualValue any
if err := json.Unmarshal(expected, &expectedValue); err != nil {
t.Fatalf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(actual, &actualValue); err != nil {
t.Fatalf("failed to unmarshal actual JSON: %v", err)
}
if !reflect.DeepEqual(expectedValue, actualValue) {
t.Fatalf("JSON content mismatch: expected %q got %q", string(expected), string(actual))
}
}