cmd/gitbay/templatecmds_test.go

main
gitbay/cmd/gitbay/templatecmds_test.go history · blame · raw

200 lines · 7570 bytes

8 symbols in this file
  1package main
  2
  3import (
  4	"fmt"
  5	"regexp"
  6	"strings"
  7	"testing"
  8
  9	"github.com/spf13/cobra"
 10
 11	"gitbay.org/gitbay/internal/control"
 12	"gitbay.org/gitbay/internal/web"
 13)
 14
 15// quotedRe finds the shapes a command appears in on a page: inline in
 16// <code>, one per line in a <pre class="quickstart"> quickstart block, or
 17// one per line in a <pre class="message"> block (settings/login pages quote
 18// commands there without wrapping each one in <code>). All need (?s) so a
 19// multi-line <pre> is captured as one match.
 20var quotedRe = regexp.MustCompile(`(?s)<code>(.*?)</code>|<pre class="quickstart"[^>]*>(.*?)</pre>|<pre class="message"[^>]*>(.*?)</pre>`)
 21
 22// commandArgv reads the literal words at the front of a quoted command
 23// line — the part naming the command rather than its arguments — and
 24// stops at the first flag, template action, literal ellipsis, or
 25// placeholder (a bare `<name>` or its HTML-escaped form `&lt;name&gt;`),
 26// since those mark the boundary between "what command" and "what
 27// argument".
 28func commandArgv(rest string) []string {
 29	var argv []string
 30	for _, tok := range strings.Fields(rest) {
 31		if strings.HasPrefix(tok, "-") || strings.Contains(tok, "{{") || strings.Contains(tok, "...") ||
 32			strings.HasPrefix(tok, "<") || strings.HasPrefix(tok, "&lt;") {
 33			break
 34		}
 35		argv = append(argv, tok)
 36	}
 37	return argv
 38}
 39
 40// checkGitbayCommand resolves argv (the words after "gitbay ") against the
 41// CLI's own cobra tree, the way a person would type it. cobra's Find walks
 42// one token at a time and, on the first token that names no child, simply
 43// returns the last command it did match with err == nil and the
 44// unmatched tokens as leftovers — so `admin user creat` resolves to the
 45// "user" group instead of failing, unless the caller checks for leftover
 46// args itself. A leaf command (no subcommands of its own) legitimately
 47// takes further words as positional arguments (a repository name, for
 48// instance), so leftover args on a leaf are not an error — only leftover
 49// args on a command that still has subcommands are, since that means the
 50// next word failed to name one of them.
 51func checkGitbayCommand(root *cobra.Command, argv []string) error {
 52	found, rest, err := root.Find(argv)
 53	if err != nil {
 54		return err
 55	}
 56	if found == root {
 57		return fmt.Errorf("does not resolve")
 58	}
 59	if found.HasSubCommands() && len(rest) > 0 {
 60		return fmt.Errorf("%q is not a subcommand of %q", rest[0], found.CommandPath())
 61	}
 62	return nil
 63}
 64
 65// checkSSHCommand resolves argv against the control registry the way
 66// "ssh git@host ..." dispatches it. control.Lookup matches the longest
 67// registered path that is a prefix of argv and reports ok=true even when
 68// trailing words remain unconsumed — e.g. argv ["whoami", "bogus"]
 69// matches the registered "whoami" leaf and silently drops "bogus".
 70// Every quoted ssh command reaching this function has already had its
 71// flags, template actions, ellipses and placeholders trimmed by
 72// commandArgv, so nothing legitimate is left dangling after a real
 73// command's words: any remaining word is either a typo'd attempt at a
 74// deeper command (checked against the registry below, for a precise
 75// message) or bare stray text, and both are bugs in the quoted line.
 76func checkSSHCommand(argv []string) error {
 77	found, rest, ok := control.Lookup(argv)
 78	if !ok {
 79		return fmt.Errorf("%s is not in the control registry", strings.Join(argv, " "))
 80	}
 81	if len(rest) == 0 {
 82		return nil
 83	}
 84	next := rest[0]
 85	for _, cmd := range control.Commands() {
 86		if len(cmd.Path) > len(found.Path) && cmd.Path[len(found.Path)] == next &&
 87			strings.Join(cmd.Path[:len(found.Path)], " ") == strings.Join(found.Path, " ") {
 88			return fmt.Errorf("%q is not a word %s takes further — did you mean %s?", next, strings.Join(found.Path, " "), strings.Join(cmd.Path, " "))
 89		}
 90	}
 91	return fmt.Errorf("%q is not consumed by %s", next, strings.Join(found.Path, " "))
 92}
 93
 94// TestTemplateQuotedCommandsResolve runs every command quoted in a web
 95// template through the same registry the server uses, so a renamed
 96// command fails CI instead of shipping a dead instruction (#263).
 97//
 98// A line starting "gitbay " is checked against the CLI's own command
 99// tree with cobra's Find, since the CLI's grouping words (like "auth")
