blob: b18abc146fe796a9fa957e56a8ed764e8a1b169a [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package proxy
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
)
// NewTestClient creates a new client for testing.
// If update is true, the returned client contacts the real
// proxy and updates the file "testdata/proxy/<TestName>.json" with
// the responses it saw.
// If update is false, the returned client is a fake that
// reads saved responses from "testdata/proxy/<TestName>.json".
func NewTestClient(t *testing.T, update bool) (*Client, error) {
t.Helper()
fpath := responsesFile(t)
if update {
// Set up a real proxy and register a function to write the responses
// after the test runs.
pc := NewClient(http.DefaultClient, ProxyURL)
t.Cleanup(func() {
if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
t.Error(err)
return
}
if err := pc.writeResponses(fpath); err != nil {
t.Error(err)
}
})
return pc, nil
}
// Get the fake client from the saved responses.
b, err := os.ReadFile(fpath)
if err != nil {
return nil, err
}
var responses map[string]*response
err = json.Unmarshal(b, &responses)
if err != nil {
return nil, err
}
c, cleanup := fakeClient(responses)
t.Cleanup(cleanup)
return c, nil
}
// response is a representation of an HTTP response used to
// facilitate testing.
type response struct {
Body string `json:"body,omitempty"`
StatusCode int `json:"status_code"`
}
// fakeClient creates a client that returns hard-coded responses.
// endpointsToResponses is a map from proxy endpoints
// (with no server url, and no leading '/'), to their desired responses.
func fakeClient(endpointsToResponses map[string]*response) (c *Client, cleanup func()) {
handler := func(w http.ResponseWriter, r *http.Request) {
for endpoint, response := range endpointsToResponses {
if r.Method == http.MethodGet &&
r.URL.Path == "/"+endpoint {
if response.StatusCode == http.StatusOK {
_, _ = w.Write([]byte(response.Body))
} else {
w.WriteHeader(response.StatusCode)
}
return
}
}
w.WriteHeader(http.StatusBadRequest)
}
s := httptest.NewServer(http.HandlerFunc(handler))
return NewClient(s.Client(), s.URL), func() { s.Close() }
}
func responsesFile(t *testing.T) string {
return filepath.Join("testdata", "proxy", t.Name()+".json")
}
// responses returns a map from endpoints to the latest response received for each endpoint.
//
// Intended for testing: the output can be passed to NewTestClient to create a fake client
// that returns the same responses.
func (c *Client) responses() map[string]*response {
m := make(map[string]*response)
for key, status := range c.errLog.getData() {
m[key] = &response{StatusCode: status}
}
for key, b := range c.cache.getData() {
m[key] = &response{Body: string(b), StatusCode: http.StatusOK}
}
return m
}
func (pc *Client) writeResponses(filepath string) error {
responses, err := json.MarshalIndent(pc.responses(), "", "\t")
if err != nil {
return err
}
return os.WriteFile(filepath, responses, 0644)
}
// An in-memory store of the errors seen so far.
// Used by the responses() function, for testing.
type errLog struct {
data map[string]int
mu sync.Mutex
}
func newErrLog() *errLog {
return &errLog{data: make(map[string]int)}
}
func (e *errLog) set(key string, status int) {
e.mu.Lock()
defer e.mu.Unlock()
e.data[key] = status
}
func (e *errLog) getData() map[string]int {
e.mu.Lock()
defer e.mu.Unlock()
return e.data
}