- fetchguard.NewClient with Do, Send, Get and PostJSON over a capped, never-redirecting transport - WithTransport: code-only context seam for offline replay; Result gains Header - tide UpstreamSidecar, LoadUpstream, UpstreamPath and the asserting UpstreamFake
151 lines
4.7 KiB
Go
151 lines
4.7 KiB
Go
package tide
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func upstreamTestSidecar() UpstreamSidecar {
|
|
return UpstreamSidecar{Version: 1, Exchanges: []UpstreamExchange{{
|
|
Request: UpstreamRequest{
|
|
Method: "POST",
|
|
URL: "https://api.example.test/v1/things?mode=fast&lang=en",
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer {{secret:example-token}}",
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "example-client/1.0",
|
|
},
|
|
Body: `{"name":"widget","count":2}`,
|
|
},
|
|
Response: UpstreamResponse{Status: 201, Body: `{"id":7}`},
|
|
}}}
|
|
}
|
|
|
|
func upstreamTestStore(t *testing.T) *Store {
|
|
t.Helper()
|
|
s, err := OpenStore("")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Set("secret:example-token", "example-token-value")
|
|
return s
|
|
}
|
|
|
|
type upstreamCall struct {
|
|
method, url, ua, auth, body string
|
|
}
|
|
|
|
func goodUpstreamCall() upstreamCall {
|
|
return upstreamCall{
|
|
method: "POST",
|
|
url: "https://api.example.test/v1/things?lang=en&mode=fast",
|
|
ua: "example-client/1.0",
|
|
auth: "Bearer example-token-value",
|
|
body: `{"count":2,"name":"widget"}`,
|
|
}
|
|
}
|
|
|
|
func (c upstreamCall) send(t *testing.T, f *UpstreamFake) error {
|
|
t.Helper()
|
|
req, err := http.NewRequest(c.method, c.url, bytes.NewReader([]byte(c.body)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("User-Agent", c.ua)
|
|
req.Header.Set("Authorization", c.auth)
|
|
resp, err := f.RoundTrip(req)
|
|
if err == nil {
|
|
_ = resp.Body.Close()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func TestUpstreamFakeRejectsMismatchedRequest(t *testing.T) {
|
|
ok := goodUpstreamCall()
|
|
t.Run("match", func(t *testing.T) {
|
|
f := NewUpstreamFake(upstreamTestSidecar(), upstreamTestStore(t))
|
|
if err := ok.send(t, f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.Verify(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
cases := []struct {
|
|
name string
|
|
edit func(*upstreamCall)
|
|
field string
|
|
}{
|
|
{"method", func(c *upstreamCall) { c.method = "PUT" }, "method"},
|
|
{"path", func(c *upstreamCall) { c.url = strings.Replace(c.url, "/v1/things", "/v1/other", 1) }, "path"},
|
|
{"query", func(c *upstreamCall) { c.url = strings.Replace(c.url, "mode=fast", "mode=slow", 1) }, "query mode"},
|
|
{"host", func(c *upstreamCall) { c.url = strings.Replace(c.url, "api.example.test", "api2.example.test", 1) }, "host"},
|
|
{"user agent", func(c *upstreamCall) { c.ua = "other/2.0" }, "header User-Agent"},
|
|
{"authorization", func(c *upstreamCall) { c.auth = "Bearer wrong-token" }, "header Authorization"},
|
|
{"json body", func(c *upstreamCall) { c.body = `{"count":3,"name":"widget"}` }, "body $.count"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
f := NewUpstreamFake(upstreamTestSidecar(), upstreamTestStore(t))
|
|
c := ok
|
|
tc.edit(&c)
|
|
if err := c.send(t, f); err == nil || !strings.Contains(err.Error(), tc.field) {
|
|
t.Fatalf("RoundTrip err = %v, want it to name %q", err, tc.field)
|
|
}
|
|
err := f.Verify()
|
|
if err == nil || !strings.Contains(err.Error(), tc.field) {
|
|
t.Fatalf("Verify = %v, want it to name %q", err, tc.field)
|
|
}
|
|
if strings.Contains(err.Error(), "wrong-token") || strings.Contains(err.Error(), "example-token-value") {
|
|
t.Fatalf("Verify leaks a credential: %v", err)
|
|
}
|
|
})
|
|
}
|
|
t.Run("extra request", func(t *testing.T) {
|
|
f := NewUpstreamFake(upstreamTestSidecar(), upstreamTestStore(t))
|
|
if err := ok.send(t, f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ok.send(t, f); err == nil {
|
|
t.Fatal("second request must fail")
|
|
}
|
|
if err := f.Verify(); err == nil || !strings.Contains(err.Error(), "extra request") {
|
|
t.Fatalf("Verify = %v, want extra request", err)
|
|
}
|
|
})
|
|
t.Run("unconsumed exchange", func(t *testing.T) {
|
|
f := NewUpstreamFake(upstreamTestSidecar(), upstreamTestStore(t))
|
|
if err := f.Verify(); err == nil || !strings.Contains(err.Error(), "unconsumed") {
|
|
t.Fatalf("Verify = %v, want unconsumed", err)
|
|
}
|
|
})
|
|
t.Run("unresolved placeholder", func(t *testing.T) {
|
|
f := NewUpstreamFake(upstreamTestSidecar(), nil)
|
|
if err := ok.send(t, f); err == nil || !strings.Contains(err.Error(), "header Authorization") {
|
|
t.Fatalf("err = %v, want unresolved Authorization placeholder", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLoadUpstreamAndPath(t *testing.T) {
|
|
if got := UpstreamPath("fixtures/routes/GET_x__ok.yaml"); got != "fixtures/routes/GET_x__ok.upstream.yaml" {
|
|
t.Fatalf("UpstreamPath = %q", got)
|
|
}
|
|
if got := UpstreamPath("a.yml"); got != "a.upstream.yaml" {
|
|
t.Fatalf("UpstreamPath(.yml) = %q", got)
|
|
}
|
|
if got := UpstreamPath("a"); got != "a.upstream.yaml" {
|
|
t.Fatalf("UpstreamPath(no ext) = %q", got)
|
|
}
|
|
s, err := LoadUpstream("testdata/upstream/post_json.upstream.yaml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(s.Exchanges) != 1 || s.Exchanges[0].Response.Status != 201 {
|
|
t.Fatalf("sidecar = %+v", s)
|
|
}
|
|
}
|