Files
GitHop/internal/hop/redact_test.go
T
2026-07-11 08:35:07 -07:00

142 lines
4.8 KiB
Go

package hop
import (
"bytes"
"context"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
)
func TestRedactPromptSecrets(t *testing.T) {
apiKey := "sk-proj-" + strings.Repeat("A7", 24)
githubToken := "ghp_" + strings.Repeat("b9", 20)
bearer := strings.Repeat("abcDEF123._-", 3)
generic := strings.Repeat("customKey9", 4)
privateKey := "-----BEGIN PRIVATE KEY-----\n" + strings.Repeat("base64material", 4) + "\n-----END PRIVATE KEY-----"
tests := []struct {
name string
prompt string
secret string
marker string
}{
{"provider API key", "Use " + apiKey + " for this request", apiKey, "[REDACTED:api_key]"},
{"provider access token", "token=" + githubToken, githubToken, "[REDACTED:access_token]"},
{"authorization header", "Authorization: Bearer " + bearer, bearer, "[REDACTED:auth_token]"},
{"contextual generic key", "OPENAI_API_KEY=\"" + generic + "\"", generic, "[REDACTED:credential]"},
{"natural language key", "my api key is " + generic, generic, "[REDACTED:credential]"},
{"private key", "deploy with\n" + privateKey + "\nnow", privateKey, "[REDACTED:private_key]"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
redacted, findings := RedactPromptSecrets(test.prompt)
if strings.Contains(redacted, test.secret) {
t.Fatalf("redacted prompt still contains secret: %q", redacted)
}
if !strings.Contains(redacted, test.marker) {
t.Fatalf("redacted prompt = %q, want marker %q", redacted, test.marker)
}
if len(findings) == 0 {
t.Fatal("redaction metadata is empty")
}
})
}
plain := "Read OPENAI_API_KEY from the environment; the key is configuration; inspect commit 0123456789abcdef0123456789abcdef."
if redacted, findings := RedactPromptSecrets(plain); redacted != plain || len(findings) != 0 {
t.Fatalf("plain prompt changed to %q with findings %#v", redacted, findings)
}
}
func TestPromptSecretNeverReachesDurableProjectBytes(t *testing.T) {
root := t.TempDir()
writeTestFile(t, filepath.Join(root, "base.txt"), "base\n")
service, _, err := InitProject(context.Background(), root)
if err != nil {
t.Fatal(err)
}
secret := "sk-proj-" + strings.Repeat("NeverPersist7", 4)
result, err := service.CreatePrompt(context.Background(), "Use this API key: "+secret, "", "test")
if err != nil {
t.Fatal(err)
}
if len(result.Redactions) != 1 || result.Redactions[0].Count != 1 {
t.Fatalf("redactions = %#v, want one finding", result.Redactions)
}
if strings.Contains(result.Prompt.Prompt, secret) || !strings.Contains(result.Prompt.Prompt, redactedMarkerPrefix) {
t.Fatalf("stored prompt was not redacted: %q", result.Prompt.Prompt)
}
check, err := service.RunCheck(context.Background(), result.Prompt.ID, []string{
"sh", "-c", "printf '%s' '" + secret + "'",
})
if err != nil {
t.Fatal(err)
}
if strings.Contains(strings.Join(check.Command, " "), secret) || strings.Contains(check.Output, secret) {
t.Fatalf("stored check leaked secret: %#v / %q", check.Command, check.Output)
}
proposal, err := service.Propose(context.Background(), result.Prompt.ID, "Completed with "+secret)
if err != nil {
t.Fatal(err)
}
if strings.Contains(proposal.Proposal.Summary, secret) {
t.Fatalf("stored proposal summary leaked secret: %q", proposal.Proposal.Summary)
}
if err := service.Close(); err != nil {
t.Fatal(err)
}
needle := []byte(secret)
for _, tree := range []string{filepath.Join(root, ".hop"), filepath.Join(root, ".git", "refs", "hop")} {
err := filepath.WalkDir(tree, func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() {
return nil
}
contents, readErr := os.ReadFile(path)
if readErr != nil {
return readErr
}
if bytes.Contains(contents, needle) {
t.Errorf("secret leaked into durable file %s", path)
}
return nil
})
if err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
}
}
func TestBeginJSONNeverEchoesPromptSecret(t *testing.T) {
root := t.TempDir()
previousDirectory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(root); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(previousDirectory) })
secret := "ghp_" + strings.Repeat("NoEcho7", 6)
var stdout, stderr bytes.Buffer
code := RunCLIWithInput(
[]string{"begin", "--agent", "codex", "--session", "secret-test", "--stdin", "--json"},
strings.NewReader("Use token="+secret), &stdout, &stderr)
if code != 0 {
t.Fatalf("begin exited %d: %s", code, stderr.String())
}
if strings.Contains(stdout.String(), secret) || strings.Contains(stderr.String(), secret) {
t.Fatal("begin output echoed the prompt secret")
}
if !strings.Contains(stdout.String(), redactedMarkerPrefix) || !strings.Contains(stdout.String(), `"redactions"`) {
t.Fatalf("begin JSON omitted redaction evidence: %s", stdout.String())
}
}