package approval import ( "context" "encoding/json" "reflect" "strings" "testing" ) // fixedRules rules the same way on every call to one tool. type fixedRules struct { tool string verdict Verdict } func (f fixedRules) Check(tool string, _ json.RawMessage) Ruling { if tool != f.tool { return Ruling{} } return Ruling{Verdict: f.verdict, Rule: "Rule(x)", Source: "settings.json"} } func (f fixedRules) Lines() []string { return []string{"settings.json", " rule"} } func TestStandingRules(t *testing.T) { tests := []struct { name string tool string verdict Verdict yolo bool remembered bool wantErr bool wantPrompt int }{ {name: "deny holds under yolo", tool: "bash", verdict: VerdictDeny, yolo: true, wantErr: true}, {name: "deny holds for a read-only tool", tool: "read_file", verdict: VerdictDeny, wantErr: true}, {name: "deny beats session memory", tool: "bash", verdict: VerdictDeny, remembered: true, wantErr: true}, {name: "allow skips the prompt", tool: "bash", verdict: VerdictAllow}, {name: "ask prompts under yolo", tool: "bash", verdict: VerdictAsk, yolo: true, wantPrompt: 1}, {name: "ask prompts for a read-only tool", tool: "read_file", verdict: VerdictAsk, wantPrompt: 1}, {name: "ask beats session memory", tool: "bash", verdict: VerdictAsk, remembered: true, wantPrompt: 1}, {name: "no verdict prompts as usual", tool: "bash", verdict: NoVerdict, wantPrompt: 1}, {name: "no verdict still honours memory", tool: "bash", verdict: NoVerdict, remembered: true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var asked int var forced string p := New(func(_ context.Context, r Request) (Decision, error) { asked++ forced = r.Forced return Once, nil }) p.YOLO = tc.yolo p.Rules = fixedRules{tool: tc.tool, verdict: tc.verdict} if tc.remembered { p.tools[tc.tool] = true } err := approve(t, p, tc.tool, `{"command":"ls","path":"a"}`) if (err != nil) != tc.wantErr { t.Fatalf("err = %v, want error %v", err, tc.wantErr) } if err != nil && !strings.Contains(err.Error(), "Rule(x) in settings.json") { t.Errorf("denial %q should name the rule and its file", err) } if asked != tc.wantPrompt { t.Errorf("prompted %d times, want %d", asked, tc.wantPrompt) } if tc.verdict == VerdictAsk && forced == "" { t.Error("a prompt an ask rule forced should say so") } }) } } // Headless, an ask rule is a refusal with nobody to put the question to — // which is what the rule asked for. func TestAskRuleHeadlessDenies(t *testing.T) { p := New(nil) p.YOLO = true p.Rules = fixedRules{tool: "bash", verdict: VerdictAsk} if err := approve(t, p, "bash", `{"command":"ls"}`); err == nil { t.Error("an ask rule with no prompt should not run the call") } } func TestRuleLines(t *testing.T) { var p *Policy if p.RuleLines() != nil { t.Error("nil policy") } p = New(nil) if p.RuleLines() != nil { t.Error("no rules") } p.Rules = fixedRules{} if len(p.RuleLines()) != 2 { t.Error("lines not passed through") } } func TestSimpleCommands(t *testing.T) { tests := []struct { script string want []string pinned []bool opaque bool }{ {"git status", []string{"git status"}, []bool{true}, false}, {"go build ./... && go test ./...", []string{"go build ./...", "go test ./..."}, []bool{true, true}, false}, {"ls | grep x; echo done", []string{"ls", "grep x", "echo done"}, []bool{true, true, true}, false}, {"(cd sub && make)", []string{"cd sub", "make"}, []bool{true, true}, false}, {`git commit -m "fix: the thing"`, []string{"git commit -m fix: the thing"}, []bool{true}, false}, {"rm $FILE", []string{"rm "}, []bool{true}, false}, {"ls *.go", []string{"ls "}, []bool{true}, false}, {"FOO=1 make", []string{"make"}, []bool{false}, false}, {"echo x > out.txt", []string{"echo x"}, []bool{false}, false}, {"echo x >> out.txt", []string{"echo x"}, []bool{false}, false}, {"echo x 2>&1 >/dev/null", []string{"echo x"}, []bool{true}, false}, {"export A=1", []string{"export"}, []bool{false}, false}, {"echo $(whoami)", []string{"echo ", "whoami"}, []bool{true, true}, true}, {"diff <(ls a) <(ls b)", []string{"diff ", "ls a", "ls b"}, []bool{true, true, true}, true}, } for _, tc := range tests { cmds, info := SimpleCommands(tc.script) var got []string var pinned []bool for _, c := range cmds { got = append(got, c.Text()) pinned = append(pinned, c.Pinned) } if !reflect.DeepEqual(got, tc.want) || !reflect.DeepEqual(pinned, tc.pinned) { t.Errorf("%q: got %q %v, want %q %v", tc.script, got, pinned, tc.want, tc.pinned) } if info.Opaque != tc.opaque { t.Errorf("%q: opaque %v, want %v", tc.script, info.Opaque, tc.opaque) } } if cmds, info := SimpleCommands("echo 'open"); cmds != nil || info.ParseError == nil { t.Error("a parse error should yield nothing") } }