carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package agentsock
2
3import (
4 "context"
5 "errors"
6 "net"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "path/filepath"
11 "strings"
12 "sync"
13 "testing"
14 "time"
15)
16
17// fakeWorkers records every effect so a test can assert the ABSENCE of one.
18// "the call was refused" is only half the property; "and nothing happened" is
19// the half that catches an auth check placed after the action.
20type fakeWorkers struct {
21 mu sync.Mutex
22 tokens map[string]string
23 contacts []string
24 due map[string]int64
25 dueErr error
26 slept []string
27 ready map[string]string
28}
29
30func (f *fakeWorkers) WorkerToken(host string) string {
31 f.mu.Lock()
32 defer f.mu.Unlock()
33 return f.tokens[host]
34}
35
36func (f *fakeWorkers) NoteWorkerContact(host string) {
37 f.mu.Lock()
38 defer f.mu.Unlock()
39 f.contacts = append(f.contacts, host)
40}
41
42func (f *fakeWorkers) SetWorkerNextDue(host string, at int64) error {
43 f.mu.Lock()
44 defer f.mu.Unlock()
45 if f.dueErr != nil {
46 return f.dueErr
47 }
48 if f.due == nil {
49 f.due = map[string]int64{}
50 }
51 f.due[host] = at
52 return nil
53}
54
55func (f *fakeWorkers) SleepWorker(host string) {
56 f.mu.Lock()
57 defer f.mu.Unlock()
58 f.slept = append(f.slept, host)
59}
60
61func (f *fakeWorkers) NoteWorkerReady(host, version string) {
62 f.mu.Lock()
63 defer f.mu.Unlock()
64 if f.ready == nil {
65 f.ready = map[string]string{}
66 }
67 f.ready[host] = version
68}
69
70// effects renders every recorded side effect, so a test can assert a refused
71// call changed NOTHING rather than checking one slice and missing the rest.
72func (f *fakeWorkers) effects() (contacts, slept int, due map[string]int64, ready map[string]string) {
73 f.mu.Lock()
74 defer f.mu.Unlock()
75 due = map[string]int64{}
76 for k, v := range f.due {
77 due[k] = v
78 }
79 ready = map[string]string{}
80 for k, v := range f.ready {
81 ready[k] = v
82 }
83 return len(f.contacts), len(f.slept), due, ready
84}
85
86func (f *fakeWorkers) assertNoEffects(t *testing.T, why string) {
87 t.Helper()
88 contacts, slept, due, ready := f.effects()
89 if contacts != 0 || slept != 0 || len(due) != 0 || len(ready) != 0 {
90 t.Fatalf("%s: a refused call must have no effect at all; contacts=%d slept=%d due=%v ready=%v",
91 why, contacts, slept, due, ready)
92 }
93}
94
95func post(h http.Handler, path, host, tok, body string) *httptest.ResponseRecorder {
96 req := httptest.NewRequest("POST", path, strings.NewReader(body))
97 if host != "" {
98 req.Header.Set("Carlos-Sidecar-Host", host)
99 }
100 if tok != "" {
101 req.Header.Set("Authorization", "Bearer "+tok)
102 }
103 rec := httptest.NewRecorder()
104 h.ServeHTTP(rec, req)
105 return rec
106}
107
108func waitFor(t *testing.T, what string, cond func() bool) {
109 t.Helper()
110 deadline := time.Now().Add(5 * time.Second)
111 for time.Now().Before(deadline) {
112 if cond() {
113 return
114 }
115 time.Sleep(2 * time.Millisecond)
116 }
117 t.Fatalf("timed out waiting for %s", what)
118}
119
120var allVerbs = []string{"/v1/ready", "/v1/next-due", "/v1/idle", "/v1/heartbeat"}
121
122func TestAgentSockRejectsWrongToken(t *testing.T) {
123 f := &fakeWorkers{tokens: map[string]string{"bot.x.internal": "good"}}
124 h := New(f).Handler()
125
126 for _, tc := range []struct{ name, host, tok string }{
127 {"wrong token", "bot.x.internal", "bad"},
128 {"no token", "bot.x.internal", ""},
129 {"unknown host", "nope.x.internal", "good"},
130 {"token for another host", "other.x.internal", "good"},
131 {"no host header", "", "good"},
132 {"empty-string token against an asleep host", "asleep.x.internal", ""},
133 } {
134 rec := post(h, "/v1/heartbeat", tc.host, tc.tok, "")
135 if rec.Code != http.StatusUnauthorized {
136 t.Fatalf("%s: code = %d, want 401", tc.name, rec.Code)
137 }
138 }
139 f.assertNoEffects(t, "wrong-token sweep")
140}
141
142// The inversion this guards is total: WorkerToken("") means "unknown, asleep,
143// or stopped", never "no token required". A caller who presents nothing must
144// not match a host that holds nothing.
145func TestAgentSockEmptyStoredTokenIsNotABypass(t *testing.T) {
146 // "asleep" is present in the map with the empty token the real activator
147 // returns for a host that is not currently awake or waking.
148 f := &fakeWorkers{tokens: map[string]string{"asleep.x.internal": ""}}
149 h := New(f).Handler()
150
151 for _, tok := range []string{"", " ", "Bearer", "anything"} {
152 for _, path := range allVerbs {
153 body := ""
154 switch path {
155 case "/v1/ready":
156 body = `{"version":"v1"}`
157 case "/v1/next-due":
158 body = `{"at":1756}`
159 }
160 rec := post(h, path, "asleep.x.internal", tok, body)
161 if rec.Code != http.StatusUnauthorized {
162 t.Fatalf("%s with token %q: code = %d, want 401", path, tok, rec.Code)
163 }
164 }
165 }
166 // The bare header form, which no post() helper shape produces: an
167 // Authorization header whose credential is literally empty.
168 req := httptest.NewRequest("POST", "/v1/heartbeat", nil)
169 req.Header.Set("Carlos-Sidecar-Host", "asleep.x.internal")
170 req.Header.Set("Authorization", "Bearer ")
171 rec := httptest.NewRecorder()
172 h.ServeHTTP(rec, req)
173 if rec.Code != http.StatusUnauthorized {
174 t.Fatalf("empty bearer credential: code = %d, want 401", rec.Code)
175 }
176 f.assertNoEffects(t, "empty stored token")
177}
178
179// A caller that names no host is refused HERE, not by the accident that the
180// activator's map happens to hold no "" key. This fake holds one deliberately:
181// if the refusal were delegated to WorkerToken, an anonymous caller presenting
182// this token would be authorized — and then act on host "", whatever that
183// turns out to mean downstream.
184func TestAgentSockAnonymousCallerIsRefusedHere(t *testing.T) {
185 f := &fakeWorkers{tokens: map[string]string{"": "sekrit", "bot.x.internal": "good"}}
186 h := New(f).Handler()
187 for _, path := range allVerbs {
188 body := ""
189 switch path {
190 case "/v1/ready":
191 body = `{"version":"v1"}`
192 case "/v1/next-due":
193 body = `{"at":1756}`
194 }
195 if rec := post(h, path, "", "sekrit", body); rec.Code != http.StatusUnauthorized {
196 t.Fatalf("%s with no host header: code = %d, want 401 — an unnamed caller has nothing to authorize", path, rec.Code)
197 }
198 }
199 f.assertNoEffects(t, "anonymous caller")
200}
201
202func TestAgentSockTokenIsScopedToOneHost(t *testing.T) {
203 // The security property: possession of A's token says nothing about B.
204 f := &fakeWorkers{tokens: map[string]string{"a.x.internal": "tok-a", "b.x.internal": "tok-b"}}
205 h := New(f).Handler()
206 if rec := post(h, "/v1/idle", "b.x.internal", "tok-a", ""); rec.Code != http.StatusUnauthorized {
207 t.Fatalf("A's token spoke about B: code = %d", rec.Code)
208 }
209 if rec := post(h, "/v1/ready", "b.x.internal", "tok-a", `{"version":"pwned"}`); rec.Code != http.StatusUnauthorized {
210 t.Fatalf("A's token reported B's version: code = %d", rec.Code)
211 }
212 if rec := post(h, "/v1/next-due", "b.x.internal", "tok-a", `{"at":1}`); rec.Code != http.StatusUnauthorized {
213 t.Fatalf("A's token rescheduled B: code = %d", rec.Code)
214 }
215 if rec := post(h, "/v1/heartbeat", "b.x.internal", "tok-a", ""); rec.Code != http.StatusUnauthorized {
216 t.Fatalf("A's token kept B alive: code = %d", rec.Code)
217 }
218 f.assertNoEffects(t, "cross-host token")
219}
220
221// Every refusal reason must be indistinguishable on the wire: status, headers
222// worth reading, and body. A caller must not be able to probe "does host X
223// exist" or "is host X awake" by reading the difference.
224func TestAgentSockRefusalsAreIndistinguishable(t *testing.T) {
225 f := &fakeWorkers{tokens: map[string]string{
226 "awake.x.internal": "good",
227 "asleep.x.internal": "",
228 }}
229 h := New(f).Handler()
230
231 type shape struct {
232 code int
233 ctype, body string
234 }
235 var seen []shape
236 for _, tc := range []struct{ name, host, tok string }{
237 {"wrong token", "awake.x.internal", "bad"},
238 {"wrong token, same length", "awake.x.internal", "goo1"},
239 {"absent token", "awake.x.internal", ""},
240 {"unknown host", "ghost.x.internal", "good"},
241 {"asleep host", "asleep.x.internal", "good"},
242 {"absent host header", "", "good"},
243 } {
244 rec := post(h, "/v1/heartbeat", tc.host, tc.tok, "")
245 seen = append(seen, shape{rec.Code, rec.Header().Get("Content-Type"), rec.Body.String()})
246 if rec.Code != http.StatusUnauthorized {
247 t.Fatalf("%s: code = %d, want 401", tc.name, rec.Code)
248 }
249 }
250 for i := 1; i < len(seen); i++ {
251 if seen[i] != seen[0] {
252 t.Fatalf("refusals are distinguishable: %+v vs %+v", seen[0], seen[i])
253 }
254 }
255 f.assertNoEffects(t, "indistinguishable refusals")
256}
257
258func TestAgentSockVerbs(t *testing.T) {
259 f := &fakeWorkers{tokens: map[string]string{"bot.x.internal": "good"}}
260 h := New(f).Handler()
261
262 if rec := post(h, "/v1/ready", "bot.x.internal", "good", `{"version":"abc123"}`); rec.Code != http.StatusNoContent {
263 t.Fatalf("ready: %d", rec.Code)
264 }
265 if f.ready["bot.x.internal"] != "abc123" {
266 t.Fatal("ready must record the running version — it is a sidecar's only version signal")
267 }
268 if rec := post(h, "/v1/next-due", "bot.x.internal", "good", `{"at":1756}`); rec.Code != http.StatusNoContent {
269 t.Fatalf("next-due: %d", rec.Code)
270 }
271 if f.due["bot.x.internal"] != 1756 {
272 t.Fatalf("due = %d", f.due["bot.x.internal"])
273 }
274 if rec := post(h, "/v1/idle", "bot.x.internal", "good", ""); rec.Code != http.StatusNoContent {
275 t.Fatalf("idle: %d", rec.Code)
276 }
277 if len(f.slept) != 1 {
278 t.Fatal("idle must ask for a stop")
279 }
280 if rec := post(h, "/v1/heartbeat", "bot.x.internal", "good", ""); rec.Code != http.StatusNoContent {
281 t.Fatalf("heartbeat: %d", rec.Code)
282 }
283 if len(f.contacts) != 1 || f.contacts[0] != "bot.x.internal" {
284 t.Fatalf("heartbeat must stamp contact for the CALLER's host; contacts = %v", f.contacts)
285 }
286}
287
288// The verb acts on the host the TOKEN authorized, never on a host named
289// anywhere in the body — there is no host field in any body, and adding one
290// would be the confused-deputy bug this pins.
291func TestAgentSockActsOnlyOnTheAuthorizedHost(t *testing.T) {
292 f := &fakeWorkers{tokens: map[string]string{"a.x.internal": "tok-a", "b.x.internal": "tok-b"}}
293 h := New(f).Handler()
294
295 if rec := post(h, "/v1/next-due", "a.x.internal", "tok-a", `{"at":99,"host":"b.x.internal"}`); rec.Code != http.StatusNoContent {
296 t.Fatalf("next-due: %d", rec.Code)
297 }
298 if _, ok := f.due["b.x.internal"]; ok {
299 t.Fatal("a body field must never redirect an effect onto another host")
300 }
301 if f.due["a.x.internal"] != 99 {
302 t.Fatalf("due for the authorized host = %d, want 99", f.due["a.x.internal"])
303 }
304}
305
306func TestAgentSockRefusesGarbage(t *testing.T) {
307 f := &fakeWorkers{tokens: map[string]string{"bot.x.internal": "good"}}
308 h := New(f).Handler()
309 for _, body := range []string{"not json", `{"at":"soon"}`, strings.Repeat("x", 1<<20), `{"at":-1}`} {
310 rec := post(h, "/v1/next-due", "bot.x.internal", "good", body)
311 if rec.Code != http.StatusBadRequest {
312 t.Fatalf("body %.20q: code = %d, want 400", body, rec.Code)
313 }
314 }
315 // An oversized body on a JSON-bearing verb must be refused, not truncated
316 // into something that happens to parse.
317 big := `{"version":"` + strings.Repeat("v", 1<<20) + `"}`
318 if rec := post(h, "/v1/ready", "bot.x.internal", "good", big); rec.Code != http.StatusBadRequest {
319 t.Fatalf("oversized ready: code = %d, want 400", rec.Code)
320 }
321 // The SIZE CAP itself, not the field bounds behind it: a body that is
322 // perfectly well-formed and whose fields are all in range, but megabytes
323 // long. Without a bounded reader this parses and acts.
324 pad := strings.Repeat("x", 1<<20)
325 if rec := post(h, "/v1/next-due", "bot.x.internal", "good", `{"at":1756,"pad":"`+pad+`"}`); rec.Code != http.StatusBadRequest {
326 t.Fatalf("padded next-due: code = %d, want 400 — the body must be read under a cap", rec.Code)
327 }
328 if rec := post(h, "/v1/ready", "bot.x.internal", "good", `{"version":"abc","pad":"`+pad+`"}`); rec.Code != http.StatusBadRequest {
329 t.Fatalf("padded ready: code = %d, want 400 — the body must be read under a cap", rec.Code)
330 }
331 for _, body := range []string{"not json", `{"version":42}`, "{\"version\":\"a\\u0000b\"}"} {
332 if rec := post(h, "/v1/ready", "bot.x.internal", "good", body); rec.Code != http.StatusBadRequest {
333 t.Fatalf("ready body %.20q: code = %d, want 400", body, rec.Code)
334 }
335 }
336 if _, ok := f.due["bot.x.internal"]; ok {
337 t.Fatal("a malformed body must leave no effect")
338 }
339 if len(f.ready) != 0 {
340 t.Fatalf("a malformed ready must leave no effect: %v", f.ready)
341 }
342
343 if rec := post(h, "/v1/nope", "bot.x.internal", "good", ""); rec.Code != http.StatusNotFound {
344 t.Fatalf("unknown verb: %d", rec.Code)
345 }
346 req := httptest.NewRequest("GET", "/v1/idle", nil)
347 // GET on a POST-only verb
348 rec := httptest.NewRecorder()
349 h.ServeHTTP(rec, req)
350 if rec.Code != http.StatusMethodNotAllowed && rec.Code != http.StatusUnauthorized {
351 t.Fatalf("GET: %d", rec.Code)
352 }
353}
354
355// A GET must not act even when it carries a perfectly good credential — the
356// method gate has to come before the effect, not merely before the 200.
357func TestAgentSockGetNeverActs(t *testing.T) {
358 f := &fakeWorkers{tokens: map[string]string{"bot.x.internal": "good"}}
359 h := New(f).Handler()
360 for _, path := range allVerbs {
361 for _, method := range []string{"GET", "HEAD", "PUT", "DELETE"} {
362 req := httptest.NewRequest(method, path, nil)
363 req.Header.Set("Carlos-Sidecar-Host", "bot.x.internal")
364 req.Header.Set("Authorization", "Bearer good")
365 rec := httptest.NewRecorder()
366 h.ServeHTTP(rec, req)
367 if rec.Code == http.StatusNoContent {
368 t.Fatalf("%s %s succeeded; only POST may act", method, path)
369 }
370 }
371 }
372 f.assertNoEffects(t, "non-POST methods")
373}
374
375// An empty next-due body is malformed: "at" is the whole message, and a
376// zero-valued default would silently reschedule the worker to the epoch.
377func TestAgentSockNextDueRequiresAt(t *testing.T) {
378 f := &fakeWorkers{tokens: map[string]string{"bot.x.internal": "good"}}
379 h := New(f).Handler()
380 for _, body := range []string{"", "{}", "null"} {
381 if rec := post(h, "/v1/next-due", "bot.x.internal", "good", body); rec.Code != http.StatusBadRequest {
382 t.Fatalf("next-due body %q: code = %d, want 400", body, rec.Code)
383 }
384 }
385 if len(f.due) != 0 {
386 t.Fatalf("due must be untouched: %v", f.due)
387 }
388}
389
390// The version field must be PRESENT — a typo'd body is a loud 400, never a
391// silent "unknown" that also ends the settle window. It may be empty, which is
392// a child honestly saying it does not know its own build.
393func TestAgentSockReadyRequiresTheVersionField(t *testing.T) {
394 f := &fakeWorkers{tokens: map[string]string{"bot.x.internal": "good"}}
395 h := New(f).Handler()
396 for _, body := range []string{"", "{}", "null", `{"verison":"abc"}`} {
397 if rec := post(h, "/v1/ready", "bot.x.internal", "good", body); rec.Code != http.StatusBadRequest {
398 t.Fatalf("ready body %q: code = %d, want 400", body, rec.Code)
399 }
400 }
401 if len(f.ready) != 0 {
402 t.Fatalf("ready must be untouched: %v", f.ready)
403 }
404 if rec := post(h, "/v1/ready", "bot.x.internal", "good", `{"version":""}`); rec.Code != http.StatusNoContent {
405 t.Fatalf("explicitly-unknown version: code = %d, want 204", rec.Code)
406 }
407 if v, ok := f.ready["bot.x.internal"]; !ok || v != "" {
408 t.Fatalf("ready = %q, %v; an explicit empty version is honest, and still ready", v, ok)
409 }
410}
411
412// Bodyless verbs stay bodyless: idle and heartbeat carry no arguments, so
413// nothing a caller sends can steer them.
414func TestAgentSockBodylessVerbsIgnoreTheirBody(t *testing.T) {
415 f := &fakeWorkers{tokens: map[string]string{"bot.x.internal": "good"}}
416 h := New(f).Handler()
417 if rec := post(h, "/v1/heartbeat", "bot.x.internal", "good", `{"host":"other.x.internal"}`); rec.Code != http.StatusNoContent {
418 t.Fatalf("heartbeat with a body: %d", rec.Code)
419 }
420 if len(f.contacts) != 1 || f.contacts[0] != "bot.x.internal" {
421 t.Fatalf("contacts = %v", f.contacts)
422 }
423}
424
425func TestAgentSockNextDueStoreFailureIsNot204(t *testing.T) {
426 f := &fakeWorkers{
427 tokens: map[string]string{"bot.x.internal": "good"},
428 dueErr: errors.New("index write failed"),
429 }
430 h := New(f).Handler()
431 rec := post(h, "/v1/next-due", "bot.x.internal", "good", `{"at":1756}`)
432 if rec.Code != http.StatusInternalServerError {
433 t.Fatalf("code = %d, want 500 — a child must learn its schedule did not land", rec.Code)
434 }
435}
436
437func TestAgentSockServeCleansUpItsSocket(t *testing.T) {
438 dir := t.TempDir()
439 path := filepath.Join(dir, "agent.sock")
440 ctx, cancel := context.WithCancel(context.Background())
441 done := make(chan error, 1)
442 go func() { done <- New(&fakeWorkers{}).Serve(ctx, path) }()
443 waitFor(t, "socket to appear", func() bool { _, err := os.Stat(path); return err == nil })
444 cancel()
445 if err := <-done; err != nil {
446 t.Fatalf("Serve returned %v on a clean shutdown", err)
447 }
448 if _, err := os.Stat(path); !os.IsNotExist(err) {
449 t.Fatal("a stale socket file must not survive shutdown — the next start would fail to bind")
450 }
451}
452
453// A hard kill leaves the socket file behind. The next start must take it over,
454// not die on "address already in use".
455func TestAgentSockServeReplacesAStaleSocketFile(t *testing.T) {
456 dir := t.TempDir()
457 path := filepath.Join(dir, "agent.sock")
458 if err := os.WriteFile(path, []byte("stale"), 0o600); err != nil {
459 t.Fatal(err)
460 }
461 ctx, cancel := context.WithCancel(context.Background())
462 defer cancel()
463 done := make(chan error, 1)
464 go func() { done <- New(&fakeWorkers{}).Serve(ctx, path) }()
465 waitFor(t, "socket to appear", func() bool {
466 c, err := net.Dial("unix", path)
467 if err != nil {
468 return false
469 }
470 c.Close()
471 return true
472 })
473 cancel()
474 <-done
475}
476
477// The other half of stale-socket removal: a socket somebody is LISTENING on is
478// not stale. Unlinking it would orphan the live agent — its children would go
479// on talking to a process nothing else can reach — so refuse instead.
480func TestAgentSockServeRefusesALiveSocket(t *testing.T) {
481 dir := t.TempDir()
482 path := filepath.Join(dir, "agent.sock")
483 ln, err := net.Listen("unix", path)
484 if err != nil {
485 t.Fatal(err)
486 }
487 defer ln.Close()
488 go func() {
489 for {
490 c, err := ln.Accept()
491 if err != nil {
492 return
493 }
494 c.Close()
495 }
496 }()
497
498 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
499 defer cancel()
500 if err := New(&fakeWorkers{}).Serve(ctx, path); err == nil {
501 t.Fatal("Serve must refuse a socket another listener already owns")
502 }
503 if _, err := os.Stat(path); err != nil {
504 t.Fatalf("the live socket was unlinked: %v", err)
505 }
506}
507
508// End to end over a real unix socket: the shape a sidecar actually dials.
509func TestAgentSockOverTheRealSocket(t *testing.T) {
510 dir := t.TempDir()
511 path := filepath.Join(dir, "agent.sock")
512 f := &fakeWorkers{tokens: map[string]string{"bot.x.internal": "good"}}
513 ctx, cancel := context.WithCancel(context.Background())
514 done := make(chan error, 1)
515 go func() { done <- New(f).Serve(ctx, path) }()
516 defer func() { cancel(); <-done }()
517 waitFor(t, "socket to appear", func() bool { _, err := os.Stat(path); return err == nil })
518
519 cl := &http.Client{
520 Transport: &http.Transport{
521 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
522 return (&net.Dialer{}).DialContext(ctx, "unix", path)
523 },
524 },
525 Timeout: 5 * time.Second,
526 }
527 call := func(tok string) int {
528 req, err := http.NewRequest("POST", "http://agent/v1/heartbeat", nil)
529 if err != nil {
530 t.Fatal(err)
531 }
532 req.Header.Set("Carlos-Sidecar-Host", "bot.x.internal")
533 req.Header.Set("Authorization", "Bearer "+tok)
534 resp, err := cl.Do(req)
535 if err != nil {
536 t.Fatal(err)
537 }
538 resp.Body.Close()
539 return resp.StatusCode
540 }
541 if code := call("good"); code != http.StatusNoContent {
542 t.Fatalf("live heartbeat: %d", code)
543 }
544 if code := call("bad"); code != http.StatusUnauthorized {
545 t.Fatalf("live bad token: %d", code)
546 }
547
548 // The mode is checked only AFTER a request has been answered, and the
549 // ordering is the whole point: Serve chmods before it starts serving, so
550 // a completed response is proof the chmod has happened. Polling on the
551 // file merely EXISTING returns inside the listen→chmod window, where an
552 // unlucky read sees whatever the umask left (0755 under the common 022)
553 // and fails a correct implementation.
554 if fi, err := os.Stat(path); err != nil {
555 t.Fatal(err)
556 } else if perm := fi.Mode().Perm(); perm&0o007 != 0 {
557 t.Fatalf("socket mode %o is reachable by other — the token is the boundary, but the mode should not widen the audience", perm)
558 }
559 contacts, _, _, _ := f.effects()
560 if contacts != 1 {
561 t.Fatalf("contacts = %d, want exactly the authorized one", contacts)
562 }
563}
564
565// Many children hammering one agent at once — the handler holds no per-request
566// state, and -race proves it.
567func TestAgentSockConcurrentCallers(t *testing.T) {
568 f := &fakeWorkers{tokens: map[string]string{"a.x.internal": "tok-a", "b.x.internal": "tok-b"}}
569 h := New(f).Handler()
570 var wg sync.WaitGroup
571 for i := range 50 {
572 wg.Add(1)
573 go func(i int) {
574 defer wg.Done()
575 if i%2 == 0 {
576 post(h, "/v1/heartbeat", "a.x.internal", "tok-a", "")
577 } else {
578 post(h, "/v1/heartbeat", "b.x.internal", "tok-a", "")
579 }
580 }(i)
581 }
582 wg.Wait()
583 contacts, _, _, _ := f.effects()
584 if contacts != 25 {
585 t.Fatalf("contacts = %d, want 25 — only a.x.internal's own token may stamp it", contacts)
586 }
587}
588