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  }