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) } }