123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 | package tooldef
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"blinker/internal/policy"
)
const sampleTool = `#!/bin/bash
# blinker-tool
# description: Commit the given files.
# description: Hooks do not run.
#
# arg message string: The commit message.
# arg level enum(low|high): How much.
# arg files path...: The files.
# write: .git
# read: .git/hooks
# network: Example.com
# timeout: 30s
msg=$1; shift
# An ordinary comment, after the header.
git commit -m "$msg" -- "$@"
`
func TestParse(t *testing.T) {
tool, err := Parse("Commit", "Commit.sh", []byte(sampleTool))
if err != nil {
t.Fatal(err)
}
if tool.Description != "Commit the given files. Hooks do not run." {
t.Errorf("description %q", tool.Description)
}
if len(tool.Args) != 3 || tool.Args[1].Type != "enum" || strings.Join(tool.Args[1].Values, ",") != "low,high" || !tool.Args[2].Variadic || tool.Args[2].Type != "path" {
t.Errorf("args %+v", tool.Args)
}
if len(tool.Files) != 2 || tool.Files[0] != (policy.FileRule{Path: ".git", Access: policy.Write}) || tool.Network[0] != "example.com" || tool.Timeout != 30*time.Second {
t.Errorf("access %+v %v %v", tool.Files, tool.Network, tool.Timeout)
}
if tool.Script != sampleTool {
t.Error("the script is not the whole file")
}
if g := tool.Grants(); g != "write .git, read .git/hooks, network example.com" {
t.Errorf("Grants = %q", g)
}
}
func TestParseErrors(t *testing.T) {
header := "# blinker-tool\n# description: d\n"
for name, text := range map[string]string{
"no marker": "# description: d\necho\n",
"no description": "# blinker-tool\n# arg a string: x\necho\n",
"unknown key": header + "# shell: zsh\n",
"not key value": header + "# just a comment\n",
"bad type": header + "# arg a file: x\n",
"no arg text": header + "# arg a string:\n",
"bad arg name": header + "# arg A string: x\n",
"list not last": header + "# arg a string...: x\n# arg b string: x\n",
"duplicate arg": header + "# arg a string: x\n# arg a path: x\n",
"empty enum value": header + "# arg a enum(x||y): x\n",
"bad path": header + "# write: ../x\n",
"bad host": header + "# network: not a host\n",
"bad timeout": header + "# timeout: soon\n",
"nul": header + "echo \x00\n",
} {
if _, err := Parse("T", name, []byte(text)); err == nil {
t.Errorf("%s: parsed", name)
}
}
for _, name := range []string{"Safe_Bash", "read", "my tool", "1x", ""} {
if _, err := Parse(name, "x", []byte(header)); err == nil {
t.Errorf("tool name %q accepted", name)
}
}
}
func TestDefaultTools(t *testing.T) {
tools, err := Load(t.TempDir())
if err != nil {
t.Fatal(err)
}
var names []string
for _, tool := range tools {
names = append(names, tool.Name)
}
if strings.Join(names, ",") != "Commit,Define,Fetch,Usage" {
t.Errorf("built-in tools: %v", names)
}
}
func TestLoadOverride(t *testing.T) {
dir := t.TempDir()
write := func(name, text string) {
if err := os.WriteFile(filepath.Join(dir, name), []byte(text), 0o600); err != nil {
t.Fatal(err)
}
}
write("Commit.sh", "# blinker-tool\n# description: Mine.\necho mine\n")
write("Format.sh", "#!/bin/sh\n# blinker-tool\n# description: Format.\n# arg file path: The file.\ngofmt -w -- \"$1\"\n")
write("notes.txt", "not a tool")
write("Format.sh~", "an editor backup")
tools, err := Load(dir)
if err != nil {
t.Fatal(err)
}
byName := map[string]Def{}
for _, tool := range tools {
byName[tool.Name] = tool
}
if byName["Commit"].Description != "Mine." || byName["Format"].Args[0].Type != "path" || len(byName) != 5 {
t.Errorf("loaded %+v", byName)
}
write("Broken.sh", "echo no header\n")
if _, err := Load(dir); err == nil {
t.Error("a broken tool file loaded")
}
}
func TestCreate(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{"Format", "Define", "Commit"} {
path, err := Create(dir, name)
if err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if _, err := Parse(name, path, data); err != nil {
t.Errorf("%s: the new file does not parse: %v", name, err)
}
}
// A built-in script starts from its file; Commit, which blinker runs
// itself, has none, so it starts from the template.
if data, _ := os.ReadFile(Path(dir, "Define")); !strings.Contains(string(data), "ctags") {
t.Error("Define did not start from the built-in tool")
}
if data, _ := os.ReadFile(Path(dir, "Commit")); !strings.Contains(string(data), "Hello from") {
t.Error("Commit started from something other than the template")
}
if _, err := Create(dir, "Safe_Bash"); err == nil {
t.Error("created a tool with a built-in name")
}
}
// Fetch's hosts are valid host rules, each listed once; Commit and Fetch
// have no files.
func TestFetchTool(t *testing.T) {
tool := fetchTool()
seen := map[string]bool{}
for _, h := range tool.Network {
if v, err := policy.ValidateHost(h); err != nil || v != h || seen[h] {
t.Errorf("host %q: %v", h, err)
}
seen[h] = true
}
if len(tool.Args) != 1 || tool.Args[0].Name != "url" || !strings.Contains(tool.Description, "pkg.go.dev") {
t.Errorf("Fetch = %+v", tool)
}
for _, name := range []string{"Commit", "Fetch"} {
if _, ok := builtinFile(name); ok {
t.Errorf("%s, which blinker runs itself, has a built-in file", name)
}
}
}
// A tool that asks runs under the unsafe policy, one allowed without
// asking under the safe one, each with its own rules added.
func TestPolicy(t *testing.T) {
env := policy.Env{Home: "/home/u", Project: "/home/u/proj"}
compile := func(text string) *policy.Compiled {
t.Helper()
f, err := policy.Parse([]byte(text))
if err != nil {
t.Fatal(err)
}
c, err := policy.Compile(policy.Layer{Name: "test", File: f}, policy.Floor{}, env)
if err != nil {
t.Fatal(err)
}
return c
}
p, err := policy.Bind(compile("file read .\n"), compile("file write .\n"))
if err != nil {
t.Fatal(err)
}
tool := Def{Name: "Commit", Access: policy.Allow, Files: []policy.FileRule{{Path: ".git", Access: policy.Write}}}
c, err := tool.Policy(p)
if err != nil {
t.Fatal(err)
}
if !c.Access("/home/u/proj/.git/index").Write || c.Access("/home/u/proj/main.go").Write {
t.Error("a tool that runs without asking did not get the safe policy and its rules")
}
tool.Access = policy.Ask
if c, err = tool.Policy(p); err != nil || !c.Access("/home/u/proj/main.go").Write {
t.Errorf("a tool that asks did not get the unsafe policy: %v", err)
}
}
|