~bigbes/sr-ht-spec

ref: 658bae75f9714af212b463dbba6bb2de565112f9 sr-ht-spec/gitx/refsrule_test.go -rw-r--r-- 8.8 KiB
658bae75 — Eugene Blikh feat(prosediff): recover the source line each word edit sits on (spec-by6.3.5) 24 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package gitx

import (
	"errors"
	"strings"
	"testing"

	"github.com/go-git/go-git/v5/plumbing"
)

var (
	hashA = plumbing.NewHash("1111111111111111111111111111111111111111")
	hashB = plumbing.NewHash("2222222222222222222222222222222222222222")
	zero  = plumbing.ZeroHash
)

// TestCheckRefUpdate is the table for the one rule the whole write model rests
// on: the human pushes to the approved branch, agents may only write
// proposals/*.
func TestCheckRefUpdate(t *testing.T) {
	cases := []struct {
		name      string
		principal PrincipalKind
		update    RefUpdate
		allow     bool
		// want, when set, must appear in the rejection message: a hook error
		// that does not say why is a support ticket.
		want string
	}{
		// --- the human and the approved branch -------------------------------
		{
			name:      "human fast-forwards the approved branch",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: true},
			allow:     true,
		},
		{
			name:      "human force-updates the approved branch",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: false},
			allow:     false,
			want:      "fast-forwards only",
		},
		{
			name:      "human deletes the approved branch",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "refs/heads/main", Old: hashA, New: zero},
			allow:     false,
			want:      "may not be deleted",
		},
		{
			name:      "human creates the approved branch",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "refs/heads/main", Old: zero, New: hashB, FastForward: true},
			allow:     true,
		},

		// --- the agent and the approved branch -------------------------------
		{
			name:      "agent fast-forwards the approved branch",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: true},
			allow:     false,
			want:      "not the approved branch",
		},
		{
			name:      "agent deletes the approved branch",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/main", Old: hashA, New: zero},
			allow:     false,
			want:      "not the approved branch",
		},

		// --- proposal branches ------------------------------------------------
		{
			name:      "agent creates a proposal branch",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/proposals/42", Old: zero, New: hashB, FastForward: true},
			allow:     true,
		},
		{
			name:      "agent force-updates its own proposal branch",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/proposals/42", Old: hashA, New: hashB, FastForward: false},
			allow:     true,
		},
		{
			name:      "agent deletes a proposal branch",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/proposals/42", Old: hashA, New: zero},
			allow:     true,
		},
		{
			name:      "human updates a proposal branch",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "refs/heads/proposals/42", Old: hashA, New: hashB, FastForward: true},
			allow:     true,
		},
		{
			name:      "a nested proposal branch is still a proposal branch",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/proposals/agent/7", Old: zero, New: hashB, FastForward: true},
			allow:     true,
		},
		{
			name:      "the bare proposals namespace is not a proposal branch",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/proposals", Old: zero, New: hashB, FastForward: true},
			allow:     false,
			want:      "and nothing else",
		},
		{
			name:      "a branch that merely starts with the word proposals",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/proposals-evil", Old: zero, New: hashB, FastForward: true},
			allow:     false,
			want:      "and nothing else",
		},

		// --- everything else --------------------------------------------------
		{
			name:      "agent writes some other branch",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/scratch", Old: zero, New: hashB, FastForward: true},
			allow:     false,
			want:      "and nothing else",
		},
		{
			name:      "human writes some other branch",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "refs/heads/scratch", Old: zero, New: hashB, FastForward: true},
			allow:     false,
			want:      "and nothing else",
		},
		{
			name:      "human pushes a tag",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "refs/tags/v1.0.0", Old: zero, New: hashB, FastForward: true},
			allow:     false,
			want:      "only branches",
		},
		{
			name:      "notes ref",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "refs/notes/commits", Old: zero, New: hashB, FastForward: true},
			allow:     false,
			want:      "only branches",
		},
		{
			name:      "a bare name is not a ref",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "main", Old: hashA, New: hashB, FastForward: true},
			allow:     false,
			want:      "must start with",
		},
		{
			name:      "a traversing ref",
			principal: PrincipalAgent,
			update:    RefUpdate{Ref: "refs/heads/proposals/../../main", Old: zero, New: hashB, FastForward: true},
			allow:     false,
			want:      "'..'",
		},
		{
			name:      "an update that moves nothing",
			principal: PrincipalHuman,
			update:    RefUpdate{Ref: "refs/heads/main", Old: zero, New: zero},
			allow:     false,
			want:      "moves nothing",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := CheckRefUpdate(tc.principal, "main", tc.update)
			if tc.allow {
				if err != nil {
					t.Fatalf("CheckRefUpdate = %v, want allowed", err)
				}
				return
			}
			if err == nil {
				t.Fatal("CheckRefUpdate allowed an update it should have rejected")
			}
			if !errors.Is(err, ErrRefRejected) && !errors.Is(err, ErrBadRev) {
				t.Fatalf("rejection %v is neither ErrRefRejected nor ErrBadRev", err)
			}
			if tc.want != "" && !strings.Contains(err.Error(), tc.want) {
				t.Fatalf("rejection %q does not mention %q", err, tc.want)
			}
		})
	}
}

