1// SPDX-License-Identifier: MIT
2
3package guest
4
5import (
6 "context"
7 "io"
8 "net"
9 "net/http"
10 "path/filepath"
11 "testing"
12 "time"
13
14 "bnema.dev/ermite-runner/internal/execstream/execpb"
15)
16
17func TestCacheURL(t *testing.T) {
18 for _, tc := range []struct {
19 port uint32
20 want string
21 }{
22 // The toolkit appends "_apis/artifactcache/…" to this: the slash is needed.
23 {9089, "http://127.0.0.1:9089/"},
24 {0, ""},
25 {70000, ""},
26 } {
27 if got := cacheURL(tc.port); got != tc.want {
28 t.Errorf("cacheURL(%d) = %q, want %q", tc.port, got, tc.want)
29 }
30 }
31}
32
33// freePort returns a loopback port nobody listens on.
34func freePort(t *testing.T) uint32 {
35 t.Helper()
36 ln, err := net.Listen("tcp", "127.0.0.1:0")
37 if err != nil {
38 t.Skipf("loopback sockets are not allowed here: %v", err)
39 }
40 defer func() { _ = ln.Close() }()
41 return uint32(ln.Addr().(*net.TCPAddr).Port)
42}
43
44// A job with a cache service gets the relay on its loopback port, and the URL
45// and token for the actions; the relay reaches the cache channel of the host,
46// the sibling of the executor target, and ends with the job.
47func TestStartCacheRelay(t *testing.T) {
48 dir := t.TempDir()
49 execSock := filepath.Join(dir, "exec.sock")
50 ln, err := net.Listen("unix", filepath.Join(dir, "cache.sock"))
51 if err != nil {
52 t.Fatal(err)
53 }
54 srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55 _, _ = io.WriteString(w, "cache says "+r.Header.Get("Authorization"))
56 })}
57 go func() { _ = srv.Serve(ln) }()
58 t.Cleanup(func() { _ = srv.Close() })
59
60 port := freePort(t)
61 spec := &execpb.JobSpec{CachePort: port, CacheToken: "tok"}
62 got := startCacheRelay(context.Background(), Target{Network: "unix", Addr: execSock}, spec)
63 if got.relay == nil || got.token != "tok" || got.url != cacheURL(port) {
64 t.Fatalf("relay %+v", got)
65 }
66
67 req, _ := http.NewRequest("GET", got.url+"_apis/artifactcache/cache", nil)
68 req.Header.Set("Authorization", "Bearer tok")
69 cl := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{DisableKeepAlives: true}}
70 resp, err := cl.Do(req)
71 if err != nil {
72 t.Fatal(err)
73 }
74 body, _ := io.ReadAll(resp.Body)
75 _ = resp.Body.Close()
76 if string(body) != "cache says Bearer tok" {
77 t.Fatalf("got %q", body)
78 }
79
80 got.close()
81 if c, err := net.DialTimeout("tcp", got.relay.Addr().String(), time.Second); err == nil {
82 _ = c.Close()
83 t.Fatal("the relay outlives the job")
84 }
85}
86
87// No cache service, no relay and no variables; a port that is taken leaves the
88// job without a cache instead of failing it.
89func TestStartCacheRelayWithoutServiceOrPort(t *testing.T) {
90 target := Target{Network: "unix", Addr: filepath.Join(t.TempDir(), "exec.sock")}
91 for _, spec := range []*execpb.JobSpec{{}, {CachePort: 9089}, {CacheToken: "tok"}} {
92 if got := startCacheRelay(context.Background(), target, spec); got != (cacheRelay{}) {
93 t.Errorf("%+v: relay %+v", spec, got)
94 }
95 }
96 busy, err := net.Listen("tcp", "127.0.0.1:0")
97 if err != nil {
98 t.Skipf("loopback sockets are not allowed here: %v", err)
99 }
100 defer func() { _ = busy.Close() }()
101 spec := &execpb.JobSpec{CachePort: uint32(busy.Addr().(*net.TCPAddr).Port), CacheToken: "tok"}
102 if got := startCacheRelay(context.Background(), target, spec); got != (cacheRelay{}) {
103 t.Errorf("taken port: relay %+v", got)
104 }
105}