Skip to content
This repository was archived by the owner on Aug 7, 2022. It is now read-only.

Commit e9f03c3

Browse files
authored
Merge pull request #4 from mockingio/feat/support-proxy
Feat/support proxy
2 parents 1746e7f + bfe2e5f commit e9f03c3

File tree

8 files changed

+188
-18
lines changed

8 files changed

+188
-18
lines changed

engine.go

+85-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package engine
22

33
import (
4+
"context"
5+
"io"
46
"net/http"
57
"time"
68

9+
"github.com/pkg/errors"
710
log "github.com/sirupsen/logrus"
811

912
"github.com/mockingio/engine/matcher"
@@ -15,6 +18,7 @@ type Engine struct {
1518
mockID string
1619
isPaused bool
1720
db persistent.Persistent
21+
mock *mock.Mock
1822
}
1923

2024
func New(mockID string, db persistent.Persistent) *Engine {
@@ -34,12 +38,13 @@ func (eng *Engine) Pause() {
3438

3539
func (eng *Engine) Match(req *http.Request) *mock.Response {
3640
ctx := req.Context()
37-
mok, err := eng.db.GetMock(ctx, eng.mockID)
38-
if err != nil {
39-
log.WithError(err).Error("loading mock")
41+
if err := eng.reloadMock(ctx); err != nil {
42+
log.WithError(err).Error("reload mock")
4043
return nil
4144
}
4245

46+
mok := eng.getMock()
47+
4348
sessionID, err := eng.db.GetActiveSession(ctx, eng.mockID)
4449
if err != nil {
4550
log.WithError(err).WithField("config_id", eng.mockID).Error("get active session")
@@ -79,8 +84,13 @@ func (eng *Engine) Handler(w http.ResponseWriter, r *http.Request) {
7984

8085
response := eng.Match(r)
8186
if response == nil {
82-
// TODO: no matched? What will be the response?
83-
w.WriteHeader(http.StatusNotFound)
87+
mok := eng.getMock()
88+
if mok.ProxyEnabled() {
89+
eng.handleProxy(w, r)
90+
return
91+
}
92+
93+
eng.noMatchHandler(w)
8494
return
8595
}
8696

@@ -91,3 +101,73 @@ func (eng *Engine) Handler(w http.ResponseWriter, r *http.Request) {
91101
w.WriteHeader(response.Status)
92102
_, _ = w.Write([]byte(response.Body))
93103
}
104+
105+
func (eng *Engine) noMatchHandler(w http.ResponseWriter) {
106+
w.WriteHeader(http.StatusNotFound)
107+
}
108+
109+
func (eng *Engine) handleProxy(w http.ResponseWriter, r *http.Request) {
110+
proxy := eng.getMock().Proxy
111+
112+
req, err := copyProxyRequest(r, proxy)
113+
if err != nil {
114+
log.WithError(err).Error("copy request")
115+
eng.noMatchHandler(w)
116+
return
117+
}
118+
119+
res, err := (&http.Client{}).Do(req)
120+
if err != nil {
121+
log.WithError(err).Error("make proxy request")
122+
eng.noMatchHandler(w)
123+
return
124+
}
125+
defer func() { _ = res.Body.Close() }()
126+
127+
writeProxyResponse(res, w, proxy)
128+
}
129+
130+
func (eng *Engine) getMock() *mock.Mock {
131+
return eng.mock
132+
}
133+
134+
func (eng *Engine) reloadMock(ctx context.Context) error {
135+
mok, err := eng.db.GetMock(ctx, eng.mockID)
136+
if err != nil {
137+
return errors.Wrap(err, "get mock from DB")
138+
}
139+
140+
if mok == nil {
141+
return errors.New("mock not found")
142+
}
143+
144+
eng.mock = mok
145+
146+
return nil
147+
}
148+
149+
func copyProxyRequest(r *http.Request, proxy *mock.Proxy) (*http.Request, error) {
150+
req, err := http.NewRequest(r.Method, proxy.Host+r.URL.Path, r.Body)
151+
if err != nil {
152+
return nil, err
153+
}
154+
req.Header = r.Header
155+
156+
for k, v := range proxy.RequestHeaders {
157+
req.Header.Add(k, v)
158+
}
159+
160+
return req, nil
161+
}
162+
163+
func writeProxyResponse(res *http.Response, w http.ResponseWriter, proxy *mock.Proxy) {
164+
for k, v := range res.Header {
165+
w.Header().Add(k, v[0])
166+
}
167+
for k, v := range proxy.ResponseHeaders {
168+
w.Header().Add(k, v)
169+
}
170+
171+
w.WriteHeader(res.StatusCode)
172+
_, _ = io.Copy(w, res.Body)
173+
}

engine_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package engine_test
22

33
import (
44
"context"
5+
"io"
56
"io/ioutil"
67
"net/http"
78
"net/http/httptest"
@@ -127,6 +128,52 @@ func TestEngine_NoResponses(t *testing.T) {
127128
assert.Equal(t, http.StatusNotFound, res.StatusCode)
128129
}
129130

131+
func TestEngine_ProxyHandler(t *testing.T) {
132+
proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
133+
if r.URL.Path != "/hello" {
134+
t.Errorf("request path is %s, not /hello", r.URL.Path)
135+
}
136+
137+
if r.Header.Get("X-Request") != "from request" {
138+
t.Error("header is not append to the request")
139+
}
140+
141+
w.Header().Set("Content-Type", "html/text")
142+
w.WriteHeader(http.StatusOK)
143+
_, _ = w.Write([]byte("From Proxy"))
144+
}))
145+
146+
mok := &mock.Mock{
147+
ID: "mock-id",
148+
Proxy: &mock.Proxy{
149+
Enabled: true,
150+
Host: proxyServer.URL,
151+
RequestHeaders: map[string]string{
152+
"X-Request": "from request",
153+
},
154+
ResponseHeaders: map[string]string{
155+
"X-Response": "from response",
156+
},
157+
},
158+
}
159+
mem := memory.New()
160+
_ = mem.SetMock(context.Background(), mok)
161+
eng := engine.New("mock-id", mem)
162+
163+
w := httptest.NewRecorder()
164+
eng.Handler(w, httptest.NewRequest(http.MethodGet, "/hello", nil))
165+
res := w.Result()
166+
body, _ := io.ReadAll(res.Body)
167+
defer func() {
168+
_ = res.Body.Close()
169+
}()
170+
171+
assert.Equal(t, http.StatusOK, res.StatusCode)
172+
assert.Equal(t, "From Proxy", string(body))
173+
assert.Equal(t, "html/text", res.Header.Get("Content-Type"))
174+
assert.Equal(t, "from response", res.Header.Get("X-Response"))
175+
}
176+
130177
func setupMock() persistent.Persistent {
131178
mok := &mock.Mock{
132179
ID: "mock-id",

matcher/route.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package matcher
22

33
import (
44
"math/rand"
5+
"net/http"
56
"strings"
67
"time"
78

@@ -28,8 +29,12 @@ func NewRouteMatcher(route *cfg.Route, req Context, db persistent.Persistent) *R
2829

2930
func (r *RouteMatcher) Match() (*cfg.Response, error) {
3031
httpRequest := r.req.HTTPRequest
32+
method := r.route.Method
33+
if r.route.Method == "" {
34+
method = http.MethodGet
35+
}
3136

32-
if !strings.EqualFold(r.route.Method, httpRequest.Method) {
37+
if !strings.EqualFold(method, httpRequest.Method) {
3338
return nil, nil
3439
}
3540

mock/fixtures/mock.golden.yml

+7
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,10 @@ routes:
5151
description: ""
5252
responses:
5353
- status: 201
54+
proxy:
55+
enabled: true
56+
host: https://google.com
57+
request_headers:
58+
X-Forward: "123"
59+
response_headers:
60+
X-Response: "123"

mock/fixtures/mock.yml

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
name: Hello World
2+
proxy:
3+
enabled: true
4+
host: https://google.com
5+
request_headers:
6+
X-Forward: 123
7+
response_headers:
8+
X-Response: 123
29
routes:
310
- method: GET
411
path: /hello/world

mock/mock.go

+12-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Mock struct {
1616
Name string `yaml:"name,omitempty" json:"name,omitempty"`
1717
Port string `yaml:"port,omitempty" json:"port,omitempty"`
1818
Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"`
19+
Proxy *Proxy `yaml:"proxy,omitempty" json:"proxy,omitempty"`
1920
options mockOptions
2021
}
2122

@@ -55,6 +56,17 @@ func FromYaml(text string, opts ...Option) (*Mock, error) {
5556
return m, nil
5657
}
5758

59+
func (c Mock) Validate() error {
60+
return validation.ValidateStruct(
61+
&c,
62+
validation.Field(&c.Routes, validation.Required),
63+
)
64+
}
65+
66+
func (c Mock) ProxyEnabled() bool {
67+
return c.Proxy != nil && c.Proxy.Enabled
68+
}
69+
5870
func defaultValues(m *Mock) {
5971
for _, r := range m.Routes {
6072
if r.Method == "" {
@@ -95,13 +107,6 @@ func addIDs(m *Mock) {
95107
}
96108
}
97109

98-
func (c Mock) Validate() error {
99-
return validation.ValidateStruct(
100-
&c,
101-
validation.Field(&c.Routes, validation.Required),
102-
)
103-
}
104-
105110
func newID() string {
106111
return uuid.NewString()
107112
}

mock/mock_test.go

+16-5
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package mock_test
22

33
import (
44
_ "embed"
5-
"gopkg.in/yaml.v2"
65
"path/filepath"
76
"testing"
87

8+
"gopkg.in/yaml.v2"
9+
910
. "github.com/mockingio/engine/mock"
1011
"github.com/mockingio/engine/test"
1112
"github.com/stretchr/testify/assert"
@@ -46,14 +47,24 @@ func TestConfig(t *testing.T) {
4647
})
4748

4849
t.Run("error loading config from YAML file", func(t *testing.T) {
49-
cfg, err := FromFile("")
50+
mock, err := FromFile("")
5051
assert.Error(t, err)
51-
assert.Nil(t, cfg)
52+
assert.Nil(t, mock)
5253
})
5354

5455
t.Run("error loading mock from empty yaml", func(t *testing.T) {
55-
cfg, err := FromYaml("")
56+
mock, err := FromYaml("")
5657
assert.Error(t, err)
57-
assert.Nil(t, cfg)
58+
assert.Nil(t, mock)
59+
})
60+
61+
t.Run("proxy is enabled", func(t *testing.T) {
62+
mock := &Mock{
63+
Proxy: &Proxy{
64+
Enabled: true,
65+
},
66+
}
67+
assert.False(t, New().ProxyEnabled())
68+
assert.True(t, mock.ProxyEnabled())
5869
})
5970
}

mock/proxy.go

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package mock
2+
3+
type Proxy struct {
4+
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
5+
Host string `yaml:"host,omitempty" json:"host,omitempty"`
6+
RequestHeaders map[string]string `yaml:"request_headers,omitempty" json:"request_headers,omitempty"`
7+
ResponseHeaders map[string]string `yaml:"response_headers,omitempty" json:"response_headers,omitempty"`
8+
}

0 commit comments

Comments
 (0)