command_test.go

  1package restic
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"reflect"
  7	"sort"
  8	"strings"
  9	"testing"
 10
 11	"git.secluded.site/keld/internal/config"
 12)
 13
 14func TestCommandsSpotChecks(t *testing.T) {
 15	t.Parallel()
 16
 17	tests := []struct {
 18		name       string
 19		command    string
 20		optionName string
 21		wantAlias  string
 22		wantRepeat bool
 23	}{
 24		{
 25			name:       "backup dry-run alias",
 26			command:    "backup",
 27			optionName: "dry-run",
 28			wantAlias:  "n",
 29			wantRepeat: false,
 30		},
 31		{
 32			name:       "backup exclude repeatable",
 33			command:    "backup",
 34			optionName: "exclude",
 35			wantAlias:  "e",
 36			wantRepeat: true,
 37		},
 38		{
 39			name:       "backup tag repeatable",
 40			command:    "backup",
 41			optionName: "tag",
 42			wantAlias:  "",
 43			wantRepeat: true,
 44		},
 45		{
 46			name:       "global repo alias",
 47			command:    "global",
 48			optionName: "repo",
 49			wantAlias:  "r",
 50			wantRepeat: false,
 51		},
 52		{
 53			name:       "global verbose alias",
 54			command:    "global",
 55			optionName: "verbose",
 56			wantAlias:  "v",
 57			wantRepeat: false,
 58		},
 59	}
 60
 61	for _, tt := range tests {
 62		tt := tt
 63		t.Run(tt.name, func(t *testing.T) {
 64			t.Parallel()
 65
 66			cmd, ok := Commands[tt.command]
 67			if !ok {
 68				t.Fatalf("command %q missing from Commands map", tt.command)
 69			}
 70
 71			opt, ok := findOption(cmd.Options, tt.optionName)
 72			if !ok {
 73				t.Fatalf("option %q missing from command %q", tt.optionName, tt.command)
 74			}
 75
 76			if opt.Alias != tt.wantAlias {
 77				t.Fatalf("alias mismatch for %s/%s: got %q, want %q", tt.command, tt.optionName, opt.Alias, tt.wantAlias)
 78			}
 79			if opt.Repeatable != tt.wantRepeat {
 80				t.Fatalf("repeatable mismatch for %s/%s: got %v, want %v", tt.command, tt.optionName, opt.Repeatable, tt.wantRepeat)
 81			}
 82		})
 83	}
 84}
 85
 86func TestEveryCommandHasHelpOption(t *testing.T) {
 87	t.Parallel()
 88
 89	commandNames := make([]string, 0, len(Commands))
 90	for name := range Commands {
 91		commandNames = append(commandNames, name)
 92	}
 93	sort.Strings(commandNames)
 94
 95	for _, name := range commandNames {
 96		name := name
 97		t.Run(name, func(t *testing.T) {
 98			t.Parallel()
 99
100			cmd := Commands[name]
101			if _, ok := findOption(cmd.Options, "help"); !ok {
102				t.Fatalf("command %q has no help option", name)
103			}
104		})
105	}
106}
107
108func TestWarnUnknownFlags(t *testing.T) {
109	t.Parallel()
110
111	tests := []struct {
112		name      string
113		command   string
114		flags     []config.Flag
115		wantLines []string
116	}{
117		{
118			name:    "known command and global flags are silent",
119			command: "backup",
120			flags: []config.Flag{
121				{Name: "--repo", Value: "/repo"},
122				{Name: "--tag", Value: "daily"},
123				{Name: "-n"},
124			},
125			wantLines: nil,
126		},
127		{
128			name:    "unknown flags emit warnings once",
129			command: "backup",
130			flags: []config.Flag{
131				{Name: "--repo", Value: "/repo"},
132				{Name: "--unknown", Value: "x"},
133				{Name: "--unknown", Value: "y"},
134				{Name: "-Z"},
135			},
136			wantLines: []string{
137				`warning: unknown restic flag "--unknown" for command "backup"`,
138				`warning: unknown restic flag "-Z" for command "backup"`,
139			},
140		},
141		{
142			name:    "empty command validates against global options",
143			command: "",
144			flags: []config.Flag{
145				{Name: "--repo", Value: "/repo"},
146				{Name: "--exclude", Value: "*.tmp"},
147			},
148			wantLines: []string{
149				`warning: unknown restic flag "--exclude" for command "global"`,
150			},
151		},
152	}
153
154	for _, tt := range tests {
155		tt := tt
156		t.Run(tt.name, func(t *testing.T) {
157			t.Parallel()
158
159			var stderr bytes.Buffer
160			warnUnknownFlags(&stderr, tt.command, tt.flags)
161
162			gotLines := splitLines(strings.TrimSpace(stderr.String()))
163			if !reflect.DeepEqual(gotLines, tt.wantLines) {
164				t.Fatalf("warnUnknownFlags() output mismatch:\n%s", fmtDiff(gotLines, tt.wantLines))
165			}
166		})
167	}
168}
169
170func findOption(options []Option, name string) (Option, bool) {
171	for _, option := range options {
172		if option.Name == name {
173			return option, true
174		}
175	}
176	return Option{}, false
177}
178
179func splitLines(s string) []string {
180	if s == "" {
181		return nil
182	}
183	return strings.Split(s, "\n")
184}
185
186func fmtDiff(got, want []string) string {
187	return fmt.Sprintf("got:\n%q\nwant:\n%q", got, want)
188}