feat(14-01): guarded fetchguard client replayed through a tide upstream fake
- 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
This commit is contained in:
150
modules/tide/upstream_test.go
Normal file
150
modules/tide/upstream_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user