tinyrelayfile
tinyrelay/tinyclient/remote_browser_test.go/

tinyclient/remote_browser_test.go raw
1package tinyclient
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "strings"
8 "sync/atomic"
9 "testing"
10 "time"
11
12 "github.com/coder/websocket"
13)
14
15func TestRemoteKeepsTenantPrefixInPagesAssetsAndRedirects(t *testing.T) {
16 backend := &fakeBackend{policy: DefaultPolicy(strings.Repeat("a", 64))}
17 app, err := New(backend, Options{})
18 if err != nil {
19 t.Fatal(err)
20 }
21 upstream := httptest.NewServer(http.StripPrefix("/r/team", app))
22 defer upstream.Close()
23 remote, err := NewRemote(RemoteOptions{BackendURL: upstream.URL + "/r/team", PublicURL: "http://relay.example/r/team"})
24 if err != nil {
25 t.Fatal(err)
26 }
27 for _, path := range []string{"/r/team/", "/r/team/account", "/r/team/manifest.webmanifest", "/r/team/scripts/bridge.js", "/r/team/rooms"} {
28 got := httptest.NewRecorder()
29 remote.ServeHTTP(got, httptest.NewRequest("GET", "http://relay.example"+path, nil))
30 if path == "/r/team/rooms" {
31 if got.Code != 301 || got.Header().Get("Location") != "/r/team/chat" {
32 t.Fatalf("redirect: %d %v", got.Code, got.Header())
33 }
34 continue
35 }
36 if got.Code != 200 {
37 t.Fatalf("prefix %s: %d", path, got.Code)
38 }
39 if path == "/r/team/" && !strings.Contains(got.Body.String(), `src="/r/team/scripts/bridge.js?`) {
40 t.Fatal("page dropped tenant asset prefix")
41 }
42 if path == "/r/team/manifest.webmanifest" && !strings.Contains(got.Body.String(), `"start_url": "/r/team/"`) {
43 t.Fatal("manifest dropped tenant prefix")
44 }
45 }
46 response := httptest.NewRecorder()
47 remote.ServeHTTP(response, httptest.NewRequest("GET", "http://relay.example/r/other/", nil))
48 if response.Code != 404 {
49 t.Fatalf("escaped tenant prefix: %d", response.Code)
50 }
51}
52
53func TestRemoteProxiesWebSocketUpgrades(t *testing.T) {
54 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55 conn, err := websocket.Accept(w, r, nil)
56 if err != nil {
57 return
58 }
59 defer conn.CloseNow()
60 kind, body, err := conn.Read(r.Context())
61 if err == nil {
62 _ = conn.Write(r.Context(), kind, body)
63 }
64 }))
65 defer upstream.Close()
66 frontend := httptest.NewUnstartedServer(nil)
67 public := "http://" + frontend.Listener.Addr().String()
68 handler, err := NewRemote(RemoteOptions{BackendURL: upstream.URL, PublicURL: public})
69 if err != nil {
70 t.Fatal(err)
71 }
72 frontend.Config.Handler = handler
73 frontend.Start()
74 defer frontend.Close()
75 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
76 defer cancel()
77 conn, _, err := websocket.Dial(ctx, frontend.URL, &websocket.DialOptions{HTTPHeader: http.Header{"Origin": {public}}})
78 if err != nil {
79 t.Fatal(err)
80 }
81 defer conn.CloseNow()
82 if err := conn.Write(ctx, websocket.MessageText, []byte(`["REQ","test",{}]`)); err != nil {
83 t.Fatal(err)
84 }
85 _, body, err := conn.Read(ctx)
86 if err != nil || string(body) != `["REQ","test",{}]` {
87 t.Fatalf("websocket relay: %s %v", body, err)
88 }
89}
90
91func TestRemoteServesImmutableAssetsWithoutBackendReads(t *testing.T) {
92 var calls atomic.Int32
93 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
94 calls.Add(1)
95 http.Error(w, "backend should not be called for immutable assets", http.StatusBadGateway)
96 }))
97 remote, err := NewRemote(RemoteOptions{BackendURL: upstream.URL, PublicURL: "http://relay.example"})
98 if err != nil {
99 t.Fatal(err)
100 }
101 upstream.Close()
102 for _, method := range []string{http.MethodGet, http.MethodHead} {
103 for _, path := range []string{"/scripts/bridge.js", "/signer.js", "/fixi.js", "/sw.js", "/icon.svg", "/icon-192.png"} {
104 recorder := httptest.NewRecorder()
105 request := httptest.NewRequest(method, "http://relay.example"+path, nil)
106 request.AddCookie(&http.Cookie{Name: "tiny_session", Value: "session"})
107 remote.ServeHTTP(recorder, request)
108 if recorder.Code != http.StatusOK || calls.Load() != 0 {
109 t.Fatalf("%s %s: status=%d backend calls=%d", method, path, recorder.Code, calls.Load())
110 }
111 }
112 }
113 for _, path := range []string{"/r/other/scripts/bridge.js", "/r/other/icon.svg"} {
114 recorder := httptest.NewRecorder()
115 remote.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "http://relay.example"+path, nil))
116 if recorder.Code != http.StatusNotFound || calls.Load() != 0 {
117 t.Fatalf("cross-tenant %s: status=%d backend calls=%d", path, recorder.Code, calls.Load())
118 }
119 }
120}
121
122func TestRemoteEmbeddedAssetsMatchIntegratedClient(t *testing.T) {
123 app, err := New(&fakeBackend{policy: DefaultPolicy(strings.Repeat("a", 64))}, Options{})
124 if err != nil {
125 t.Fatal(err)
126 }
127 var calls atomic.Int32
128 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129 calls.Add(1)
130 http.Error(w, "unexpected upstream request", http.StatusBadGateway)
131 }))
132 defer upstream.Close()
133 paths := []string{"/scripts/bridge.js", "/scripts/bridge.js?v=" + scriptsVersion, "/signer.js", "/fixi.js", "/sw.js", "/icon.svg", "/icon-mono.svg", "/icon-192.png"}
134 for _, prefix := range []string{"", "/r/team"} {
135 remote, err := NewRemote(RemoteOptions{BackendURL: upstream.URL + prefix, PublicURL: "http://relay.example" + prefix})
136 if err != nil {
137 t.Fatal(err)
138 }
139 for _, method := range []string{http.MethodGet, http.MethodHead} {
140 for _, path := range paths {
141 request := httptest.NewRequest(method, "http://relay.example"+prefix+path, nil)
142 request.AddCookie(&http.Cookie{Name: "tiny_session", Value: "session"})
143 got, want := httptest.NewRecorder(), httptest.NewRecorder()
144 remote.ServeHTTP(got, request)
145 local := request.Clone(request.Context())
146 local.URL.Path = strings.TrimPrefix(local.URL.Path, prefix)
147 app.ServeHTTP(want, local)
148 if got.Code != want.Code || got.Body.String() != want.Body.String() {
149 t.Fatalf("%s %s%s: standalone status/body differs from integrated", method, prefix, path)
150 }
151 if method == http.MethodHead && got.Body.Len() != 0 {
152 t.Fatalf("HEAD %s carries a response body", path)
153 }
154 for _, header := range []string{"Content-Type", "Cache-Control", "Service-Worker-Allowed"} {
155 if got.Header().Get(header) != want.Header().Get(header) {
156 t.Fatalf("%s %s: %s=%q, want %q", method, path, header, got.Header().Get(header), want.Header().Get(header))
157 }
158 }
159 }
160 }
161 }
162 if calls.Load() != 0 {
163 t.Fatalf("embedded assets made %d upstream requests", calls.Load())
164 }
165}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs