eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net"
8 "net/http"
9 "net/http/httptest"
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/coder/websocket"
17)
18
19func TestRouterProxiesByHost(t *testing.T) {
20 // A backend on a unix socket that echoes the Host header it receives.
21 sock := tmpSock(t)
22 ln, err := net.Listen("unix", sock)
23 if err != nil {
24 t.Fatalf("listen unix: %v", err)
25 }
26 backend := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27 io.WriteString(w, "backend:"+r.Host)
28 })}
29 go backend.Serve(ln)
30 t.Cleanup(func() { backend.Close() })
31
32 reg, err := openRegistry(filepath.Join(t.TempDir(), "registry.db"))
33 if err != nil {
34 t.Fatalf("open registry: %v", err)
35 }
36 if err := reg.upsert(Route{Host: "chat.example.com", Kind: "instance", Socket: sock}); err != nil {
37 t.Fatalf("upsert: %v", err)
38 }
39 rt, err := newRouter(reg, nil)
40 if err != nil {
41 t.Fatalf("new router: %v", err)
42 }
43 front := httptest.NewServer(rt)
44 t.Cleanup(front.Close)
45
46 // Known host is proxied, and the backend sees the real Host header.
47 if body, code := proxyGet(t, front.URL, "chat.example.com"); code != 200 || body != "backend:chat.example.com" {
48 t.Fatalf("known host: got %d %q, want 200 backend:chat.example.com", code, body)
49 }
50 // Unknown host → 404 (and, as HostPolicy, no cert would be issued for it).
51 if _, code := proxyGet(t, front.URL, "nope.example.com"); code != http.StatusNotFound {
52 t.Fatalf("unknown host: got %d, want 404", code)
53 }
54 if rt.allowed("nope.example.com") {
55 t.Fatal("allowed() should be false for an unconfigured host")
56 }
57
58 // A new route isn't served until reload; after reload it is (atomic swap).
59 if err := reg.upsert(Route{Host: "two.example.com", Kind: "instance", Socket: sock}); err != nil {
60 t.Fatalf("upsert two: %v", err)
61 }
62 if rt.allowed("two.example.com") {
63 t.Fatal("route should not be live before reload")
64 }
65 if err := rt.reload(); err != nil {
66 t.Fatalf("reload: %v", err)
67 }
68 if body, code := proxyGet(t, front.URL, "two.example.com"); code != 200 || body != "backend:two.example.com" {
69 t.Fatalf("after reload: got %d %q, want 200 backend:two.example.com", code, body)
70 }
71}
72
73func TestRouterProxiesWebSocket(t *testing.T) {
74 // A backend WebSocket echo server on a unix socket.
75 sock := tmpSock(t)
76 ln, err := net.Listen("unix", sock)
77 if err != nil {
78 t.Fatalf("listen unix: %v", err)
79 }
80 backend := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
81 c, err := websocket.Accept(w, r, nil)
82 if err != nil {
83 return
84 }
85 defer c.Close(websocket.StatusNormalClosure, "")
86 mt, data, err := c.Read(r.Context())
87 if err != nil {
88 return
89 }
90 _ = c.Write(r.Context(), mt, append([]byte("echo:"), data...))
91 })}
92 go backend.Serve(ln)
93 t.Cleanup(func() { backend.Close() })
94
95 reg, err := openRegistry(filepath.Join(t.TempDir(), "registry.db"))
96 if err != nil {
97 t.Fatalf("open registry: %v", err)
98 }
99 // Route the loopback host so the client reaches it without a Host override
100 // (ServeHTTP strips the port, matching "127.0.0.1").
101 if err := reg.upsert(Route{Host: "127.0.0.1", Kind: "instance", Socket: sock}); err != nil {
102 t.Fatalf("upsert: %v", err)
103 }
104 rt, err := newRouter(reg, nil)
105 if err != nil {
106 t.Fatalf("new router: %v", err)
107 }
108 front := httptest.NewServer(rt)
109 t.Cleanup(front.Close)
110
111 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
112 defer cancel()
113 wsURL := "ws" + strings.TrimPrefix(front.URL, "http") + "/ws"
114 c, _, err := websocket.Dial(ctx, wsURL, nil)
115 if err != nil {
116 t.Fatalf("ws dial through router: %v", err)
117 }
118 defer c.Close(websocket.StatusNormalClosure, "")
119 if err := c.Write(ctx, websocket.MessageText, []byte("hi")); err != nil {
120 t.Fatalf("ws write: %v", err)
121 }
122 _, data, err := c.Read(ctx)
123 if err != nil {
124 t.Fatalf("ws read: %v", err)
125 }
126 if string(data) != "echo:hi" {
127 t.Fatalf("ws echo through router: got %q, want %q", data, "echo:hi")
128 }
129}
130
131func TestIsRetryableDialErr(t *testing.T) {
132 sock := tmpSock(t) // a path with no listener → ENOENT
133 if _, err := net.Dial("unix", sock); err == nil {
134 t.Fatal("expected a dial error for a missing socket")
135 } else if !isRetryableDialErr(err) {
136 t.Errorf("missing socket: isRetryableDialErr(%v) = false, want true", err)
137 }
138 if isRetryableDialErr(context.Canceled) {
139 t.Error("context.Canceled should not be retryable")
140 }
141}
142
143func TestRouterRetriesAcrossRestart(t *testing.T) {
144 sock := tmpSock(t)
145 reg, err := openRegistry(filepath.Join(t.TempDir(), "registry.db"))
146 if err != nil {
147 t.Fatal(err)
148 }
149 if err := reg.upsert(Route{Host: "chat.example.com", Kind: "instance", Socket: sock}); err != nil {
150 t.Fatal(err)
151 }
152 rt, err := newRouter(reg, nil)
153 if err != nil {
154 t.Fatal(err)
155 }
156 front := httptest.NewServer(rt)
157 t.Cleanup(front.Close)
158
159 // Fire the request before any backend exists; the router must retry the dial.
160 type res struct {
161 code int
162 body string
163 }
164 ch := make(chan res, 1)
165 go func() {
166 req, _ := http.NewRequest(http.MethodGet, front.URL+"/", nil)
167 req.Host = "chat.example.com"
168 resp, err := http.DefaultClient.Do(req)
169 if err != nil {
170 ch <- res{0, "err: " + err.Error()}
171 return
172 }
173 b, _ := io.ReadAll(resp.Body)
174 resp.Body.Close()
175 ch <- res{resp.StatusCode, string(b)}
176 }()
177
178 // Bind the backend after a gap, simulating the instance restart window.
179 time.Sleep(300 * time.Millisecond)
180 ln, err := net.Listen("unix", sock)
181 if err != nil {
182 t.Fatalf("listen: %v", err)
183 }
184 backend := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
185 io.WriteString(w, "late-backend")
186 })}
187 go backend.Serve(ln)
188 t.Cleanup(func() { backend.Close() })
189
190 select {
191 case r := <-ch:
192 if r.code != 200 || r.body != "late-backend" {
193 t.Fatalf("retry across gap: got %d %q, want 200 late-backend", r.code, r.body)
194 }
195 case <-time.After(10 * time.Second):
196 t.Fatal("request never completed; router did not retry across the gap")
197 }
198}
199
200func TestReloadOnlyServesReady(t *testing.T) {
201 reg, err := openRegistry(filepath.Join(t.TempDir(), "registry.db"))
202 if err != nil {
203 t.Fatal(err)
204 }
205 for _, r := range []Route{
206 {Host: "ready.example.com", Kind: "instance", Socket: "/run/lchat/a.sock", Status: "ready"},
207 {Host: "failed.example.com", Kind: "instance", Socket: "/run/lchat/b.sock", Status: "failed"},
208 {Host: "creating.example.com", Kind: "instance", Socket: "/run/lchat/c.sock", Status: "creating"},
209 } {
210 if err := reg.upsert(r); err != nil {
211 t.Fatal(err)
212 }
213 }
214 rt, err := newRouter(reg, nil)
215 if err != nil {
216 t.Fatal(err)
217 }
218 if !rt.allowed("ready.example.com") {
219 t.Error("ready route should be served")
220 }
221 if rt.allowed("failed.example.com") {
222 t.Error("failed route should not be served or cert-eligible")
223 }
224 if rt.allowed("creating.example.com") {
225 t.Error("creating route should not be served")
226 }
227}
228
229func TestWaitHealthy(t *testing.T) {
230 serve := func(status int) string {
231 sock := tmpSock(t)
232 ln, err := net.Listen("unix", sock)
233 if err != nil {
234 t.Fatal(err)
235 }
236 srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(status) })}
237 go srv.Serve(ln)
238 t.Cleanup(func() { srv.Close() })
239 return sock
240 }
241 if err := waitHealthy(serve(200), 3*time.Second); err != nil {
242 t.Errorf("200 backend: waitHealthy = %v, want nil", err)
243 }
244 if err := waitHealthy(serve(500), 1*time.Second); err == nil {
245 t.Error("500 backend: waitHealthy = nil, want error (a broken binary must fail the deploy gate)")
246 }
247}
248
249// proxyGet sends GET base+"/" with an overridden Host header and returns the
250// response body + status code.
251func proxyGet(t *testing.T, base, host string) (string, int) {
252 t.Helper()
253 req, err := http.NewRequest(http.MethodGet, base+"/", nil)
254 if err != nil {
255 t.Fatalf("new request: %v", err)
256 }
257 req.Host = host
258 resp, err := http.DefaultClient.Do(req)
259 if err != nil {
260 t.Fatalf("request: %v", err)
261 }
262 defer resp.Body.Close()
263 b, _ := io.ReadAll(resp.Body)
264 return string(b), resp.StatusCode
265}
266
267// tmpSock returns a short unix socket path (the sun_path limit is ~104 bytes, so
268// t.TempDir() can be too long on macOS) and schedules its removal.
269func tmpSock(t *testing.T) string {
270 t.Helper()
271 s := fmt.Sprintf("/tmp/lchat-rt-%d-%d.sock", os.Getpid(), time.Now().UnixNano()%1_000_000)
272 os.Remove(s)
273 t.Cleanup(func() { os.Remove(s) })
274 return s
275}
276
277func TestRouterUnknownHostFallback(t *testing.T) {
278 reg, err := openRegistry(filepath.Join(t.TempDir(), "registry.db"))
279 if err != nil {
280 t.Fatalf("open registry: %v", err)
281 }
282 rt, err := newRouter(reg, nil)
283 if err != nil {
284 t.Fatalf("new router: %v", err)
285 }
286 rt.fallbackURL = "https://elevenmessenger.com"
287 front := httptest.NewServer(rt)
288 t.Cleanup(front.Close)
289
290 // Unknown host → 302 to the fallback, never a bare 404. 302 (not 301) so a
291 // typo'd host isn't cached as permanently moved.
292 req, _ := http.NewRequest(http.MethodGet, front.URL+"/some/path", nil)
293 req.Host = "typo.elevenmessenger.com"
294 client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
295 resp, err := client.Do(req)
296 if err != nil {
297 t.Fatalf("request: %v", err)
298 }
299 defer resp.Body.Close()
300 if resp.StatusCode != http.StatusFound {
301 t.Fatalf("status = %d, want 302", resp.StatusCode)
302 }
303 if loc := resp.Header.Get("Location"); loc != "https://elevenmessenger.com" {
304 t.Fatalf("Location = %q, want the fallback", loc)
305 }
306
307 // Without a fallback the classic 404 stands (covered again for regressions).
308 rt.fallbackURL = ""
309 if _, code := proxyGet(t, front.URL, "typo.elevenmessenger.com"); code != http.StatusNotFound {
310 t.Fatalf("no fallback: status = %d, want 404", code)
311 }
312}
313