This repository has no description
1package db
2
3import (
4 "slices"
5 "testing"
6 "time"
7)
8
9func TestAddExecutorTokenRejectsDuplicateName(t *testing.T) {
10 d := newTestDB(t)
11
12 if err := d.AddExecutorToken("exec-1", "hash-a", nil, nil); err != nil {
13 t.Fatalf("AddExecutorToken: %v", err)
14 }
15 if err := d.AddExecutorToken("exec-1", "hash-b", nil, nil); err == nil {
16 t.Fatal("AddExecutorToken re-registered an existing name; a duplicate must be rejected")
17 }
18
19 name, _, ok, err := d.ResolveExecutorToken("hash-a")
20 if err != nil {
21 t.Fatalf("ResolveExecutorToken(hash-a): %v", err)
22 }
23 if !ok || name != "exec-1" {
24 t.Fatalf("ResolveExecutorToken(hash-a) = (%q, %v), want (exec-1, true)", name, ok)
25 }
26 if _, _, ok, _ := d.ResolveExecutorToken("hash-b"); ok {
27 t.Fatal("rejected duplicate's token resolved; the failed insert leaked a credential")
28 }
29}
30
31func TestResolveExecutorTokenMissAndHit(t *testing.T) {
32 d := newTestDB(t)
33
34 name, _, ok, err := d.ResolveExecutorToken("no-such-hash")
35 if err != nil {
36 t.Fatalf("ResolveExecutorToken(miss): %v", err)
37 }
38 if ok || name != "" {
39 t.Fatalf("ResolveExecutorToken(miss) = (%q, %v), want (\"\", false)", name, ok)
40 }
41
42 if err := d.AddExecutorToken("exec-1", "hash-1", nil, nil); err != nil {
43 t.Fatalf("AddExecutorToken: %v", err)
44 }
45
46 name, _, ok, err = d.ResolveExecutorToken("hash-1")
47 if err != nil {
48 t.Fatalf("ResolveExecutorToken(hit): %v", err)
49 }
50 if !ok || name != "exec-1" {
51 t.Fatalf("ResolveExecutorToken(hash-1) = (%q, %v), want (exec-1, true)", name, ok)
52 }
53
54 if _, _, ok, _ := d.ResolveExecutorToken("hash-unregistered"); ok {
55 t.Fatal("ResolveExecutorToken matched an unregistered hash")
56 }
57}
58
59func TestRevokeExecutorToken(t *testing.T) {
60 d := newTestDB(t)
61
62 if err := d.AddExecutorToken("exec-1", "hash-1", nil, nil); err != nil {
63 t.Fatalf("AddExecutorToken: %v", err)
64 }
65
66 deleted, err := d.RevokeExecutorToken("exec-1")
67 if err != nil {
68 t.Fatalf("RevokeExecutorToken: %v", err)
69 }
70 if !deleted {
71 t.Fatal("RevokeExecutorToken reported no deletion for an existing identity")
72 }
73
74 if _, _, ok, _ := d.ResolveExecutorToken("hash-1"); ok {
75 t.Fatal("revoked token still resolves; revocation is not enforced")
76 }
77
78 if deleted, err := d.RevokeExecutorToken("exec-1"); err != nil || deleted {
79 t.Fatalf("RevokeExecutorToken(already-gone) = (%v, %v), want (false, nil)", deleted, err)
80 }
81
82 if deleted, err := d.RevokeExecutorToken("ghost"); err != nil || deleted {
83 t.Fatalf("RevokeExecutorToken(unknown) = (%v, %v), want (false, nil)", deleted, err)
84 }
85}
86
87func TestExecutorQuarantineIsVisibleAndReversible(t *testing.T) {
88 d := newTestDB(t)
89 if err := d.AddExecutorToken("exec-1", "hash-1", nil, nil); err != nil {
90 t.Fatalf("AddExecutorToken: %v", err)
91 }
92 if err := d.QuarantineExecutor("exec-1", "missed cancel deadline"); err != nil {
93 t.Fatalf("QuarantineExecutor: %v", err)
94 }
95 if _, _, ok, err := d.ResolveExecutorToken("hash-1"); err != nil || ok {
96 t.Fatalf("ResolveExecutorToken(quarantined) = (ok=%v, err=%v), want (false, nil)", ok, err)
97 }
98 tokens, err := d.ListExecutorTokens()
99 if err != nil {
100 t.Fatalf("ListExecutorTokens: %v", err)
101 }
102 if len(tokens) != 1 || tokens[0].QuarantineReason == nil || *tokens[0].QuarantineReason != "missed cancel deadline" || tokens[0].QuarantinedAt == nil {
103 t.Fatalf("quarantined token not surfaced: %+v", tokens)
104 }
105 if cleared, err := d.ClearExecutorQuarantine("exec-1"); err != nil || !cleared {
106 t.Fatalf("ClearExecutorQuarantine = (%v, %v), want (true, nil)", cleared, err)
107 }
108 if _, _, ok, err := d.ResolveExecutorToken("hash-1"); err != nil || !ok {
109 t.Fatalf("ResolveExecutorToken(cleared) = (ok=%v, err=%v), want (true, nil)", ok, err)
110 }
111 if cleared, err := d.ClearExecutorQuarantine("missing"); err != nil || cleared {
112 t.Fatalf("ClearExecutorQuarantine(missing) = (%v, %v), want (false, nil)", cleared, err)
113 }
114}
115
116func TestListExecutorTokens(t *testing.T) {
117 d := newTestDB(t)
118
119 tokens, err := d.ListExecutorTokens()
120 if err != nil {
121 t.Fatalf("ListExecutorTokens(empty): %v", err)
122 }
123 if len(tokens) != 0 {
124 t.Fatalf("ListExecutorTokens on empty table = %d rows, want 0", len(tokens))
125 }
126
127 // insert out of alphabetical order to test query sorting
128 for _, name := range []string{"charlie", "alice", "bob"} {
129 if err := d.AddExecutorToken(name, "hash-"+name, nil, nil); err != nil {
130 t.Fatalf("AddExecutorToken(%s): %v", name, err)
131 }
132 }
133
134 tokens, err = d.ListExecutorTokens()
135 if err != nil {
136 t.Fatalf("ListExecutorTokens: %v", err)
137 }
138 want := []string{"alice", "bob", "charlie"}
139 if len(tokens) != len(want) {
140 t.Fatalf("ListExecutorTokens = %d rows, want %d", len(tokens), len(want))
141 }
142 for i := range want {
143 if tokens[i].Name != want[i] {
144 t.Fatalf("ListExecutorTokens[%d].Name = %q, want %q (ordered by name)", i, tokens[i].Name, want[i])
145 }
146 }
147}
148
149// expired tokens must fail closed
150func TestResolveExecutorTokenExpiry(t *testing.T) {
151 cases := []struct {
152 name string
153 seqno time.Duration
154 noExpiry bool
155 wantOK bool
156 }{
157 {"future expiry resolves", time.Hour, false, true},
158 {"past expiry fails closed", -time.Hour, false, false},
159 {"nil expiry never expires", 0, true, true},
160 }
161 for _, tc := range cases {
162 t.Run(tc.name, func(t *testing.T) {
163 d := newTestDB(t)
164
165 var expires *time.Time
166 if !tc.noExpiry {
167 exp := time.Now().Add(tc.seqno)
168 expires = &exp
169 }
170 if err := d.AddExecutorToken("exec-1", "hash-1", expires, nil); err != nil {
171 t.Fatalf("AddExecutorToken: %v", err)
172 }
173
174 name, _, ok, err := d.ResolveExecutorToken("hash-1")
175 if err != nil {
176 t.Fatalf("ResolveExecutorToken: %v", err)
177 }
178 if ok != tc.wantOK {
179 t.Fatalf("ResolveExecutorToken ok = %v, want %v", ok, tc.wantOK)
180 }
181 if tc.wantOK && name != "exec-1" {
182 t.Fatalf("ResolveExecutorToken name = %q, want exec-1", name)
183 }
184 if !tc.wantOK && name != "" {
185 t.Fatalf("ResolveExecutorToken name = %q, want \"\" when failing closed", name)
186 }
187 })
188 }
189}
190
191func TestResolveExecutorTokenRejectsMalformedExpiry(t *testing.T) {
192 d := newTestDB(t)
193 if _, err := d.Exec(
194 `insert into mill_executors (name, token_hash, expires_at) values (?, ?, ?)`,
195 "exec-1", "hash-1", "not-a-timestamp",
196 ); err != nil {
197 t.Fatalf("insert malformed token: %v", err)
198 }
199
200 name, _, ok, err := d.ResolveExecutorToken("hash-1")
201 if err == nil {
202 t.Fatal("ResolveExecutorToken accepted a malformed non-NULL expiry")
203 }
204 if ok || name != "" {
205 t.Fatalf("ResolveExecutorToken = (%q, %v, %v), want (\"\", false, error)", name, ok, err)
206 }
207}
208
209// expiry storage is RFC3339 second precision UTC so
210// round trips compare to the second
211func TestListExecutorTokensSurfacesExpiry(t *testing.T) {
212 d := newTestDB(t)
213
214 exp := time.Now().Add(24 * time.Hour)
215 if err := d.AddExecutorToken("expiring", "hash-exp", &exp, nil); err != nil {
216 t.Fatalf("AddExecutorToken(expiring): %v", err)
217 }
218 if err := d.AddExecutorToken("forever", "hash-forever", nil, nil); err != nil {
219 t.Fatalf("AddExecutorToken(forever): %v", err)
220 }
221
222 tokens, err := d.ListExecutorTokens()
223 if err != nil {
224 t.Fatalf("ListExecutorTokens: %v", err)
225 }
226
227 got := make(map[string]*time.Time, len(tokens))
228 for _, tok := range tokens {
229 got[tok.Name] = tok.ExpiresAt
230 }
231
232 e, present := got["forever"]
233 if !present {
234 t.Fatal("ListExecutorTokens omitted the non-expiring identity")
235 }
236 if e != nil {
237 t.Fatalf("forever.ExpiresAt = %v, want nil (never expires)", e)
238 }
239
240 e, present = got["expiring"]
241 if !present {
242 t.Fatal("ListExecutorTokens omitted the expiring identity")
243 }
244 if e == nil {
245 t.Fatal("expiring.ExpiresAt = nil, want the stored expiry")
246 }
247 if e.Unix() != exp.Unix() {
248 t.Fatalf("expiring.ExpiresAt = %d (unix), want %d", e.Unix(), exp.Unix())
249 }
250}
251
252func TestExecutorTokenLabels(t *testing.T) {
253 d := newTestDB(t)
254
255 labels := []string{" foo ", "bar", " foo", ""}
256 if err := d.AddExecutorToken("exec-1", "hash-1", nil, labels); err != nil {
257 t.Fatalf("AddExecutorToken: %v", err)
258 }
259
260 name, resolvedLabels, ok, err := d.ResolveExecutorToken("hash-1")
261 if err != nil {
262 t.Fatalf("ResolveExecutorToken: %v", err)
263 }
264 if !ok || name != "exec-1" {
265 t.Fatalf("ResolveExecutorToken: ok=%v, name=%q, want true, exec-1", ok, name)
266 }
267
268 wantLabels := []string{"bar", "foo"}
269 if !slices.Equal(resolvedLabels, wantLabels) {
270 t.Fatalf("resolved labels = %v, want %v", resolvedLabels, wantLabels)
271 }
272}