func TestCheckRefUpdateHonoursANonDefaultApprovedBranch(t *testing.T) {
	u := RefUpdate{Ref: "refs/heads/approved", Old: hashA, New: hashB, FastForward: true}
	if err := CheckRefUpdate(PrincipalHuman, "approved", u); err != nil {
		t.Fatalf("human on the configured approved branch = %v, want allowed", err)
	}
	if err := CheckRefUpdate(PrincipalAgent, "approved", u); !errors.Is(err, ErrRefRejected) {
		t.Fatalf("agent on the configured approved branch = %v, want rejected", err)
	}
	// "main" is nothing special once the space says otherwise.
	main := RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: true}
	if err := CheckRefUpdate(PrincipalHuman, "approved", main); !errors.Is(err, ErrRefRejected) {
		t.Fatalf("human on a non-approved branch = %v, want rejected", err)
	}
}

func TestCheckRefUpdateRejectsUnknownPrincipals(t *testing.T) {
	u := RefUpdate{Ref: "refs/heads/proposals/1", Old: zero, New: hashB, FastForward: true}
	for _, p := range []PrincipalKind{"", "root", "Human", "HUMAN"} {
		if err := CheckRefUpdate(p, "main", u); !errors.Is(err, ErrRefRejected) {
			t.Fatalf("CheckRefUpdate with principal %q = %v, want rejected", p, err)
		}
	}
	if _, err := ParsePrincipalKind("agent"); err != nil {
		t.Fatalf("ParsePrincipalKind(agent): %v", err)
	}
}

func TestCheckRefUpdateRejectsAnUnusableApprovedBranch(t *testing.T) {
	u := RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: true}
	for _, branch := range []string{"", "refs/heads/main", "ma in", "-main", "a..b"} {
		if err := CheckRefUpdate(PrincipalHuman, branch, u); !errors.Is(err, ErrRefRejected) {
			t.Fatalf("approved branch %q = %v, want rejected", branch, err)
		}
	}
}

func TestValidateBranch(t *testing.T) {
	good := []string{"main", "approved", "proposals/42", "proposals/agent/7", "release-1.0"}
	for _, b := range good {
		if err := ValidateBranch(b); err != nil {
			t.Fatalf("ValidateBranch(%q) = %v, want nil", b, err)
		}
	}
	bad := []string{
		"", "refs/heads/main", "-main", "main/", "/main", "a//b", "a..b",
		"a b", "a~b", "a^b", "a:b", "a?b", "a*b", "a[b", `a\b`, "a@{b", "@",
		"main.lock", ".hidden", "trailing.", "a\tb", "a\x00b",
		strings.Repeat("a", maxRefLen+1),
	}
	for _, b := range bad {
		if err := ValidateBranch(b); err == nil {
			t.Fatalf("ValidateBranch(%q) = nil, want an error", b)
		}
	}
}

func TestValidateRev(t *testing.T) {
	good := []string{
		"main", "proposals/42", "HEAD",
		"1111111111111111111111111111111111111111", "1111111",
	}
	for _, rev := range good {
		if err := ValidateRev(rev); err != nil {
			t.Fatalf("ValidateRev(%q) = %v, want nil", rev, err)
		}
	}
	bad := []string{"", "main^", "main~1", "main@{0}", "a b", "-main", "a..b", ".hidden"}
	for _, rev := range bad {
		if err := ValidateRev(rev); !errors.Is(err, ErrBadRev) {
			t.Fatalf("ValidateRev(%q) = %v, want ErrBadRev", rev, err)
		}
	}
}