100// are not part of the server's argv. A line starting "ssh git@{{.Host}}
101// " is checked directly against control.Lookup, since that is exactly
102// the argv the server receives.
103func TestTemplateQuotedCommandsResolve(t *testing.T) {
104	root := newRoot()
105	for _, name := range web.Pages() {
106		src, err := web.TemplateSource(name)
107		if err != nil {
108			t.Fatalf("%s: %v", name, err)
109		}
110		for _, m := range quotedRe.FindAllStringSubmatch(src, -1) {
111			block := m[1] + m[2] + m[3]
112			for _, line := range strings.Split(block, "\n") {
113				if i := strings.Index(line, "#"); i >= 0 {
114					line = line[:i]
115				}
116				line = strings.TrimSpace(line)
117				switch {
118				case strings.HasPrefix(line, "gitbay "):
119					argv := commandArgv(strings.TrimPrefix(line, "gitbay "))
120					if len(argv) == 0 {
121						continue
122					}
123					if err := checkGitbayCommand(root, argv); err != nil {
124						t.Errorf("%s: %q: gitbay %s: %v", name, line, strings.Join(argv, " "), err)
125					}
126				case strings.HasPrefix(line, "ssh git@{{.Host}} "):
127					argv := commandArgv(strings.TrimPrefix(line, "ssh git@{{.Host}} "))
128					if len(argv) == 0 {
129						continue
130					}
131					if err := checkSSHCommand(argv); err != nil {
132						t.Errorf("%s: %q: %v", name, line, err)
133					}
134				}
135			}
136		}
137	}
138}
139
140func TestCommandArgv(t *testing.T) {
141	cases := []struct {
142		in   string
143		want []string
144	}{
145		{"org delete {{$org}} --yes", []string{"org", "delete"}},
146		{"admin user create &lt;name&gt; --key - &lt; key.pub", []string{"admin", "user", "create"}},
147		{"admin ...", []string{"admin"}},
148		{"whoami", []string{"whoami"}},
149		{"web sessions list", []string{"web", "sessions", "list"}},
150		{"snippet create <file> < file", []string{"snippet", "create"}},
151	}
152	for _, c := range cases {
153		got := commandArgv(c.in)
154		if strings.Join(got, " ") != strings.Join(c.want, " ") {
155			t.Errorf("commandArgv(%q) = %v, want %v", c.in, got, c.want)
156		}
157	}
158}
159
160func TestCheckGitbayCommand(t *testing.T) {
161	root := newRoot()
162	cases := []struct {
163		name    string
164		argv    []string
165		wantErr bool
166	}{
167		{"real leaf", []string{"auth", "export"}, false},
168		{"real leaf two groups deep", []string{"admin", "user", "create"}, false},
169		{"leaf with a positional leftover is fine", []string{"org", "delete", "krz/gitbay"}, false},
170		{"typo'd subcommand under a group", []string{"admin", "user", "creat"}, true},
171		{"top-level typo", []string{"bogus"}, true},
172		{"old wrong top-level path", []string{"account", "export"}, true},
173	}
174	for _, c := range cases {
175		err := checkGitbayCommand(root, c.argv)
176		if (err != nil) != c.wantErr {
177			t.Errorf("%s: checkGitbayCommand(%v) error = %v, wantErr %v", c.name, c.argv, err, c.wantErr)
178		}
179	}
180}
181
182func TestCheckSSHCommand(t *testing.T) {
183	cases := []struct {
184		name    string
185		argv    []string
186		wantErr bool
187	}{
188		{"real leaf", []string{"whoami"}, false},
189		{"real leaf three deep", []string{"web", "sessions", "list"}, false},
190		{"stray trailing word", []string{"whoami", "bogus"}, true},
191		{"unregistered path entirely", []string{"auth", "whoami"}, true},
192		{"typo'd word past a real prefix", []string{"web", "sessions", "listing"}, true},
193	}
194	for _, c := range cases {
195		err := checkSSHCommand(c.argv)
196		if (err != nil) != c.wantErr {
197			t.Errorf("%s: checkSSHCommand(%v) error = %v, wantErr %v", c.name, c.argv, err, c.wantErr)
198		}
199	}
200}