golang.org/x/playground@v0.0.0-20230418134305-14ebe15bcd59/server_test.go (about) 1 // Copyright 2017 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bytes" 9 "context" 10 "encoding/json" 11 "fmt" 12 "io" 13 "net/http" 14 "net/http/httptest" 15 "os" 16 "runtime" 17 "sync" 18 "testing" 19 "time" 20 21 "github.com/bradfitz/gomemcache/memcache" 22 "github.com/google/go-cmp/cmp" 23 ) 24 25 type testLogger struct { 26 t *testing.T 27 } 28 29 func (l testLogger) Printf(format string, args ...interface{}) { 30 l.t.Logf(format, args...) 31 } 32 func (l testLogger) Errorf(format string, args ...interface{}) { 33 l.t.Errorf(format, args...) 34 } 35 func (l testLogger) Fatalf(format string, args ...interface{}) { 36 l.t.Fatalf(format, args...) 37 } 38 39 func testingOptions(t *testing.T) func(s *server) error { 40 return func(s *server) error { 41 s.db = &inMemStore{} 42 s.log = testLogger{t} 43 var err error 44 s.examples, err = newExamplesHandler(false, time.Now()) 45 if err != nil { 46 return err 47 } 48 return nil 49 } 50 } 51 52 func TestEdit(t *testing.T) { 53 s, err := newServer(testingOptions(t)) 54 if err != nil { 55 t.Fatalf("newServer(testingOptions(t)): %v", err) 56 } 57 id := "bar" 58 barBody := []byte("Snippy McSnipface") 59 snip := &snippet{Body: barBody} 60 if err := s.db.PutSnippet(context.Background(), id, snip); err != nil { 61 t.Fatalf("s.dbPutSnippet(context.Background(), %+v, %+v): %v", id, snip, err) 62 } 63 64 testCases := []struct { 65 desc string 66 method string 67 url string 68 statusCode int 69 headers map[string]string 70 respBody []byte 71 }{ 72 {"OPTIONS no-op", http.MethodOptions, "https://play.golang.org/p/foo", http.StatusOK, nil, nil}, 73 {"foo.play.golang.org to play.golang.org", http.MethodGet, "https://foo.play.golang.org", http.StatusFound, map[string]string{"Location": "https://play.golang.org"}, nil}, 74 {"Non-existent page", http.MethodGet, "https://play.golang.org/foo", http.StatusNotFound, nil, nil}, 75 {"Unknown snippet", http.MethodGet, "https://play.golang.org/p/foo", http.StatusNotFound, nil, nil}, 76 {"Existing snippet", http.MethodGet, "https://play.golang.org/p/" + id, http.StatusFound, nil, nil}, 77 {"Plaintext snippet", http.MethodGet, "https://play.golang.org/p/" + id + ".go", http.StatusOK, nil, barBody}, 78 {"Download snippet", http.MethodGet, "https://play.golang.org/p/" + id + ".go?download=true", http.StatusOK, map[string]string{"Content-Disposition": fmt.Sprintf(`attachment; filename="%s.go"`, id)}, barBody}, 79 } 80 81 for _, tc := range testCases { 82 req := httptest.NewRequest(tc.method, tc.url, nil) 83 w := httptest.NewRecorder() 84 s.handleEdit(w, req) 85 resp := w.Result() 86 corsHeader := "Access-Control-Allow-Origin" 87 if got, want := resp.Header.Get(corsHeader), "*"; got != want { 88 t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) 89 } 90 if got, want := resp.StatusCode, tc.statusCode; got != want { 91 t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) 92 } 93 for k, v := range tc.headers { 94 if got, want := resp.Header.Get(k), v; got != want { 95 t.Errorf("Got header value %q of %q; want %q", k, got, want) 96 } 97 } 98 if tc.respBody != nil { 99 defer resp.Body.Close() 100 b, err := io.ReadAll(resp.Body) 101 if err != nil { 102 t.Errorf("%s: io.ReadAll(resp.Body): %v", tc.desc, err) 103 } 104 if !bytes.Equal(b, tc.respBody) { 105 t.Errorf("%s: got unexpected body %q; want %q", tc.desc, b, tc.respBody) 106 } 107 } 108 } 109 } 110 111 func TestServer(t *testing.T) { 112 s, err := newServer(testingOptions(t)) 113 if err != nil { 114 t.Fatalf("newServer(testingOptions(t)): %v", err) 115 } 116 117 const shareURL = "https://play.golang.org/share" 118 testCases := []struct { 119 desc string 120 method string 121 url string 122 statusCode int 123 reqBody []byte 124 respBody []byte 125 }{ 126 // Share tests. 127 {"OPTIONS no-op", http.MethodOptions, shareURL, http.StatusOK, nil, nil}, 128 {"Non-POST request", http.MethodGet, shareURL, http.StatusMethodNotAllowed, nil, nil}, 129 {"Standard flow", http.MethodPost, shareURL, http.StatusOK, []byte("Snippy McSnipface"), []byte("N_M_YelfGeR")}, 130 {"Snippet too large", http.MethodPost, shareURL, http.StatusRequestEntityTooLarge, make([]byte, maxSnippetSize+1), nil}, 131 132 // Examples tests. 133 {"Hello example", http.MethodGet, "https://play.golang.org/doc/play/hello.txt", http.StatusOK, nil, []byte("Hello")}, 134 {"HTTP example", http.MethodGet, "https://play.golang.org/doc/play/http.txt", http.StatusOK, nil, []byte("net/http")}, 135 // Gotip examples should not be available on the non-tip playground. 136 {"Gotip example", http.MethodGet, "https://play.golang.org/doc/play/min.gotip.txt", http.StatusNotFound, nil, nil}, 137 138 {"Versions json", http.MethodGet, "https://play.golang.org/version", http.StatusOK, nil, []byte(runtime.Version())}, 139 } 140 141 for _, tc := range testCases { 142 req := httptest.NewRequest(tc.method, tc.url, bytes.NewReader(tc.reqBody)) 143 w := httptest.NewRecorder() 144 s.mux.ServeHTTP(w, req) 145 resp := w.Result() 146 corsHeader := "Access-Control-Allow-Origin" 147 if got, want := resp.Header.Get(corsHeader), "*"; got != want { 148 t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) 149 } 150 if got, want := resp.StatusCode, tc.statusCode; got != want { 151 t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) 152 } 153 if tc.respBody != nil { 154 defer resp.Body.Close() 155 b, err := io.ReadAll(resp.Body) 156 if err != nil { 157 t.Errorf("%s: io.ReadAll(resp.Body): %v", tc.desc, err) 158 } 159 if !bytes.Contains(b, tc.respBody) { 160 t.Errorf("%s: got unexpected body %q; want contains %q", tc.desc, b, tc.respBody) 161 } 162 } 163 } 164 } 165 166 func TestNoTrailingUnderscore(t *testing.T) { 167 const trailingUnderscoreSnip = `package main 168 169 import "unsafe" 170 171 type T struct{} 172 173 func (T) m1() {} 174 func (T) m2([unsafe.Sizeof(T.m1)]int) {} 175 176 func main() {} 177 ` 178 snip := &snippet{[]byte(trailingUnderscoreSnip)} 179 if got, want := snip.ID(), "WCktUidLyc_3"; got != want { 180 t.Errorf("got %q; want %q", got, want) 181 } 182 } 183 184 func TestCommandHandler(t *testing.T) { 185 s, err := newServer(func(s *server) error { 186 s.db = &inMemStore{} 187 // testLogger makes tests fail. 188 // Should we verify that s.log.Errorf was called 189 // instead of just printing or failing the test? 190 s.log = newStdLogger() 191 s.cache = new(inMemCache) 192 var err error 193 s.examples, err = newExamplesHandler(false, time.Now()) 194 if err != nil { 195 return err 196 } 197 return nil 198 }) 199 if err != nil { 200 t.Fatalf("newServer(testingOptions(t)): %v", err) 201 } 202 testHandler := s.commandHandler("test", func(_ context.Context, r *request) (*response, error) { 203 if r.Body == "fail" { 204 return nil, fmt.Errorf("non recoverable") 205 } 206 if r.Body == "error" { 207 return &response{Errors: "errors"}, nil 208 } 209 if r.Body == "oom-error" { 210 // To throw an oom in a local playground instance, increase the server timeout 211 // to 20 seconds (within sandbox.go), spin up the Docker instance and run 212 // this code: https://play.golang.org/p/aaCv86m0P14. 213 return &response{Events: []Event{{"out of memory", "stderr", 0}}}, nil 214 } 215 if r.Body == "allocate-memory-error" { 216 return &response{Events: []Event{{"cannot allocate memory", "stderr", 0}}}, nil 217 } 218 if r.Body == "oom-compile-error" { 219 return &response{Errors: "out of memory"}, nil 220 } 221 if r.Body == "allocate-memory-compile-error" { 222 return &response{Errors: "cannot allocate memory"}, nil 223 } 224 if r.Body == "build-timeout-error" { 225 return &response{Errors: goBuildTimeoutError}, nil 226 } 227 if r.Body == "run-timeout-error" { 228 return &response{Errors: runTimeoutError}, nil 229 } 230 resp := &response{Events: []Event{{r.Body, "stdout", 0}}} 231 return resp, nil 232 }) 233 234 testCases := []struct { 235 desc string 236 method string 237 statusCode int 238 reqBody []byte 239 respBody []byte 240 shouldCache bool 241 }{ 242 {"OPTIONS request", http.MethodOptions, http.StatusOK, nil, nil, false}, 243 {"GET request", http.MethodGet, http.StatusBadRequest, nil, nil, false}, 244 {"Empty POST", http.MethodPost, http.StatusBadRequest, nil, nil, false}, 245 {"Failed cmdFunc", http.MethodPost, http.StatusInternalServerError, []byte(`{"Body":"fail"}`), nil, false}, 246 {"Standard flow", http.MethodPost, http.StatusOK, 247 []byte(`{"Body":"ok"}`), 248 []byte(`{"Errors":"","Events":[{"Message":"ok","Kind":"stdout","Delay":0}],"Status":0,"IsTest":false,"TestsFailed":0} 249 `), 250 true}, 251 {"Cache-able Errors in response", http.MethodPost, http.StatusOK, 252 []byte(`{"Body":"error"}`), 253 []byte(`{"Errors":"errors","Events":null,"Status":0,"IsTest":false,"TestsFailed":0} 254 `), 255 true}, 256 {"Out of memory error in response body event message", http.MethodPost, http.StatusInternalServerError, 257 []byte(`{"Body":"oom-error"}`), nil, false}, 258 {"Cannot allocate memory error in response body event message", http.MethodPost, http.StatusInternalServerError, 259 []byte(`{"Body":"allocate-memory-error"}`), nil, false}, 260 {"Out of memory error in response errors", http.MethodPost, http.StatusInternalServerError, 261 []byte(`{"Body":"oom-compile-error"}`), nil, false}, 262 {"Cannot allocate memory error in response errors", http.MethodPost, http.StatusInternalServerError, 263 []byte(`{"Body":"allocate-memory-compile-error"}`), nil, false}, 264 { 265 desc: "Build timeout error", 266 method: http.MethodPost, 267 statusCode: http.StatusOK, 268 reqBody: []byte(`{"Body":"build-timeout-error"}`), 269 respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running go build","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)), 270 }, 271 { 272 desc: "Run timeout error", 273 method: http.MethodPost, 274 statusCode: http.StatusOK, 275 reqBody: []byte(`{"Body":"run-timeout-error"}`), 276 respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running program","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)), 277 }, 278 } 279 280 for _, tc := range testCases { 281 t.Run(tc.desc, func(t *testing.T) { 282 req := httptest.NewRequest(tc.method, "/compile", bytes.NewReader(tc.reqBody)) 283 w := httptest.NewRecorder() 284 testHandler(w, req) 285 resp := w.Result() 286 corsHeader := "Access-Control-Allow-Origin" 287 if got, want := resp.Header.Get(corsHeader), "*"; got != want { 288 t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) 289 } 290 if got, want := resp.StatusCode, tc.statusCode; got != want { 291 t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) 292 } 293 if tc.respBody != nil { 294 defer resp.Body.Close() 295 b, err := io.ReadAll(resp.Body) 296 if err != nil { 297 t.Errorf("%s: io.ReadAll(resp.Body): %v", tc.desc, err) 298 } 299 if !bytes.Equal(b, tc.respBody) { 300 t.Errorf("%s: got unexpected body %q; want %q", tc.desc, b, tc.respBody) 301 } 302 } 303 304 // Test caching semantics. 305 sbreq := new(request) // A sandbox request, used in the cache key. 306 json.Unmarshal(tc.reqBody, sbreq) // Ignore errors, request may be empty. 307 gotCache := new(response) 308 if err := s.cache.Get(cacheKey("test", sbreq.Body), gotCache); (err == nil) != tc.shouldCache { 309 t.Errorf("s.cache.Get(%q, %v) = %v, shouldCache: %v", cacheKey("test", sbreq.Body), gotCache, err, tc.shouldCache) 310 } 311 wantCache := new(response) 312 if tc.shouldCache { 313 if err := json.Unmarshal(tc.respBody, wantCache); err != nil { 314 t.Errorf("json.Unmarshal(%q, %v) = %v, wanted no error", tc.respBody, wantCache, err) 315 } 316 } 317 if diff := cmp.Diff(wantCache, gotCache); diff != "" { 318 t.Errorf("s.Cache.Get(%q) mismatch (-want +got):\n%s", cacheKey("test", sbreq.Body), diff) 319 } 320 }) 321 } 322 } 323 324 func TestPlaygroundGoproxy(t *testing.T) { 325 const envKey = "PLAY_GOPROXY" 326 defer os.Setenv(envKey, os.Getenv(envKey)) 327 328 tests := []struct { 329 name string 330 env string 331 want string 332 }{ 333 {name: "missing", env: "", want: "https://proxy.golang.org"}, 334 {name: "set_to_default", env: "https://proxy.golang.org", want: "https://proxy.golang.org"}, 335 {name: "changed", env: "https://company.intranet", want: "https://company.intranet"}, 336 } 337 for _, tt := range tests { 338 t.Run(tt.name, func(t *testing.T) { 339 if tt.env != "" { 340 if err := os.Setenv(envKey, tt.env); err != nil { 341 t.Errorf("unable to set environment variable for test: %s", err) 342 } 343 } else { 344 if err := os.Unsetenv(envKey); err != nil { 345 t.Errorf("unable to unset environment variable for test: %s", err) 346 } 347 } 348 got := playgroundGoproxy() 349 if got != tt.want { 350 t.Errorf("playgroundGoproxy = %s; want %s; env: %s", got, tt.want, tt.env) 351 } 352 }) 353 } 354 } 355 356 // inMemCache is a responseCache backed by a map. It is only suitable for testing. 357 type inMemCache struct { 358 l sync.Mutex 359 m map[string]*response 360 } 361 362 // Set implements the responseCache interface. 363 // Set stores a *response in the cache. It panics for other types to ensure test failure. 364 func (i *inMemCache) Set(key string, v interface{}) error { 365 i.l.Lock() 366 defer i.l.Unlock() 367 if i.m == nil { 368 i.m = make(map[string]*response) 369 } 370 i.m[key] = v.(*response) 371 return nil 372 } 373 374 // Get implements the responseCache interface. 375 // Get fetches a *response from the cache, or returns a memcache.ErrcacheMiss. 376 // It panics for other types to ensure test failure. 377 func (i *inMemCache) Get(key string, v interface{}) error { 378 i.l.Lock() 379 defer i.l.Unlock() 380 target := v.(*response) 381 got, ok := i.m[key] 382 if !ok { 383 return memcache.ErrCacheMiss 384 } 385 *target = *got 386 return nil 387 }