From a8d0ec4e63002519a3f61bc8ba00f076a82e02c7 Mon Sep 17 00:00:00 2001 From: vokomarov Date: Mon, 1 Jan 2024 11:59:02 +0200 Subject: [PATCH 1/5] Add HTTP client and mock. Refactor Captcha package --- Makefile | 7 +- go.mod | 9 +- go.sum | 12 ++ http/client.go | 44 +++++ http/client_test.go | 23 +++ mocks/http_client_mock.go | 84 +++++++++ router/api/handler.go | 6 +- router/captcha/captcha.go | 7 + router/captcha/client_mock_test.go | 85 --------- .../{verify.go => google_recaptcha.go} | 60 +++---- router/captcha/google_recaptcha_test.go | 169 ++++++++++++++++++ router/captcha/verify_test.go | 136 -------------- 12 files changed, 384 insertions(+), 258 deletions(-) create mode 100644 http/client.go create mode 100644 http/client_test.go create mode 100644 mocks/http_client_mock.go create mode 100644 router/captcha/captcha.go delete mode 100644 router/captcha/client_mock_test.go rename router/captcha/{verify.go => google_recaptcha.go} (54%) create mode 100644 router/captcha/google_recaptcha_test.go delete mode 100644 router/captcha/verify_test.go diff --git a/Makefile b/Makefile index 3a05d76..8dae194 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ IMAGE_RELEASE=$(REPO):$(RELEASE_VERSION) IMAGE_DEV=$(REPO):dev IMAGE_LATEST=$(REPO):latest -.PHONY: run test build tag push start stop +.PHONY: run test build tag push start stop mock-gen run: go run -race main.go @@ -42,3 +42,8 @@ start: stop: docker stop $(CONTAINER_NAME) + +mock-gen: + go install go.uber.org/mock/mockgen@latest + mockgen -source=http/client.go -package=mocks -destination=mocks/http_client_mock.go Client + diff --git a/go.mod b/go.mod index b3a5987..42dfee5 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/cash-track/gateway go 1.21 require ( - github.com/fasthttp/router v1.4.21 + github.com/fasthttp/router v1.4.22 github.com/flf2ko/fasthttp-prometheus v0.1.0 github.com/stretchr/testify v1.8.0 - github.com/valyala/fasthttp v1.50.0 + github.com/valyala/fasthttp v1.51.0 ) require ( @@ -26,7 +26,10 @@ require ( github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/sys v0.6.0 // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/tools v0.2.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 46f9683..5d5ed48 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fasthttp/router v1.4.21 h1:Ysgck9aZwaovqxsfhv7nPx9EgsYvB7t3nthrBMQoeIg= github.com/fasthttp/router v1.4.21/go.mod h1:wtOlZHmOSGD048li7Nkuhw+ov40rr0tY2+IjT+mN9p4= +github.com/fasthttp/router v1.4.22 h1:qwWcYBbndVDwts4dKaz+A2ehsnbKilmiP6pUhXBfYKo= +github.com/fasthttp/router v1.4.22/go.mod h1:KeMvHLqhlB9vyDWD5TSvTccl9qeWrjSSiTJrJALHKV0= github.com/flf2ko/fasthttp-prometheus v0.1.0 h1:hj4K3TwJ2B7Fe2E7lWE/eb9mtb7gBvwURXr4+iEFoCI= github.com/flf2ko/fasthttp-prometheus v0.1.0/go.mod h1:5tGRWsJeP8ABLYovqPxa5c/zCgnsYUhhC1ivs/Kv/c4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -51,9 +53,19 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/http/client.go b/http/client.go new file mode 100644 index 0000000..00448c1 --- /dev/null +++ b/http/client.go @@ -0,0 +1,44 @@ +package http + +import ( + "time" + + "github.com/valyala/fasthttp" +) + +type Client interface { + Do(req *fasthttp.Request, resp *fasthttp.Response) error + WithReadTimeout(timeout time.Duration) Client + WithWriteTimeout(timeout time.Duration) Client +} + +type FastHttpClient struct { + *fasthttp.Client +} + +func NewFastHttpClient() Client { + return &FastHttpClient{ + Client: &fasthttp.Client{ + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + MaxIdleConnDuration: time.Hour, + NoDefaultUserAgentHeader: true, + DisableHeaderNamesNormalizing: true, + DisablePathNormalizing: true, + Dial: (&fasthttp.TCPDialer{ + Concurrency: 4096, + DNSCacheDuration: time.Hour, + }).Dial, + }, + } +} + +func (c *FastHttpClient) WithReadTimeout(timeout time.Duration) Client { + c.ReadTimeout = timeout + return c +} + +func (c *FastHttpClient) WithWriteTimeout(timeout time.Duration) Client { + c.WriteTimeout = timeout + return c +} diff --git a/http/client_test.go b/http/client_test.go new file mode 100644 index 0000000..d8d2e46 --- /dev/null +++ b/http/client_test.go @@ -0,0 +1,23 @@ +package http + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" +) + +func TestNewFastHttpClient(t *testing.T) { + client := NewFastHttpClient() + assert.NotNil(t, client) +} + +func TestWithReadTimeout(t *testing.T) { + client := FastHttpClient{ + Client: &fasthttp.Client{}, + } + client.WithReadTimeout(1 * time.Second) + + assert.Equal(t, 1*time.Second, client.ReadTimeout) +} diff --git a/mocks/http_client_mock.go b/mocks/http_client_mock.go new file mode 100644 index 0000000..c19ed89 --- /dev/null +++ b/mocks/http_client_mock.go @@ -0,0 +1,84 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: http/client.go +// +// Generated by this command: +// +// mockgen -source=http/client.go -package=mocks -destination=mocks/http_client_mock.go Client +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + time "time" + + http "github.com/cash-track/gateway/http" + fasthttp "github.com/valyala/fasthttp" + gomock "go.uber.org/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Do mocks base method. +func (m *MockClient) Do(req *fasthttp.Request, resp *fasthttp.Response) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Do", req, resp) + ret0, _ := ret[0].(error) + return ret0 +} + +// Do indicates an expected call of Do. +func (mr *MockClientMockRecorder) Do(req, resp any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockClient)(nil).Do), req, resp) +} + +// WithReadTimeout mocks base method. +func (m *MockClient) WithReadTimeout(timeout time.Duration) http.Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithReadTimeout", timeout) + ret0, _ := ret[0].(http.Client) + return ret0 +} + +// WithReadTimeout indicates an expected call of WithReadTimeout. +func (mr *MockClientMockRecorder) WithReadTimeout(timeout any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithReadTimeout", reflect.TypeOf((*MockClient)(nil).WithReadTimeout), timeout) +} + +// WithWriteTimeout mocks base method. +func (m *MockClient) WithWriteTimeout(timeout time.Duration) http.Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithWriteTimeout", timeout) + ret0, _ := ret[0].(http.Client) + return ret0 +} + +// WithWriteTimeout indicates an expected call of WithWriteTimeout. +func (mr *MockClientMockRecorder) WithWriteTimeout(timeout any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithWriteTimeout", reflect.TypeOf((*MockClient)(nil).WithWriteTimeout), timeout) +} diff --git a/router/api/handler.go b/router/api/handler.go index 1312dad..fc0befe 100644 --- a/router/api/handler.go +++ b/router/api/handler.go @@ -6,7 +6,9 @@ import ( "github.com/valyala/fasthttp" + "github.com/cash-track/gateway/config" "github.com/cash-track/gateway/headers/cookie" + "github.com/cash-track/gateway/http" "github.com/cash-track/gateway/router/api/client" "github.com/cash-track/gateway/router/captcha" "github.com/cash-track/gateway/router/response" @@ -22,7 +24,9 @@ var allowedMethods = map[string]bool{ } func AuthSetHandler(ctx *fasthttp.RequestCtx) { - if ok, err := captcha.Verify(ctx); err != nil || !ok { + reCaptcha := captcha.NewGoogleReCaptchaProvider(http.NewFastHttpClient(), config.Global) + + if ok, err := reCaptcha.Verify(ctx); err != nil || !ok { if err != nil { response.NewCaptchaErrorResponse(err).Write(ctx) return diff --git a/router/captcha/captcha.go b/router/captcha/captcha.go new file mode 100644 index 0000000..79b0909 --- /dev/null +++ b/router/captcha/captcha.go @@ -0,0 +1,7 @@ +package captcha + +import "github.com/valyala/fasthttp" + +type Provider interface { + Verify(ctx *fasthttp.RequestCtx) (bool, error) +} diff --git a/router/captcha/client_mock_test.go b/router/captcha/client_mock_test.go deleted file mode 100644 index 846998b..0000000 --- a/router/captcha/client_mock_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package captcha - -import "github.com/valyala/fasthttp" - -type MockClient struct { - list []*MockExpectation - index uint -} - -func (m *MockClient) next() *MockExpectation { - i := m.index - m.index++ - return m.list[i] -} - -func (m *MockClient) Do(req *fasthttp.Request, resp *fasthttp.Response) error { - e := m.next() - if e == nil { - return nil - } - - if e.respFn != nil { - e.respFn(resp) - } - - e.req = &fasthttp.Request{} - req.CopyTo(e.req) - return e.err -} - -func (m *MockClient) Expect(at uint) *MockExpectation { - if m.list == nil { - m.list = make([]*MockExpectation, 1) - } - if len(m.list) <= int(at) { - m.list = append(m.list, &MockExpectation{}) - } - if m.list[at] == nil { - m.list[at] = &MockExpectation{} - } - return m.list[at] -} - -func (m *MockClient) GetRequestAt(at uint) *fasthttp.Request { - if m.list == nil || m.list[at] == nil { - return nil - } - return m.list[at].req -} - -func (m *MockClient) GetRequest() *fasthttp.Request { - return m.GetRequestAt(0) -} - -func (m *MockClient) ReturnError(err error) *MockExpectation { - m.Expect(0) - m.list[0].err = err - return m.list[0] -} - -func (m *MockClient) MockResponse(fn func(*fasthttp.Response)) *MockExpectation { - m.Expect(0) - m.list[0].respFn = fn - return m.list[0] -} - -type MockExpectation struct { - respFn func(*fasthttp.Response) - req *fasthttp.Request - err error -} - -func (m *MockExpectation) GetRequest() *fasthttp.Request { - return m.req -} - -func (m *MockExpectation) ReturnError(err error) *MockExpectation { - m.err = err - return m -} - -func (m *MockExpectation) MockResponse(fn func(*fasthttp.Response)) *MockExpectation { - m.respFn = fn - return m -} diff --git a/router/captcha/verify.go b/router/captcha/google_recaptcha.go similarity index 54% rename from router/captcha/verify.go rename to router/captcha/google_recaptcha.go index 5c2c93b..bb1cd04 100644 --- a/router/captcha/verify.go +++ b/router/captcha/google_recaptcha.go @@ -11,14 +11,21 @@ import ( "github.com/cash-track/gateway/config" "github.com/cash-track/gateway/headers" - api "github.com/cash-track/gateway/router/api/client" + "github.com/cash-track/gateway/http" ) -const verifyUrl = "https://www.google.com/recaptcha/api/siteverify" +const ( + googleApiReCaptchaVerifyUrl = "https://www.google.com/recaptcha/api/siteverify" + googleApiReadTimeout = 500 * time.Millisecond + googleApiWriteTimeout = time.Second +) -var client api.Client +type GoogleReCaptchaProvider struct { + client http.Client + secret string +} -type VerifyResponse struct { +type googleReCaptchaVerifyResponse struct { Success bool `json:"success"` ChallengeTS string `json:"challenge_ts,omitempty"` Hostname string `json:"hostname,omitempty"` @@ -27,31 +34,20 @@ type VerifyResponse struct { ErrorCodes []string `json:"error-codes,omitempty"` } -func init() { - newClient() -} - -func newClient() { - if client != nil { - return - } +func NewGoogleReCaptchaProvider(httpClient http.Client, options config.Config) *GoogleReCaptchaProvider { + httpClient.WithReadTimeout(googleApiReadTimeout) + httpClient.WithWriteTimeout(googleApiWriteTimeout) - client = &fasthttp.Client{ - ReadTimeout: 500 * time.Millisecond, - WriteTimeout: time.Second, - MaxIdleConnDuration: time.Hour, - NoDefaultUserAgentHeader: true, - Dial: (&fasthttp.TCPDialer{ - Concurrency: 4096, - DNSCacheDuration: time.Hour, - }).Dial, + return &GoogleReCaptchaProvider{ + client: httpClient, + secret: options.CaptchaSecret, } } -func Verify(ctx *fasthttp.RequestCtx) (bool, error) { +func (p *GoogleReCaptchaProvider) Verify(ctx *fasthttp.RequestCtx) (bool, error) { clientIp := headers.GetClientIPFromContext(ctx) - if config.Global.CaptchaSecret == "" { + if p.secret == "" { log.Printf("[%s] captcha secret empty, skipping verify", clientIp) return true, nil } @@ -69,19 +65,19 @@ func Verify(ctx *fasthttp.RequestCtx) (bool, error) { fasthttp.ReleaseResponse(resp) }() - prepareGoogleReCaptchaVerifyRequest(req, challenge, clientIp) + p.buildReq(req, challenge, clientIp) - if err := client.Do(req, resp); err != nil { + if err := p.client.Do(req, resp); err != nil { return false, fmt.Errorf("captcha verify request error: %w", err) } - verifyResponse := VerifyResponse{} - if err := json.Unmarshal(resp.Body(), &verifyResponse); err != nil { + verifyResp := googleReCaptchaVerifyResponse{} + if err := json.Unmarshal(resp.Body(), &verifyResp); err != nil { return false, fmt.Errorf("captcha verify response unexpected: %w", err) } - if !verifyResponse.Success { - log.Printf("[%s] captcha verify unsuccessfull: score %f, errors: %s", clientIp, verifyResponse.Score, strings.Join(verifyResponse.ErrorCodes, ", ")) + if !verifyResp.Success { + log.Printf("[%s] captcha verify unsuccessfull: score %f, errors: %s", clientIp, verifyResp.Score, strings.Join(verifyResp.ErrorCodes, ", ")) return false, nil } @@ -90,11 +86,11 @@ func Verify(ctx *fasthttp.RequestCtx) (bool, error) { return true, nil } -func prepareGoogleReCaptchaVerifyRequest(req *fasthttp.Request, challenge []byte, clientIp string) { - req.SetRequestURI(verifyUrl) +func (p *GoogleReCaptchaProvider) buildReq(req *fasthttp.Request, challenge []byte, clientIp string) { + req.SetRequestURI(googleApiReCaptchaVerifyUrl) req.Header.SetMethod(fasthttp.MethodPost) req.Header.SetContentTypeBytes(headers.ContentTypeForm) - req.PostArgs().Set("secret", config.Global.CaptchaSecret) + req.PostArgs().Set("secret", p.secret) req.PostArgs().Set("remoteip", clientIp) req.PostArgs().SetBytesV("response", challenge) } diff --git a/router/captcha/google_recaptcha_test.go b/router/captcha/google_recaptcha_test.go new file mode 100644 index 0000000..19cbebf --- /dev/null +++ b/router/captcha/google_recaptcha_test.go @@ -0,0 +1,169 @@ +package captcha + +import ( + "fmt" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" + "go.uber.org/mock/gomock" + + "github.com/cash-track/gateway/config" + "github.com/cash-track/gateway/headers" + "github.com/cash-track/gateway/mocks" +) + +func TestVerify(t *testing.T) { + ctrl := gomock.NewController(t) + c := mocks.NewMockClient(ctrl) + + ctx := fasthttp.RequestCtx{} + ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) + ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2") + + c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout)) + c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout)) + c.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + resp.SetBodyString(`{"success":true,"score":0.99,"error-codes":["no-error"]}`) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodPost, string(req.Header.Method())) + assert.Equal(t, googleApiReCaptchaVerifyUrl, req.URI().String()) + assert.Equal(t, string(headers.ContentTypeForm), string(req.Header.ContentType())) + assert.Equal(t, "captcha_secret_1", string(req.PostArgs().Peek("secret"))) + assert.Equal(t, "10.0.0.1", string(req.PostArgs().Peek("remoteip"))) + assert.Equal(t, "captcha_challenge_2", string(req.PostArgs().Peek("response"))) + + return nil + }) + + p := NewGoogleReCaptchaProvider(c, config.Config{ + CaptchaSecret: "captcha_secret_1", + }) + state, err := p.Verify(&ctx) + + assert.True(t, state) + assert.NoError(t, err) +} + +func TestVerifyUnsuccessful(t *testing.T) { + ctrl := gomock.NewController(t) + c := mocks.NewMockClient(ctrl) + + ctx := fasthttp.RequestCtx{} + ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) + ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2") + + c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout)) + c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout)) + c.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + resp.SetBodyString(`{"success":false,"score":0.99,"error-codes":["bad-input"]}`) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodPost, string(req.Header.Method())) + assert.Equal(t, googleApiReCaptchaVerifyUrl, req.URI().String()) + assert.Equal(t, string(headers.ContentTypeForm), string(req.Header.ContentType())) + assert.Equal(t, "captcha_secret_1", string(req.PostArgs().Peek("secret"))) + assert.Equal(t, "10.0.0.1", string(req.PostArgs().Peek("remoteip"))) + assert.Equal(t, "captcha_challenge_2", string(req.PostArgs().Peek("response"))) + + return nil + }) + + p := NewGoogleReCaptchaProvider(c, config.Config{ + CaptchaSecret: "captcha_secret_1", + }) + state, err := p.Verify(&ctx) + + assert.False(t, state) + assert.NoError(t, err) +} + +func TestVerifyEmptySecret(t *testing.T) { + ctrl := gomock.NewController(t) + c := mocks.NewMockClient(ctrl) + + ctx := fasthttp.RequestCtx{} + ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) + ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2") + + c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout)) + c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout)) + + p := NewGoogleReCaptchaProvider(c, config.Config{ + CaptchaSecret: "", + }) + state, err := p.Verify(&ctx) + + assert.True(t, state) + assert.NoError(t, err) +} + +func TestVerifyEmptyChallenge(t *testing.T) { + ctrl := gomock.NewController(t) + c := mocks.NewMockClient(ctrl) + + ctx := fasthttp.RequestCtx{} + ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) + ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "") + + c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout)) + c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout)) + + p := NewGoogleReCaptchaProvider(c, config.Config{ + CaptchaSecret: "captcha_secret_1", + }) + state, err := p.Verify(&ctx) + + assert.False(t, state) + assert.NoError(t, err) +} + +func TestVerifyRequestFail(t *testing.T) { + ctrl := gomock.NewController(t) + c := mocks.NewMockClient(ctrl) + + ctx := fasthttp.RequestCtx{} + ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) + ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2") + + c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout)) + c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout)) + c.EXPECT().Do(gomock.Any(), gomock.Any()).Return(fmt.Errorf("broken pipe")) + + p := NewGoogleReCaptchaProvider(c, config.Config{ + CaptchaSecret: "captcha_secret_1", + }) + state, err := p.Verify(&ctx) + + assert.False(t, state) + assert.Error(t, err) +} + +func TestVerifyBadResponse(t *testing.T) { + ctrl := gomock.NewController(t) + c := mocks.NewMockClient(ctrl) + + ctx := fasthttp.RequestCtx{} + ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) + ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2") + + c.EXPECT().WithReadTimeout(gomock.Eq(googleApiReadTimeout)) + c.EXPECT().WithWriteTimeout(gomock.Eq(googleApiWriteTimeout)) + c.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + resp.SetBodyString(`{"success":true`) + return nil + }) + + p := NewGoogleReCaptchaProvider(c, config.Config{ + CaptchaSecret: "captcha_secret_1", + }) + state, err := p.Verify(&ctx) + + assert.False(t, state) + assert.Error(t, err) +} diff --git a/router/captcha/verify_test.go b/router/captcha/verify_test.go deleted file mode 100644 index 5e1b6bf..0000000 --- a/router/captcha/verify_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package captcha - -import ( - "fmt" - "net" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/valyala/fasthttp" - - "github.com/cash-track/gateway/config" - "github.com/cash-track/gateway/headers" -) - -func TestVerify(t *testing.T) { - newClient() - - config.Global.CaptchaSecret = "captcha_secret_1" - - ctx := fasthttp.RequestCtx{} - ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) - ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2") - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - resp.SetBodyString(`{"success":true,"score":0.99,"error-codes":["no-error"]}`) - }) - - client = mock - - state, err := Verify(&ctx) - - assert.True(t, state) - assert.NoError(t, err) - - assert.NotNil(t, mock.GetRequest()) - assert.Equal(t, fasthttp.MethodPost, string(mock.GetRequest().Header.Method())) - assert.Equal(t, verifyUrl, mock.GetRequest().URI().String()) - assert.Equal(t, string(headers.ContentTypeForm), string(mock.GetRequest().Header.ContentType())) - assert.Equal(t, "captcha_secret_1", string(mock.GetRequest().PostArgs().Peek("secret"))) - assert.Equal(t, "10.0.0.1", string(mock.GetRequest().PostArgs().Peek("remoteip"))) - assert.Equal(t, "captcha_challenge_2", string(mock.GetRequest().PostArgs().Peek("response"))) -} - -func TestVerifyUnsuccessful(t *testing.T) { - config.Global.CaptchaSecret = "captcha_secret_1" - - ctx := fasthttp.RequestCtx{} - ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) - ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2") - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - resp.SetBodyString(`{"success":false,"score":0.99,"error-codes":["bad-input"]}`) - }) - - client = mock - - state, err := Verify(&ctx) - - assert.False(t, state) - assert.NoError(t, err) - - assert.NotNil(t, mock.GetRequest()) - assert.Equal(t, fasthttp.MethodPost, string(mock.GetRequest().Header.Method())) - assert.Equal(t, verifyUrl, mock.GetRequest().URI().String()) - assert.Equal(t, string(headers.ContentTypeForm), string(mock.GetRequest().Header.ContentType())) - assert.Equal(t, "captcha_secret_1", string(mock.GetRequest().PostArgs().Peek("secret"))) - assert.Equal(t, "10.0.0.1", string(mock.GetRequest().PostArgs().Peek("remoteip"))) - assert.Equal(t, "captcha_challenge_2", string(mock.GetRequest().PostArgs().Peek("response"))) -} - -func TestVerifyEmptySecret(t *testing.T) { - config.Global.CaptchaSecret = "" - - ctx := fasthttp.RequestCtx{} - - state, err := Verify(&ctx) - - assert.True(t, state) - assert.NoError(t, err) -} - -func TestVerifyEmptyChallenge(t *testing.T) { - config.Global.CaptchaSecret = "captcha_secret_1" - - ctx := fasthttp.RequestCtx{} - ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) - ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "") - - state, err := Verify(&ctx) - - assert.False(t, state) - assert.NoError(t, err) -} - -func TestVerifyRequestFail(t *testing.T) { - config.Global.CaptchaSecret = "captcha_secret_1" - - ctx := fasthttp.RequestCtx{} - ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) - ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2") - - mock := &MockClient{} - mock.ReturnError(fmt.Errorf("broken pipe")) - - client = mock - - state, err := Verify(&ctx) - - assert.False(t, state) - assert.Error(t, err) -} - -func TestVerifyBadResponse(t *testing.T) { - config.Global.CaptchaSecret = "captcha_secret_1" - - ctx := fasthttp.RequestCtx{} - ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) - ctx.Request.Header.Set(headers.XCtCaptchaChallenge, "captcha_challenge_2") - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - resp.SetBodyString(`{"success":true`) - }) - - client = mock - - state, err := Verify(&ctx) - - assert.False(t, state) - assert.Error(t, err) -} From 3b24b676e13592ee57526ffcef5c37d7722ec7a1 Mon Sep 17 00:00:00 2001 From: vokomarov Date: Mon, 1 Jan 2024 13:16:03 +0200 Subject: [PATCH 2/5] Move captcha package --- {router/captcha => captcha}/captcha.go | 0 .../captcha => captcha}/google_recaptcha.go | 0 .../google_recaptcha_test.go | 0 main.go | 4 +-- router/api/handler.go | 2 +- router/{healthcheck => }/healthcheck.go | 10 ++++---- router/{healthcheck => }/healthcheck_test.go | 6 +++-- router/router.go | 25 +++++++++++++------ router/router_test.go | 3 ++- 9 files changed, 32 insertions(+), 18 deletions(-) rename {router/captcha => captcha}/captcha.go (100%) rename {router/captcha => captcha}/google_recaptcha.go (100%) rename {router/captcha => captcha}/google_recaptcha_test.go (100%) rename router/{healthcheck => }/healthcheck.go (69%) rename router/{healthcheck => }/healthcheck_test.go (73%) diff --git a/router/captcha/captcha.go b/captcha/captcha.go similarity index 100% rename from router/captcha/captcha.go rename to captcha/captcha.go diff --git a/router/captcha/google_recaptcha.go b/captcha/google_recaptcha.go similarity index 100% rename from router/captcha/google_recaptcha.go rename to captcha/google_recaptcha.go diff --git a/router/captcha/google_recaptcha_test.go b/captcha/google_recaptcha_test.go similarity index 100% rename from router/captcha/google_recaptcha_test.go rename to captcha/google_recaptcha_test.go diff --git a/main.go b/main.go index 76d45c4..7eaddb0 100644 --- a/main.go +++ b/main.go @@ -20,8 +20,8 @@ const ( func main() { config.Global.Load() - r := router.New() - h := prom.NewPrometheus("http").WrapHandler(r) + r := router.New(config.Global) + h := prom.NewPrometheus("http").WrapHandler(r.Router) h = headers.Handler(h) h = headers.CorsHandler(h) h = logger.DebugHandler(h) diff --git a/router/api/handler.go b/router/api/handler.go index fc0befe..0a4a546 100644 --- a/router/api/handler.go +++ b/router/api/handler.go @@ -6,11 +6,11 @@ import ( "github.com/valyala/fasthttp" + "github.com/cash-track/gateway/captcha" "github.com/cash-track/gateway/config" "github.com/cash-track/gateway/headers/cookie" "github.com/cash-track/gateway/http" "github.com/cash-track/gateway/router/api/client" - "github.com/cash-track/gateway/router/captcha" "github.com/cash-track/gateway/router/response" ) diff --git a/router/healthcheck/healthcheck.go b/router/healthcheck.go similarity index 69% rename from router/healthcheck/healthcheck.go rename to router/healthcheck.go index de3cd16..2a9654d 100644 --- a/router/healthcheck/healthcheck.go +++ b/router/healthcheck.go @@ -1,11 +1,11 @@ -package healthcheck +package router import ( "log" "github.com/valyala/fasthttp" - apiClient "github.com/cash-track/gateway/router/api/client" + api "github.com/cash-track/gateway/router/api/client" ) var ( @@ -14,14 +14,14 @@ var ( ) // LiveHandler consider liveness check successful if request reached the handler -func LiveHandler(ctx *fasthttp.RequestCtx) { +func (r *Router) LiveHandler(ctx *fasthttp.RequestCtx) { ctx.SetStatusCode(fasthttp.StatusOK) ctx.SetBody(bodyOk) } // ReadyHandler check all dependency for service readiness -func ReadyHandler(ctx *fasthttp.RequestCtx) { - if err := apiClient.Healthcheck(); err != nil { +func (r *Router) ReadyHandler(ctx *fasthttp.RequestCtx) { + if err := api.Healthcheck(); err != nil { log.Printf("API not ready: %s", err.Error()) ctx.SetStatusCode(fasthttp.StatusInternalServerError) ctx.SetBody(bodyApiNok) diff --git a/router/healthcheck/healthcheck_test.go b/router/healthcheck_test.go similarity index 73% rename from router/healthcheck/healthcheck_test.go rename to router/healthcheck_test.go index d38f364..e294448 100644 --- a/router/healthcheck/healthcheck_test.go +++ b/router/healthcheck_test.go @@ -1,16 +1,18 @@ -package healthcheck +package router import ( "testing" + "github.com/cash-track/gateway/config" "github.com/stretchr/testify/assert" "github.com/valyala/fasthttp" ) func TestLiveHandler(t *testing.T) { ctx := fasthttp.RequestCtx{} + r := New(config.Config{}) - LiveHandler(&ctx) + r.LiveHandler(&ctx) assert.Equal(t, fasthttp.StatusOK, ctx.Response.StatusCode()) assert.Equal(t, "ok", string(ctx.Response.Body())) diff --git a/router/router.go b/router/router.go index 74be6d6..5b7da39 100644 --- a/router/router.go +++ b/router/router.go @@ -3,20 +3,31 @@ package router import ( "github.com/fasthttp/router" + "github.com/cash-track/gateway/config" "github.com/cash-track/gateway/router/api" - "github.com/cash-track/gateway/router/healthcheck" ) -func New() *router.Router { - r := router.New() - r.ANY("/live", healthcheck.LiveHandler) - r.ANY("/ready", healthcheck.ReadyHandler) +type Router struct { + *router.Router + config config.Config +} + +func New(config config.Config) *Router { + r := &Router{ + Router: router.New(), + config: config, + } + r.register() + return r +} + +func (r *Router) register() { + r.ANY("/live", r.LiveHandler) + r.ANY("/ready", r.ReadyHandler) r.POST("/api/auth/login", api.AuthSetHandler) r.POST("/api/auth/register", api.AuthSetHandler) r.POST("/api/auth/provider/google", api.AuthSetHandler) r.POST("/api/auth/logout", api.AuthResetHandler) r.ANY("/api/{path:*}", api.FullForwardedHandler) - - return r } diff --git a/router/router_test.go b/router/router_test.go index eb324b9..6d0c3db 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -3,11 +3,12 @@ package router import ( "testing" + "github.com/cash-track/gateway/config" "github.com/stretchr/testify/assert" ) func TestNew(t *testing.T) { - r := New() + r := New(config.Config{}) l := r.List() From a6a98df33f349ba70ec948c98076ee86948431ff Mon Sep 17 00:00:00 2001 From: vokomarov Date: Mon, 1 Jan 2024 21:36:58 +0200 Subject: [PATCH 3/5] Add more mocks, refactor api client --- Makefile | 5 +- captcha/google_recaptcha_test.go | 12 +- captcha/{captcha.go => provider.go} | 0 main.go | 12 +- mocks/api_handler_mock.go | 90 ++++++ mocks/api_service_mock.go | 68 +++++ mocks/captcha_provider_mock.go | 55 ++++ mocks/http_client_mock.go | 42 +-- router/api/client/client.go | 57 ---- router/api/client/client_mock_test.go | 85 ------ router/api/client/client_test.go | 46 --- router/api/client/forward_test.go | 203 -------------- router/api/client/healthcheck_test.go | 65 ----- router/api/client/refresh_token_test.go | 128 --------- router/api/handler.go | 50 +++- router/api/login.go | 4 +- router/api/login_test.go | 29 +- router/api/logout.go | 4 +- router/api/logout_test.go | 11 +- router/api/redirect_response.go | 18 +- router/healthcheck.go | 4 +- router/healthcheck_test.go | 38 ++- router/router.go | 17 +- router/router_test.go | 8 +- {router/api/client => service/api}/forward.go | 26 +- service/api/forward_test.go | 261 ++++++++++++++++++ .../api/client => service/api}/healthcheck.go | 12 +- service/api/healthcheck_test.go | 79 ++++++ .../client => service/api}/refresh_token.go | 12 +- service/api/refresh_token_test.go | 160 +++++++++++ service/api/service.go | 56 ++++ service/api/service_test.go | 63 +++++ 32 files changed, 1032 insertions(+), 688 deletions(-) rename captcha/{captcha.go => provider.go} (100%) create mode 100644 mocks/api_handler_mock.go create mode 100644 mocks/api_service_mock.go create mode 100644 mocks/captcha_provider_mock.go delete mode 100644 router/api/client/client.go delete mode 100644 router/api/client/client_mock_test.go delete mode 100644 router/api/client/client_test.go delete mode 100644 router/api/client/forward_test.go delete mode 100644 router/api/client/healthcheck_test.go delete mode 100644 router/api/client/refresh_token_test.go rename {router/api/client => service/api}/forward.go (80%) create mode 100644 service/api/forward_test.go rename {router/api/client => service/api}/healthcheck.go (79%) create mode 100644 service/api/healthcheck_test.go rename {router/api/client => service/api}/refresh_token.go (84%) create mode 100644 service/api/refresh_token_test.go create mode 100644 service/api/service.go create mode 100644 service/api/service_test.go diff --git a/Makefile b/Makefile index 8dae194..5db52c1 100644 --- a/Makefile +++ b/Makefile @@ -45,5 +45,8 @@ stop: mock-gen: go install go.uber.org/mock/mockgen@latest - mockgen -source=http/client.go -package=mocks -destination=mocks/http_client_mock.go Client + mockgen -source=http/client.go -package=mocks -destination=mocks/http_client_mock.go -mock_names=Client=HttpClientMock + mockgen -source=captcha/provider.go -package=mocks -destination=mocks/captcha_provider_mock.go -mock_names=Provider=CaptchaProviderMock + mockgen -source=service/api/service.go -package=mocks -destination=mocks/api_service_mock.go -mock_names=Service=ApiServiceMock + mockgen -source=router/api/handler.go -package=mocks -destination=mocks/api_handler_mock.go -mock_names=Handler=ApiHandlerMock diff --git a/captcha/google_recaptcha_test.go b/captcha/google_recaptcha_test.go index 19cbebf..6a062c0 100644 --- a/captcha/google_recaptcha_test.go +++ b/captcha/google_recaptcha_test.go @@ -16,7 +16,7 @@ import ( func TestVerify(t *testing.T) { ctrl := gomock.NewController(t) - c := mocks.NewMockClient(ctrl) + c := mocks.NewHttpClientMock(ctrl) ctx := fasthttp.RequestCtx{} ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) @@ -50,7 +50,7 @@ func TestVerify(t *testing.T) { func TestVerifyUnsuccessful(t *testing.T) { ctrl := gomock.NewController(t) - c := mocks.NewMockClient(ctrl) + c := mocks.NewHttpClientMock(ctrl) ctx := fasthttp.RequestCtx{} ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) @@ -84,7 +84,7 @@ func TestVerifyUnsuccessful(t *testing.T) { func TestVerifyEmptySecret(t *testing.T) { ctrl := gomock.NewController(t) - c := mocks.NewMockClient(ctrl) + c := mocks.NewHttpClientMock(ctrl) ctx := fasthttp.RequestCtx{} ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) @@ -104,7 +104,7 @@ func TestVerifyEmptySecret(t *testing.T) { func TestVerifyEmptyChallenge(t *testing.T) { ctrl := gomock.NewController(t) - c := mocks.NewMockClient(ctrl) + c := mocks.NewHttpClientMock(ctrl) ctx := fasthttp.RequestCtx{} ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) @@ -124,7 +124,7 @@ func TestVerifyEmptyChallenge(t *testing.T) { func TestVerifyRequestFail(t *testing.T) { ctrl := gomock.NewController(t) - c := mocks.NewMockClient(ctrl) + c := mocks.NewHttpClientMock(ctrl) ctx := fasthttp.RequestCtx{} ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) @@ -145,7 +145,7 @@ func TestVerifyRequestFail(t *testing.T) { func TestVerifyBadResponse(t *testing.T) { ctrl := gomock.NewController(t) - c := mocks.NewMockClient(ctrl) + c := mocks.NewHttpClientMock(ctrl) ctx := fasthttp.RequestCtx{} ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) diff --git a/captcha/captcha.go b/captcha/provider.go similarity index 100% rename from captcha/captcha.go rename to captcha/provider.go diff --git a/main.go b/main.go index 7eaddb0..b65430c 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,14 @@ import ( prom "github.com/flf2ko/fasthttp-prometheus" "github.com/valyala/fasthttp" + "github.com/cash-track/gateway/captcha" "github.com/cash-track/gateway/config" "github.com/cash-track/gateway/headers" + "github.com/cash-track/gateway/http" "github.com/cash-track/gateway/logger" "github.com/cash-track/gateway/router" + apiHandler "github.com/cash-track/gateway/router/api" + apiService "github.com/cash-track/gateway/service/api" ) const ( @@ -20,7 +24,13 @@ const ( func main() { config.Global.Load() - r := router.New(config.Global) + r := router.New( + apiHandler.NewHttp( + config.Global, + apiService.NewHttp(http.NewFastHttpClient(), config.Global), + captcha.NewGoogleReCaptchaProvider(http.NewFastHttpClient(), config.Global), + ), + ) h := prom.NewPrometheus("http").WrapHandler(r.Router) h = headers.Handler(h) h = headers.CorsHandler(h) diff --git a/mocks/api_handler_mock.go b/mocks/api_handler_mock.go new file mode 100644 index 0000000..efd803a --- /dev/null +++ b/mocks/api_handler_mock.go @@ -0,0 +1,90 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: router/api/handler.go +// +// Generated by this command: +// +// mockgen -source=router/api/handler.go -package=mocks -destination=mocks/api_handler_mock.go -mock_names=Handler=ApiHandlerMock +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + fasthttp "github.com/valyala/fasthttp" + gomock "go.uber.org/mock/gomock" +) + +// ApiHandlerMock is a mock of Handler interface. +type ApiHandlerMock struct { + ctrl *gomock.Controller + recorder *ApiHandlerMockMockRecorder +} + +// ApiHandlerMockMockRecorder is the mock recorder for ApiHandlerMock. +type ApiHandlerMockMockRecorder struct { + mock *ApiHandlerMock +} + +// NewApiHandlerMock creates a new mock instance. +func NewApiHandlerMock(ctrl *gomock.Controller) *ApiHandlerMock { + mock := &ApiHandlerMock{ctrl: ctrl} + mock.recorder = &ApiHandlerMockMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *ApiHandlerMock) EXPECT() *ApiHandlerMockMockRecorder { + return m.recorder +} + +// AuthResetHandler mocks base method. +func (m *ApiHandlerMock) AuthResetHandler(ctx *fasthttp.RequestCtx) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AuthResetHandler", ctx) +} + +// AuthResetHandler indicates an expected call of AuthResetHandler. +func (mr *ApiHandlerMockMockRecorder) AuthResetHandler(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthResetHandler", reflect.TypeOf((*ApiHandlerMock)(nil).AuthResetHandler), ctx) +} + +// AuthSetHandler mocks base method. +func (m *ApiHandlerMock) AuthSetHandler(ctx *fasthttp.RequestCtx) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AuthSetHandler", ctx) +} + +// AuthSetHandler indicates an expected call of AuthSetHandler. +func (mr *ApiHandlerMockMockRecorder) AuthSetHandler(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthSetHandler", reflect.TypeOf((*ApiHandlerMock)(nil).AuthSetHandler), ctx) +} + +// FullForwardedHandler mocks base method. +func (m *ApiHandlerMock) FullForwardedHandler(ctx *fasthttp.RequestCtx) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "FullForwardedHandler", ctx) +} + +// FullForwardedHandler indicates an expected call of FullForwardedHandler. +func (mr *ApiHandlerMockMockRecorder) FullForwardedHandler(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FullForwardedHandler", reflect.TypeOf((*ApiHandlerMock)(nil).FullForwardedHandler), ctx) +} + +// Healthcheck mocks base method. +func (m *ApiHandlerMock) Healthcheck() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Healthcheck") + ret0, _ := ret[0].(error) + return ret0 +} + +// Healthcheck indicates an expected call of Healthcheck. +func (mr *ApiHandlerMockMockRecorder) Healthcheck() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Healthcheck", reflect.TypeOf((*ApiHandlerMock)(nil).Healthcheck)) +} diff --git a/mocks/api_service_mock.go b/mocks/api_service_mock.go new file mode 100644 index 0000000..dd88cc1 --- /dev/null +++ b/mocks/api_service_mock.go @@ -0,0 +1,68 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: service/api/service.go +// +// Generated by this command: +// +// mockgen -source=service/api/service.go -package=mocks -destination=mocks/api_service_mock.go -mock_names=Service=ApiServiceMock +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + fasthttp "github.com/valyala/fasthttp" + gomock "go.uber.org/mock/gomock" +) + +// ApiServiceMock is a mock of Service interface. +type ApiServiceMock struct { + ctrl *gomock.Controller + recorder *ApiServiceMockMockRecorder +} + +// ApiServiceMockMockRecorder is the mock recorder for ApiServiceMock. +type ApiServiceMockMockRecorder struct { + mock *ApiServiceMock +} + +// NewApiServiceMock creates a new mock instance. +func NewApiServiceMock(ctrl *gomock.Controller) *ApiServiceMock { + mock := &ApiServiceMock{ctrl: ctrl} + mock.recorder = &ApiServiceMockMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *ApiServiceMock) EXPECT() *ApiServiceMockMockRecorder { + return m.recorder +} + +// ForwardRequest mocks base method. +func (m *ApiServiceMock) ForwardRequest(ctx *fasthttp.RequestCtx, body []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ForwardRequest", ctx, body) + ret0, _ := ret[0].(error) + return ret0 +} + +// ForwardRequest indicates an expected call of ForwardRequest. +func (mr *ApiServiceMockMockRecorder) ForwardRequest(ctx, body any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForwardRequest", reflect.TypeOf((*ApiServiceMock)(nil).ForwardRequest), ctx, body) +} + +// Healthcheck mocks base method. +func (m *ApiServiceMock) Healthcheck() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Healthcheck") + ret0, _ := ret[0].(error) + return ret0 +} + +// Healthcheck indicates an expected call of Healthcheck. +func (mr *ApiServiceMockMockRecorder) Healthcheck() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Healthcheck", reflect.TypeOf((*ApiServiceMock)(nil).Healthcheck)) +} diff --git a/mocks/captcha_provider_mock.go b/mocks/captcha_provider_mock.go new file mode 100644 index 0000000..7974d25 --- /dev/null +++ b/mocks/captcha_provider_mock.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: captcha/provider.go +// +// Generated by this command: +// +// mockgen -source=captcha/provider.go -package=mocks -destination=mocks/captcha_provider_mock.go -mock_names=Provider=CaptchaProviderMock +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + fasthttp "github.com/valyala/fasthttp" + gomock "go.uber.org/mock/gomock" +) + +// CaptchaProviderMock is a mock of Provider interface. +type CaptchaProviderMock struct { + ctrl *gomock.Controller + recorder *CaptchaProviderMockMockRecorder +} + +// CaptchaProviderMockMockRecorder is the mock recorder for CaptchaProviderMock. +type CaptchaProviderMockMockRecorder struct { + mock *CaptchaProviderMock +} + +// NewCaptchaProviderMock creates a new mock instance. +func NewCaptchaProviderMock(ctrl *gomock.Controller) *CaptchaProviderMock { + mock := &CaptchaProviderMock{ctrl: ctrl} + mock.recorder = &CaptchaProviderMockMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *CaptchaProviderMock) EXPECT() *CaptchaProviderMockMockRecorder { + return m.recorder +} + +// Verify mocks base method. +func (m *CaptchaProviderMock) Verify(ctx *fasthttp.RequestCtx) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Verify", ctx) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Verify indicates an expected call of Verify. +func (mr *CaptchaProviderMockMockRecorder) Verify(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*CaptchaProviderMock)(nil).Verify), ctx) +} diff --git a/mocks/http_client_mock.go b/mocks/http_client_mock.go index c19ed89..4ccf13c 100644 --- a/mocks/http_client_mock.go +++ b/mocks/http_client_mock.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -source=http/client.go -package=mocks -destination=mocks/http_client_mock.go Client +// mockgen -source=http/client.go -package=mocks -destination=mocks/http_client_mock.go -mock_names=Client=HttpClientMock // // Package mocks is a generated GoMock package. @@ -18,31 +18,31 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockClient is a mock of Client interface. -type MockClient struct { +// HttpClientMock is a mock of Client interface. +type HttpClientMock struct { ctrl *gomock.Controller - recorder *MockClientMockRecorder + recorder *HttpClientMockMockRecorder } -// MockClientMockRecorder is the mock recorder for MockClient. -type MockClientMockRecorder struct { - mock *MockClient +// HttpClientMockMockRecorder is the mock recorder for HttpClientMock. +type HttpClientMockMockRecorder struct { + mock *HttpClientMock } -// NewMockClient creates a new mock instance. -func NewMockClient(ctrl *gomock.Controller) *MockClient { - mock := &MockClient{ctrl: ctrl} - mock.recorder = &MockClientMockRecorder{mock} +// NewHttpClientMock creates a new mock instance. +func NewHttpClientMock(ctrl *gomock.Controller) *HttpClientMock { + mock := &HttpClientMock{ctrl: ctrl} + mock.recorder = &HttpClientMockMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockClient) EXPECT() *MockClientMockRecorder { +func (m *HttpClientMock) EXPECT() *HttpClientMockMockRecorder { return m.recorder } // Do mocks base method. -func (m *MockClient) Do(req *fasthttp.Request, resp *fasthttp.Response) error { +func (m *HttpClientMock) Do(req *fasthttp.Request, resp *fasthttp.Response) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Do", req, resp) ret0, _ := ret[0].(error) @@ -50,13 +50,13 @@ func (m *MockClient) Do(req *fasthttp.Request, resp *fasthttp.Response) error { } // Do indicates an expected call of Do. -func (mr *MockClientMockRecorder) Do(req, resp any) *gomock.Call { +func (mr *HttpClientMockMockRecorder) Do(req, resp any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockClient)(nil).Do), req, resp) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*HttpClientMock)(nil).Do), req, resp) } // WithReadTimeout mocks base method. -func (m *MockClient) WithReadTimeout(timeout time.Duration) http.Client { +func (m *HttpClientMock) WithReadTimeout(timeout time.Duration) http.Client { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WithReadTimeout", timeout) ret0, _ := ret[0].(http.Client) @@ -64,13 +64,13 @@ func (m *MockClient) WithReadTimeout(timeout time.Duration) http.Client { } // WithReadTimeout indicates an expected call of WithReadTimeout. -func (mr *MockClientMockRecorder) WithReadTimeout(timeout any) *gomock.Call { +func (mr *HttpClientMockMockRecorder) WithReadTimeout(timeout any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithReadTimeout", reflect.TypeOf((*MockClient)(nil).WithReadTimeout), timeout) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithReadTimeout", reflect.TypeOf((*HttpClientMock)(nil).WithReadTimeout), timeout) } // WithWriteTimeout mocks base method. -func (m *MockClient) WithWriteTimeout(timeout time.Duration) http.Client { +func (m *HttpClientMock) WithWriteTimeout(timeout time.Duration) http.Client { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WithWriteTimeout", timeout) ret0, _ := ret[0].(http.Client) @@ -78,7 +78,7 @@ func (m *MockClient) WithWriteTimeout(timeout time.Duration) http.Client { } // WithWriteTimeout indicates an expected call of WithWriteTimeout. -func (mr *MockClientMockRecorder) WithWriteTimeout(timeout any) *gomock.Call { +func (mr *HttpClientMockMockRecorder) WithWriteTimeout(timeout any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithWriteTimeout", reflect.TypeOf((*MockClient)(nil).WithWriteTimeout), timeout) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithWriteTimeout", reflect.TypeOf((*HttpClientMock)(nil).WithWriteTimeout), timeout) } diff --git a/router/api/client/client.go b/router/api/client/client.go deleted file mode 100644 index 7a78317..0000000 --- a/router/api/client/client.go +++ /dev/null @@ -1,57 +0,0 @@ -package client - -import ( - "strings" - "time" - - "github.com/valyala/fasthttp" - - "github.com/cash-track/gateway/config" -) - -func init() { - NewClient() -} - -const Service = "API" - -type Client interface { - Do(req *fasthttp.Request, resp *fasthttp.Response) error -} - -var ( - client Client - methodsWithBody = map[string]bool{ - fasthttp.MethodPost: true, - fasthttp.MethodPut: true, - fasthttp.MethodPatch: true, - } -) - -func NewClient() { - client = &fasthttp.Client{ - ReadTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, - MaxIdleConnDuration: time.Hour, - NoDefaultUserAgentHeader: true, - DisableHeaderNamesNormalizing: true, - DisablePathNormalizing: true, - Dial: (&fasthttp.TCPDialer{ - Concurrency: 4096, - DNSCacheDuration: time.Hour, - }).Dial, - } -} - -func setRequestURI(dest *fasthttp.URI, path []byte) { - _ = dest.Parse([]byte(config.Global.ApiUrl), nil) - dest.SetScheme(config.Global.ApiURI.Scheme) - dest.SetHost(config.Global.ApiURI.Host) - dest.SetPathBytes(path) -} - -func copyRequestURI(src, dest *fasthttp.URI) { - path := strings.TrimPrefix(string(src.PathOriginal()), "/api") - setRequestURI(dest, []byte(path)) - dest.SetQueryStringBytes(src.QueryString()) -} diff --git a/router/api/client/client_mock_test.go b/router/api/client/client_mock_test.go deleted file mode 100644 index 215f72b..0000000 --- a/router/api/client/client_mock_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package client - -import "github.com/valyala/fasthttp" - -type MockClient struct { - list []*MockExpectation - index uint -} - -func (m *MockClient) next() *MockExpectation { - i := m.index - m.index++ - return m.list[i] -} - -func (m *MockClient) Do(req *fasthttp.Request, resp *fasthttp.Response) error { - e := m.next() - if e == nil { - return nil - } - - if e.respFn != nil { - e.respFn(resp) - } - - e.req = &fasthttp.Request{} - req.CopyTo(e.req) - return e.err -} - -func (m *MockClient) Expect(at uint) *MockExpectation { - if m.list == nil { - m.list = make([]*MockExpectation, 1) - } - if len(m.list) <= int(at) { - m.list = append(m.list, &MockExpectation{}) - } - if m.list[at] == nil { - m.list[at] = &MockExpectation{} - } - return m.list[at] -} - -func (m *MockClient) GetRequestAt(at uint) *fasthttp.Request { - if m.list == nil || m.list[at] == nil { - return nil - } - return m.list[at].req -} - -func (m *MockClient) GetRequest() *fasthttp.Request { - return m.GetRequestAt(0) -} - -func (m *MockClient) ReturnError(err error) *MockExpectation { - m.Expect(0) - m.list[0].err = err - return m.list[0] -} - -func (m *MockClient) MockResponse(fn func(*fasthttp.Response)) *MockExpectation { - m.Expect(0) - m.list[0].respFn = fn - return m.list[0] -} - -type MockExpectation struct { - respFn func(*fasthttp.Response) - req *fasthttp.Request - err error -} - -func (m *MockExpectation) GetRequest() *fasthttp.Request { - return m.req -} - -func (m *MockExpectation) ReturnError(err error) *MockExpectation { - m.err = err - return m -} - -func (m *MockExpectation) MockResponse(fn func(*fasthttp.Response)) *MockExpectation { - m.respFn = fn - return m -} diff --git a/router/api/client/client_test.go b/router/api/client/client_test.go deleted file mode 100644 index d26a2ec..0000000 --- a/router/api/client/client_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package client - -import ( - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/valyala/fasthttp" - - "github.com/cash-track/gateway/config" -) - -func TestNewClient(t *testing.T) { - NewClient() - - assert.NotNil(t, client) - - if c, ok := client.(*fasthttp.Client); ok { - assert.True(t, c.NoDefaultUserAgentHeader) - assert.True(t, c.DisableHeaderNamesNormalizing) - assert.True(t, c.DisablePathNormalizing) - } -} - -func TestSetRequestURI(t *testing.T) { - config.Global.ApiURI, _ = url.Parse("http://api.test.com") - - uri := fasthttp.URI{} - - setRequestURI(&uri, []byte("/users/create one")) - - assert.Equal(t, "http://api.test.com/users/create%20one", uri.String()) -} - -func TestCopyRequestURI(t *testing.T) { - config.Global.ApiURI, _ = url.Parse("http://api.test.com") - - src := fasthttp.URI{} - src.SetPath("/api/users/create one") - src.SetQueryString("one=two%203") - dest := fasthttp.URI{} - - copyRequestURI(&src, &dest) - - assert.Equal(t, "http://api.test.com/users/create%20one?one=two%203", dest.String()) -} diff --git a/router/api/client/forward_test.go b/router/api/client/forward_test.go deleted file mode 100644 index 626a0c9..0000000 --- a/router/api/client/forward_test.go +++ /dev/null @@ -1,203 +0,0 @@ -package client - -import ( - "fmt" - "net" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/valyala/fasthttp" - - "github.com/cash-track/gateway/config" - "github.com/cash-track/gateway/headers" - "github.com/cash-track/gateway/headers/cookie" -) - -func TestFullForwardRequestWithAuth(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - uri := &fasthttp.URI{} - _ = uri.Parse(nil, []byte("https://gateway.test.com/api/auth/profile")) - - ctx := fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod(fasthttp.MethodPatch) - ctx.Request.Header.SetCookie(cookie.AccessTokenCookieName, "access_token") - ctx.Request.SetURI(uri) - ctx.Request.SetBodyString(`{"status":"ok"}`) - ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - }) - - client = mock - err := ForwardRequest(&ctx, nil) - - assert.NoError(t, err) - assert.NotNil(t, mock.GetRequest()) - assert.Equal(t, fasthttp.MethodPatch, string(mock.GetRequest().Header.Method())) - assert.Equal(t, fmt.Sprintf("%s%s", endpoint, "/auth/profile"), mock.GetRequest().URI().String()) - assert.Equal(t, string(headers.ContentTypeJson), string(mock.GetRequest().Header.ContentType())) - assert.Equal(t, string(headers.ContentTypeJson), string(mock.GetRequest().Header.Peek(headers.Accept))) - assert.Equal(t, "10.0.0.1", string(mock.GetRequest().Header.Peek(headers.XForwardedFor))) - assert.Equal(t, "Bearer access_token", string(mock.GetRequest().Header.Peek(headers.Authorization))) - assert.Equal(t, `{"status":"ok"}`, string(mock.GetRequest().Body())) -} - -func TestForwardRequestWithBodyOverride(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - ctx := fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod(fasthttp.MethodPost) - ctx.Request.SetBodyString(`{"status":"ok"}`) - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - }) - - client = mock - err := ForwardRequest(&ctx, []byte(`{"status":"false"}`)) - - assert.NoError(t, err) - assert.NotNil(t, mock.GetRequest()) - assert.Equal(t, fasthttp.MethodPost, string(mock.GetRequest().Header.Method())) - - assert.Equal(t, `{"status":"false"}`, string(mock.GetRequest().Body())) -} - -func TestForwardRequestWithAuthRefresh(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - ctx := fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod(fasthttp.MethodGet) - ctx.Request.Header.SetCookie(cookie.AccessTokenCookieName, "access_token") - ctx.Request.Header.SetCookie(cookie.RefreshTokenCookieName, "refresh_token") - - mock := &MockClient{} - mock.Expect(0).MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusUnauthorized) - }) - mock.Expect(1).MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - resp.SetBodyString(fmt.Sprintf(`{"accessToken":"%s","refreshToken":"%s"}`, "new_access_token", "new_refresh_token")) - }) - mock.Expect(2).MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - }) - - client = mock - err := ForwardRequest(&ctx, nil) - - assert.NoError(t, err) - assert.NotNil(t, mock.GetRequest()) - assert.Equal(t, fasthttp.MethodGet, string(mock.GetRequest().Header.Method())) - assert.Equal(t, "Bearer access_token", string(mock.GetRequestAt(0).Header.Peek(headers.Authorization))) - assert.Equal(t, fasthttp.MethodGet, string(mock.GetRequestAt(2).Header.Method())) - assert.Equal(t, "Bearer new_access_token", string(mock.GetRequestAt(2).Header.Peek(headers.Authorization))) - assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.AccessTokenCookieName)), "new_access_token") - assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.RefreshTokenCookieName)), "new_refresh_token") -} - -func TestForwardRequestWithAuthRefreshFailLogout(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - ctx := fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod(fasthttp.MethodGet) - ctx.Request.Header.SetCookie(cookie.AccessTokenCookieName, "access_token") - ctx.Request.Header.SetCookie(cookie.RefreshTokenCookieName, "refresh_token") - - mock := &MockClient{} - mock.Expect(0).MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusUnauthorized) - }) - mock.Expect(1).ReturnError(fmt.Errorf("broken pipe")) - - client = mock - err := ForwardRequest(&ctx, nil) - - assert.NoError(t, err) - assert.NotNil(t, mock.GetRequest()) - assert.Equal(t, fasthttp.MethodGet, string(mock.GetRequest().Header.Method())) - assert.Equal(t, "Bearer access_token", string(mock.GetRequestAt(0).Header.Peek(headers.Authorization))) - assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.AccessTokenCookieName)), fmt.Sprintf("%s=;", cookie.AccessTokenCookieName)) - assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.RefreshTokenCookieName)), fmt.Sprintf("%s=;", cookie.RefreshTokenCookieName)) -} - -func TestForwardRequestWithAuthRefreshSecondFail(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - ctx := fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod(fasthttp.MethodGet) - ctx.Request.Header.SetCookie(cookie.AccessTokenCookieName, "access_token") - ctx.Request.Header.SetCookie(cookie.RefreshTokenCookieName, "refresh_token") - - mock := &MockClient{} - mock.Expect(0).MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusUnauthorized) - }) - mock.Expect(1).MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - resp.SetBodyString(fmt.Sprintf(`{"accessToken":"%s","refreshToken":"%s"}`, "new_access_token", "new_refresh_token")) - }) - mock.Expect(2).ReturnError(fmt.Errorf("broken pipe")) - - client = mock - err := ForwardRequest(&ctx, nil) - - assert.Error(t, err) - assert.Equal(t, fasthttp.MethodGet, string(mock.GetRequest().Header.Method())) - assert.Equal(t, "Bearer access_token", string(mock.GetRequestAt(0).Header.Peek(headers.Authorization))) - assert.Equal(t, fasthttp.MethodGet, string(mock.GetRequestAt(2).Header.Method())) - assert.Equal(t, "Bearer new_access_token", string(mock.GetRequestAt(2).Header.Peek(headers.Authorization))) -} - -func TestForwardRequestError(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - ctx := fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod(fasthttp.MethodGet) - - mock := &MockClient{} - mock.ReturnError(fmt.Errorf("broken pipe")) - mock.MockResponse(func(resp *fasthttp.Response) {}) - - client = mock - err := ForwardRequest(&ctx, nil) - - assert.Error(t, err) -} - -func TestForwardResponse(t *testing.T) { - ctx := fasthttp.RequestCtx{} - - resp := fasthttp.Response{} - resp.SetStatusCode(fasthttp.StatusUnauthorized) - resp.SetBody([]byte("not allowed\n\r")) - resp.Header.Set(headers.AccessControlAllowOrigin, "test.com") - resp.Header.Set(headers.AccessControlMaxAge, "3600") - resp.Header.Set(headers.AccessControlAllowMethods, "GET,POST") - resp.Header.Set(headers.AccessControlAllowHeaders, "Content-Type,Accept-Language") - resp.Header.Set(headers.ContentType, "text/plain") - resp.Header.Set(headers.RetryAfter, "200") - resp.Header.Set(headers.Vary, "Content-Type,X-Rate-Limit") - resp.Header.Set(headers.XRateLimit, "123") - resp.Header.Set(headers.XRateLimitRemaining, "2") - - err := ForwardResponse(&ctx, &resp) - - assert.NoError(t, err) - - assert.Equal(t, "test.com", string(ctx.Response.Header.Peek(headers.AccessControlAllowOrigin))) - assert.Equal(t, "true", string(ctx.Response.Header.Peek(headers.AccessControlAllowCredentials))) - assert.Equal(t, "GET,POST", string(ctx.Response.Header.Peek(headers.AccessControlAllowMethods))) - assert.Equal(t, "Content-Type,Accept-Language", string(ctx.Response.Header.Peek(headers.AccessControlAllowHeaders))) - assert.Equal(t, "3600", string(ctx.Response.Header.Peek(headers.AccessControlMaxAge))) - assert.Equal(t, "text/plain", string(ctx.Response.Header.Peek(headers.ContentType))) - assert.Equal(t, "200", string(ctx.Response.Header.Peek(headers.RetryAfter))) - assert.Equal(t, "Content-Type,X-Rate-Limit", string(ctx.Response.Header.Peek(headers.Vary))) - assert.Equal(t, "123", string(ctx.Response.Header.Peek(headers.XRateLimit))) - assert.Equal(t, "2", string(ctx.Response.Header.Peek(headers.XRateLimitRemaining))) -} diff --git a/router/api/client/healthcheck_test.go b/router/api/client/healthcheck_test.go deleted file mode 100644 index b3e62c7..0000000 --- a/router/api/client/healthcheck_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package client - -import ( - "fmt" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/valyala/fasthttp" - - "github.com/cash-track/gateway/config" - "github.com/cash-track/gateway/headers" -) - -const endpoint = "http://api.test.com" - -func TestHealthcheckOk(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - }) - - client = mock - err := Healthcheck() - - assert.NoError(t, err) - assert.NotNil(t, mock.GetRequest()) - assert.Equal(t, fasthttp.MethodGet, string(mock.GetRequest().Header.Method())) - assert.Equal(t, fmt.Sprintf("%s%s", endpoint, healthcheckURI), mock.GetRequest().URI().String()) - assert.Equal(t, string(headers.ContentTypeJson), string(mock.GetRequest().Header.ContentType())) - assert.Equal(t, string(headers.ContentTypeJson), string(mock.GetRequest().Header.Peek(headers.Accept))) -} - -func TestHealthcheckFail(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusInternalServerError) - }) - - client = mock - err := Healthcheck() - - assert.Error(t, err) - assert.NotNil(t, mock.GetRequest()) -} - -func TestHealthcheckError(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - mock := &MockClient{} - mock.ReturnError(fmt.Errorf("connection reset by peer")) - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - }) - - client = mock - err := Healthcheck() - - assert.Error(t, err) - assert.NotNil(t, mock.GetRequest()) -} diff --git a/router/api/client/refresh_token_test.go b/router/api/client/refresh_token_test.go deleted file mode 100644 index 14310a6..0000000 --- a/router/api/client/refresh_token_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package client - -import ( - "fmt" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/valyala/fasthttp" - - "github.com/cash-track/gateway/config" - "github.com/cash-track/gateway/headers" - "github.com/cash-track/gateway/headers/cookie" -) - -func TestRefreshTokenOk(t *testing.T) { - const ( - oldAccessToken = "test_old_access_token" - oldRefreshToken = "test_old_refresh_token" - newAccessToken = "test_new_access_token" - newRefreshToken = "test_new_refresh_token" - ) - - config.Global.ApiURI, _ = url.Parse(endpoint) - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - resp.SetBodyString(fmt.Sprintf(`{"accessToken":"%s","refreshToken":"%s"}`, newAccessToken, newRefreshToken)) - }) - - auth := cookie.Auth{ - RefreshToken: oldRefreshToken, - AccessToken: oldAccessToken, - } - - client = mock - newAuth, err := refreshToken(auth) - - assert.NoError(t, err) - assert.NotNil(t, mock.GetRequest()) - assert.Equal(t, fasthttp.MethodPost, string(mock.GetRequest().Header.Method())) - assert.Equal(t, fmt.Sprintf("%s%s", endpoint, refreshURI), mock.GetRequest().URI().String()) - assert.Equal(t, string(headers.ContentTypeJson), string(mock.GetRequest().Header.ContentType())) - assert.Equal(t, string(headers.ContentTypeJson), string(mock.GetRequest().Header.Peek(headers.Accept))) - assert.Equal(t, fmt.Sprintf("Bearer %s", oldRefreshToken), string(mock.GetRequest().Header.Peek(headers.Authorization))) - assert.Equal(t, fmt.Sprintf(`{"accessToken":"%s"}`, oldAccessToken), string(mock.GetRequest().Body())) - - assert.NotEmpty(t, newAuth) - assert.Equal(t, newAccessToken, newAuth.AccessToken) - assert.Equal(t, newRefreshToken, newAuth.RefreshToken) -} - -func TestRefreshTokenFail(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusInternalServerError) - resp.SetBodyString(`{"error":"user deleted"}`) - }) - - auth := cookie.Auth{} - - client = mock - newAuth, err := refreshToken(auth) - - assert.Error(t, err) - assert.Empty(t, newAuth.AccessToken) - assert.Empty(t, newAuth.RefreshToken) -} - -func TestRefreshTokenError(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - }) - mock.ReturnError(fmt.Errorf("context cancelled")) - - auth := cookie.Auth{} - - client = mock - newAuth, err := refreshToken(auth) - - assert.Error(t, err) - assert.Empty(t, newAuth.AccessToken) - assert.Empty(t, newAuth.RefreshToken) -} - -func TestRefreshTokenErrorBadResponse(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusOK) - resp.SetBodyString("{") - }) - - auth := cookie.Auth{} - - client = mock - newAuth, err := refreshToken(auth) - - assert.Error(t, err) - assert.Empty(t, newAuth.AccessToken) - assert.Empty(t, newAuth.RefreshToken) -} - -func TestRefreshTokenErrorLoggedOff(t *testing.T) { - config.Global.ApiURI, _ = url.Parse(endpoint) - - mock := &MockClient{} - mock.MockResponse(func(resp *fasthttp.Response) { - resp.SetStatusCode(fasthttp.StatusUnauthorized) - resp.SetBodyString(`{"message":"refresh token expired"}`) - }) - - auth := cookie.Auth{} - - client = mock - newAuth, err := refreshToken(auth) - - assert.NoError(t, err) - assert.Empty(t, newAuth.AccessToken) - assert.Empty(t, newAuth.RefreshToken) -} diff --git a/router/api/handler.go b/router/api/handler.go index 0a4a546..f974142 100644 --- a/router/api/handler.go +++ b/router/api/handler.go @@ -9,9 +9,8 @@ import ( "github.com/cash-track/gateway/captcha" "github.com/cash-track/gateway/config" "github.com/cash-track/gateway/headers/cookie" - "github.com/cash-track/gateway/http" - "github.com/cash-track/gateway/router/api/client" "github.com/cash-track/gateway/router/response" + "github.com/cash-track/gateway/service/api" ) var allowedMethods = map[string]bool{ @@ -23,10 +22,29 @@ var allowedMethods = map[string]bool{ fasthttp.MethodOptions: true, } -func AuthSetHandler(ctx *fasthttp.RequestCtx) { - reCaptcha := captcha.NewGoogleReCaptchaProvider(http.NewFastHttpClient(), config.Global) +type Handler interface { + AuthSetHandler(ctx *fasthttp.RequestCtx) + AuthResetHandler(ctx *fasthttp.RequestCtx) + FullForwardedHandler(ctx *fasthttp.RequestCtx) + Healthcheck() error +} + +type HttpHandler struct { + config config.Config + captcha captcha.Provider + service api.Service +} + +func NewHttp(config config.Config, service api.Service, captcha captcha.Provider) *HttpHandler { + return &HttpHandler{ + config: config, + captcha: captcha, + service: service, + } +} - if ok, err := reCaptcha.Verify(ctx); err != nil || !ok { +func (h *HttpHandler) AuthSetHandler(ctx *fasthttp.RequestCtx) { + if ok, err := h.captcha.Verify(ctx); err != nil || !ok { if err != nil { response.NewCaptchaErrorResponse(err).Write(ctx) return @@ -36,26 +54,26 @@ func AuthSetHandler(ctx *fasthttp.RequestCtx) { } } - FullForwardedHandler(ctx) + h.FullForwardedHandler(ctx) - if err := Login(ctx); err != nil { + if err := h.Login(ctx); err != nil { response.ByError(err).Write(ctx) } } -func AuthResetHandler(ctx *fasthttp.RequestCtx) { +func (h *HttpHandler) AuthResetHandler(ctx *fasthttp.RequestCtx) { auth := cookie.ReadAuthCookie(ctx) - FullForwardedHandlerWithBody(ctx, cookie.Auth{ + h.FullForwardedHandlerWithBody(ctx, cookie.Auth{ RefreshToken: auth.RefreshToken, }) - if err := Logout(ctx); err != nil { + if err := h.Logout(ctx); err != nil { response.ByError(err).Write(ctx) } } -func FullForwardedHandler(ctx *fasthttp.RequestCtx) { +func (h *HttpHandler) FullForwardedHandler(ctx *fasthttp.RequestCtx) { if _, ok := allowedMethods[string(ctx.Request.Header.Method())]; !ok { response.ByErrorAndStatus( fmt.Errorf("request method %s is not allowed", ctx.Request.Header.Method()), @@ -64,14 +82,14 @@ func FullForwardedHandler(ctx *fasthttp.RequestCtx) { return } - err := client.ForwardRequest(ctx, nil) + err := h.service.ForwardRequest(ctx, nil) if err != nil { response.ByErrorAndStatus(err, fasthttp.StatusBadGateway).Write(ctx) return } } -func FullForwardedHandlerWithBody(ctx *fasthttp.RequestCtx, body interface{}) { +func (h *HttpHandler) FullForwardedHandlerWithBody(ctx *fasthttp.RequestCtx, body interface{}) { if _, ok := allowedMethods[string(ctx.Request.Header.Method())]; !ok { response.ByErrorAndStatus( fmt.Errorf("request method %s is not allowed", ctx.Request.Header.Method()), @@ -86,8 +104,12 @@ func FullForwardedHandlerWithBody(ctx *fasthttp.RequestCtx, body interface{}) { return } - if err := client.ForwardRequest(ctx, b); err != nil { + if err := h.service.ForwardRequest(ctx, b); err != nil { response.ByErrorAndStatus(err, fasthttp.StatusBadGateway).Write(ctx) return } } + +func (h *HttpHandler) Healthcheck() error { + return h.service.Healthcheck() +} diff --git a/router/api/login.go b/router/api/login.go index 39b8729..69f27a0 100644 --- a/router/api/login.go +++ b/router/api/login.go @@ -9,7 +9,7 @@ import ( "github.com/cash-track/gateway/headers/cookie" ) -func Login(ctx *fasthttp.RequestCtx) error { +func (h *HttpHandler) Login(ctx *fasthttp.RequestCtx) error { if ctx.Response.StatusCode() != fasthttp.StatusOK { return nil } @@ -21,7 +21,7 @@ func Login(ctx *fasthttp.RequestCtx) error { auth.WriteCookie(ctx) - b, _ := newWebAppRedirect().ToJson() + b, _ := h.newWebAppRedirect().ToJson() ctx.Response.SetBody(b) diff --git a/router/api/login_test.go b/router/api/login_test.go index e3066b7..18c0693 100644 --- a/router/api/login_test.go +++ b/router/api/login_test.go @@ -5,19 +5,26 @@ import ( "github.com/stretchr/testify/assert" "github.com/valyala/fasthttp" + "go.uber.org/mock/gomock" "github.com/cash-track/gateway/config" "github.com/cash-track/gateway/headers/cookie" + "github.com/cash-track/gateway/mocks" ) func TestLogin(t *testing.T) { - config.Global.WebAppUrl = "https://home.com" + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{ + WebAppUrl: "https://home.com", + }, s, c) ctx := fasthttp.RequestCtx{} ctx.Response.SetStatusCode(fasthttp.StatusOK) ctx.Response.SetBodyString(`{"accessToken":"new_access_token","refreshToken":"new_refresh_token"}`) - err := Login(&ctx) + err := h.Login(&ctx) assert.NoError(t, err) assert.Equal(t, `{"redirectUrl":"https://home.com"}`, string(ctx.Response.Body())) @@ -26,11 +33,18 @@ func TestLogin(t *testing.T) { } func TestLoginBadStatus(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{ + WebAppUrl: "https://home.com", + }, s, c) + ctx := fasthttp.RequestCtx{} ctx.Response.SetStatusCode(fasthttp.StatusUnauthorized) ctx.Response.SetBodyString(`{"accessToken":"new_access_token","refreshToken":"new_refresh_token"}`) - err := Login(&ctx) + err := h.Login(&ctx) assert.NoError(t, err) assert.NotContains(t, string(ctx.Response.Header.PeekCookie(cookie.AccessTokenCookieName)), "new_access_token") @@ -38,11 +52,18 @@ func TestLoginBadStatus(t *testing.T) { } func TestLoginInvalidResponse(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{ + WebAppUrl: "https://home.com", + }, s, c) + ctx := fasthttp.RequestCtx{} ctx.Response.SetStatusCode(fasthttp.StatusOK) ctx.Response.SetBodyString(`{"accessToken":"new_access_token","refreshToken":"new_refresh_token`) - err := Login(&ctx) + err := h.Login(&ctx) assert.Error(t, err) assert.NotContains(t, string(ctx.Response.Header.PeekCookie(cookie.AccessTokenCookieName)), "new_access_token") diff --git a/router/api/logout.go b/router/api/logout.go index 25c9088..dbe8c1c 100644 --- a/router/api/logout.go +++ b/router/api/logout.go @@ -6,10 +6,10 @@ import ( "github.com/cash-track/gateway/headers/cookie" ) -func Logout(ctx *fasthttp.RequestCtx) error { +func (h *HttpHandler) Logout(ctx *fasthttp.RequestCtx) error { cookie.Auth{}.WriteCookie(ctx) - b, _ := newWebsiteRedirect().ToJson() + b, _ := h.newWebsiteRedirect().ToJson() ctx.Response.SetBody(b) diff --git a/router/api/logout_test.go b/router/api/logout_test.go index 9e4d9de..994485c 100644 --- a/router/api/logout_test.go +++ b/router/api/logout_test.go @@ -6,17 +6,24 @@ import ( "github.com/stretchr/testify/assert" "github.com/valyala/fasthttp" + "go.uber.org/mock/gomock" "github.com/cash-track/gateway/config" "github.com/cash-track/gateway/headers/cookie" + "github.com/cash-track/gateway/mocks" ) func TestLogout(t *testing.T) { - config.Global.WebsiteUrl = "https://test.com" + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{ + WebsiteUrl: "https://test.com", + }, s, c) ctx := fasthttp.RequestCtx{} - err := Logout(&ctx) + err := h.Logout(&ctx) assert.NoError(t, err) assert.Equal(t, `{"redirectUrl":"https://test.com"}`, string(ctx.Response.Body())) diff --git a/router/api/redirect_response.go b/router/api/redirect_response.go index 1b8a322..d719496 100644 --- a/router/api/redirect_response.go +++ b/router/api/redirect_response.go @@ -2,22 +2,12 @@ package api import ( "encoding/json" - - "github.com/cash-track/gateway/config" ) type redirectResponse struct { RedirectUrl string `json:"redirectUrl"` } -func newWebAppRedirect() *redirectResponse { - return newRedirectResponse(config.Global.WebAppUrl) -} - -func newWebsiteRedirect() *redirectResponse { - return newRedirectResponse(config.Global.WebsiteUrl) -} - func newRedirectResponse(url string) *redirectResponse { return &redirectResponse{RedirectUrl: url} } @@ -25,3 +15,11 @@ func newRedirectResponse(url string) *redirectResponse { func (r *redirectResponse) ToJson() ([]byte, error) { return json.Marshal(r) } + +func (h *HttpHandler) newWebAppRedirect() *redirectResponse { + return newRedirectResponse(h.config.WebAppUrl) +} + +func (h *HttpHandler) newWebsiteRedirect() *redirectResponse { + return newRedirectResponse(h.config.WebsiteUrl) +} diff --git a/router/healthcheck.go b/router/healthcheck.go index 2a9654d..1dd7a06 100644 --- a/router/healthcheck.go +++ b/router/healthcheck.go @@ -4,8 +4,6 @@ import ( "log" "github.com/valyala/fasthttp" - - api "github.com/cash-track/gateway/router/api/client" ) var ( @@ -21,7 +19,7 @@ func (r *Router) LiveHandler(ctx *fasthttp.RequestCtx) { // ReadyHandler check all dependency for service readiness func (r *Router) ReadyHandler(ctx *fasthttp.RequestCtx) { - if err := api.Healthcheck(); err != nil { + if err := r.api.Healthcheck(); err != nil { log.Printf("API not ready: %s", err.Error()) ctx.SetStatusCode(fasthttp.StatusInternalServerError) ctx.SetBody(bodyApiNok) diff --git a/router/healthcheck_test.go b/router/healthcheck_test.go index e294448..239e194 100644 --- a/router/healthcheck_test.go +++ b/router/healthcheck_test.go @@ -1,19 +1,53 @@ package router import ( + "fmt" "testing" - "github.com/cash-track/gateway/config" "github.com/stretchr/testify/assert" "github.com/valyala/fasthttp" + "go.uber.org/mock/gomock" + + "github.com/cash-track/gateway/mocks" ) func TestLiveHandler(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewApiHandlerMock(ctrl) + r := New(h) + ctx := fasthttp.RequestCtx{} - r := New(config.Config{}) r.LiveHandler(&ctx) assert.Equal(t, fasthttp.StatusOK, ctx.Response.StatusCode()) assert.Equal(t, "ok", string(ctx.Response.Body())) } + +func TestReadyHandler(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewApiHandlerMock(ctrl) + h.EXPECT().Healthcheck().Return(nil) + r := New(h) + + ctx := fasthttp.RequestCtx{} + + r.ReadyHandler(&ctx) + + assert.Equal(t, fasthttp.StatusOK, ctx.Response.StatusCode()) + assert.Equal(t, "ok", string(ctx.Response.Body())) +} + +func TestReadyHandlerFail(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewApiHandlerMock(ctrl) + h.EXPECT().Healthcheck().Return(fmt.Errorf("context cancelled")) + r := New(h) + + ctx := fasthttp.RequestCtx{} + + r.ReadyHandler(&ctx) + + assert.Equal(t, fasthttp.StatusInternalServerError, ctx.Response.StatusCode()) + assert.Equal(t, "[api] nok", string(ctx.Response.Body())) +} diff --git a/router/router.go b/router/router.go index 5b7da39..60be8f3 100644 --- a/router/router.go +++ b/router/router.go @@ -3,19 +3,18 @@ package router import ( "github.com/fasthttp/router" - "github.com/cash-track/gateway/config" "github.com/cash-track/gateway/router/api" ) type Router struct { *router.Router - config config.Config + api api.Handler } -func New(config config.Config) *Router { +func New(api api.Handler) *Router { r := &Router{ Router: router.New(), - config: config, + api: api, } r.register() return r @@ -25,9 +24,9 @@ func (r *Router) register() { r.ANY("/live", r.LiveHandler) r.ANY("/ready", r.ReadyHandler) - r.POST("/api/auth/login", api.AuthSetHandler) - r.POST("/api/auth/register", api.AuthSetHandler) - r.POST("/api/auth/provider/google", api.AuthSetHandler) - r.POST("/api/auth/logout", api.AuthResetHandler) - r.ANY("/api/{path:*}", api.FullForwardedHandler) + r.POST("/api/auth/login", r.api.AuthSetHandler) + r.POST("/api/auth/register", r.api.AuthSetHandler) + r.POST("/api/auth/provider/google", r.api.AuthSetHandler) + r.POST("/api/auth/logout", r.api.AuthResetHandler) + r.ANY("/api/{path:*}", r.api.FullForwardedHandler) } diff --git a/router/router_test.go b/router/router_test.go index 6d0c3db..60c31d1 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -3,12 +3,16 @@ package router import ( "testing" - "github.com/cash-track/gateway/config" "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/cash-track/gateway/mocks" ) func TestNew(t *testing.T) { - r := New(config.Config{}) + ctrl := gomock.NewController(t) + h := mocks.NewApiHandlerMock(ctrl) + r := New(h) l := r.List() diff --git a/router/api/client/forward.go b/service/api/forward.go similarity index 80% rename from router/api/client/forward.go rename to service/api/forward.go index 19887bf..a5cfd11 100644 --- a/router/api/client/forward.go +++ b/service/api/forward.go @@ -1,4 +1,4 @@ -package client +package api import ( "bytes" @@ -12,7 +12,7 @@ import ( "github.com/cash-track/gateway/logger" ) -func ForwardRequest(ctx *fasthttp.RequestCtx, body []byte) error { +func (s *HttpService) ForwardRequest(ctx *fasthttp.RequestCtx, body []byte) error { // prepare req based on incoming ctx.Request req := fasthttp.AcquireRequest() defer func() { @@ -22,7 +22,7 @@ func ForwardRequest(ctx *fasthttp.RequestCtx, body []byte) error { remoteIp := headers.GetClientIPFromContext(ctx) req.Header.SetMethodBytes(bytes.Clone(ctx.Request.Header.Method())) - copyRequestURI(ctx.Request.URI(), req.URI()) + s.copyRequestURI(ctx.Request.URI(), req.URI()) req.Header.SetContentTypeBytes(headers.ContentTypeJson) req.Header.SetBytesV(headers.Accept, headers.ContentTypeJson) @@ -55,7 +55,7 @@ func ForwardRequest(ctx *fasthttp.RequestCtx, body []byte) error { } } - logger.DebugRequest(req, Service) + logger.DebugRequest(req, ServiceId) // execute request resp := fasthttp.AcquireResponse() @@ -63,19 +63,19 @@ func ForwardRequest(ctx *fasthttp.RequestCtx, body []byte) error { fasthttp.ReleaseResponse(resp) }() - if err := client.Do(req, resp); err != nil { + if err := s.http.Do(req, resp); err != nil { return fmt.Errorf("API request error: %w", err) } - logger.DebugResponse(resp, Service) - logger.FullForwarded(ctx, req, resp, Service) + logger.DebugResponse(resp, ServiceId) + logger.FullForwarded(ctx, req, resp, ServiceId) if !auth.IsLogged() || !auth.CanRefresh() || resp.StatusCode() != fasthttp.StatusUnauthorized { - return ForwardResponse(ctx, resp) + return forwardResponse(ctx, resp) } // perform refresh token - newAuth, err := refreshToken(auth) + newAuth, err := s.refreshToken(auth) if err != nil { log.Printf("[%s] refresh token attempt: %s", remoteIp, err.Error()) } @@ -84,19 +84,19 @@ func ForwardRequest(ctx *fasthttp.RequestCtx, body []byte) error { headers.WriteBearerToken(req, newAuth.AccessToken) // execute request 2nd attempt - if err := client.Do(req, resp); err != nil { + if err := s.http.Do(req, resp); err != nil { return fmt.Errorf("API request with fresh token error: %w", err) } - logger.DebugResponse(resp, Service) + logger.DebugResponse(resp, ServiceId) } newAuth.WriteCookie(ctx) - return ForwardResponse(ctx, resp) + return forwardResponse(ctx, resp) } -func ForwardResponse(ctx *fasthttp.RequestCtx, resp *fasthttp.Response) error { +func forwardResponse(ctx *fasthttp.RequestCtx, resp *fasthttp.Response) error { ctx.SetStatusCode(resp.StatusCode()) ctx.SetBody(bytes.Clone(resp.Body())) diff --git a/service/api/forward_test.go b/service/api/forward_test.go new file mode 100644 index 0000000..85d31fa --- /dev/null +++ b/service/api/forward_test.go @@ -0,0 +1,261 @@ +package api + +import ( + "fmt" + "net" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" + "go.uber.org/mock/gomock" + + "github.com/cash-track/gateway/config" + "github.com/cash-track/gateway/headers" + "github.com/cash-track/gateway/headers/cookie" + "github.com/cash-track/gateway/mocks" +) + +func TestFullForwardRequestWithAuth(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodPatch, string(req.Header.Method())) + assert.Equal(t, fmt.Sprintf("%s%s", endpoint, "/auth/profile"), req.URI().String()) + assert.Equal(t, string(headers.ContentTypeJson), string(req.Header.ContentType())) + assert.Equal(t, string(headers.ContentTypeJson), string(req.Header.Peek(headers.Accept))) + assert.Equal(t, "10.0.0.1", string(req.Header.Peek(headers.XForwardedFor))) + assert.Equal(t, "Bearer access_token", string(req.Header.Peek(headers.Authorization))) + assert.Equal(t, `{"status":"ok"}`, string(req.Body())) + + return nil + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + uri := &fasthttp.URI{} + _ = uri.Parse(nil, []byte("https://gateway.test.com/api/auth/profile")) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPatch) + ctx.Request.Header.SetCookie(cookie.AccessTokenCookieName, "access_token") + ctx.Request.SetURI(uri) + ctx.Request.SetBodyString(`{"status":"ok"}`) + ctx.SetRemoteAddr(&net.TCPAddr{IP: []byte{0xA, 0x0, 0x0, 0x1}}) + + err := s.ForwardRequest(&ctx, nil) + + assert.NoError(t, err) +} + +func TestForwardRequestWithBodyOverride(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodPost, string(req.Header.Method())) + assert.Equal(t, `{"status":"false"}`, string(req.Body())) + + return nil + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.SetBodyString(`{"status":"ok"}`) + + err := s.ForwardRequest(&ctx, []byte(`{"status":"false"}`)) + + assert.NoError(t, err) +} + +func TestForwardRequestWithAuthRefresh(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusUnauthorized) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodGet, string(req.Header.Method())) + assert.Equal(t, "Bearer access_token", string(req.Header.Peek(headers.Authorization))) + + return nil + }) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + resp.SetBodyString(fmt.Sprintf(`{"accessToken":"%s","refreshToken":"%s"}`, "new_access_token", "new_refresh_token")) + return nil + }) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodGet, string(req.Header.Method())) + assert.Equal(t, "Bearer new_access_token", string(req.Header.Peek(headers.Authorization))) + + return nil + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodGet) + ctx.Request.Header.SetCookie(cookie.AccessTokenCookieName, "access_token") + ctx.Request.Header.SetCookie(cookie.RefreshTokenCookieName, "refresh_token") + + err := s.ForwardRequest(&ctx, nil) + + assert.NoError(t, err) + assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.AccessTokenCookieName)), "new_access_token") + assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.RefreshTokenCookieName)), "new_refresh_token") +} + +func TestForwardRequestWithAuthRefreshFailLogout(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusUnauthorized) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodGet, string(req.Header.Method())) + assert.Equal(t, "Bearer access_token", string(req.Header.Peek(headers.Authorization))) + + return nil + }) + h.EXPECT().Do(gomock.Any(), gomock.Any()).Return(fmt.Errorf("broken pipe")) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodGet) + ctx.Request.Header.SetCookie(cookie.AccessTokenCookieName, "access_token") + ctx.Request.Header.SetCookie(cookie.RefreshTokenCookieName, "refresh_token") + + err := s.ForwardRequest(&ctx, nil) + + assert.NoError(t, err) + + assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.AccessTokenCookieName)), fmt.Sprintf("%s=;", cookie.AccessTokenCookieName)) + assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.RefreshTokenCookieName)), fmt.Sprintf("%s=;", cookie.RefreshTokenCookieName)) +} + +func TestForwardRequestWithAuthRefreshSecondFail(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusUnauthorized) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodGet, string(req.Header.Method())) + assert.Equal(t, "Bearer access_token", string(req.Header.Peek(headers.Authorization))) + + return nil + }) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + resp.SetBodyString(fmt.Sprintf(`{"accessToken":"%s","refreshToken":"%s"}`, "new_access_token", "new_refresh_token")) + + return nil + }) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodGet, string(req.Header.Method())) + assert.Equal(t, "Bearer new_access_token", string(req.Header.Peek(headers.Authorization))) + + return fmt.Errorf("broken pipe") + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodGet) + ctx.Request.Header.SetCookie(cookie.AccessTokenCookieName, "access_token") + ctx.Request.Header.SetCookie(cookie.RefreshTokenCookieName, "refresh_token") + + err := s.ForwardRequest(&ctx, nil) + + assert.Error(t, err) +} + +func TestForwardRequestError(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).Return(fmt.Errorf("broken pipe")) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodGet) + + err := s.ForwardRequest(&ctx, nil) + + assert.Error(t, err) +} + +func TestForwardResponse(t *testing.T) { + ctx := fasthttp.RequestCtx{} + + resp := fasthttp.Response{} + resp.SetStatusCode(fasthttp.StatusUnauthorized) + resp.SetBody([]byte("not allowed\n\r")) + resp.Header.Set(headers.AccessControlAllowOrigin, "test.com") + resp.Header.Set(headers.AccessControlMaxAge, "3600") + resp.Header.Set(headers.AccessControlAllowMethods, "GET,POST") + resp.Header.Set(headers.AccessControlAllowHeaders, "Content-Type,Accept-Language") + resp.Header.Set(headers.ContentType, "text/plain") + resp.Header.Set(headers.RetryAfter, "200") + resp.Header.Set(headers.Vary, "Content-Type,X-Rate-Limit") + resp.Header.Set(headers.XRateLimit, "123") + resp.Header.Set(headers.XRateLimitRemaining, "2") + + err := forwardResponse(&ctx, &resp) + + assert.NoError(t, err) + + assert.Equal(t, "test.com", string(ctx.Response.Header.Peek(headers.AccessControlAllowOrigin))) + assert.Equal(t, "true", string(ctx.Response.Header.Peek(headers.AccessControlAllowCredentials))) + assert.Equal(t, "GET,POST", string(ctx.Response.Header.Peek(headers.AccessControlAllowMethods))) + assert.Equal(t, "Content-Type,Accept-Language", string(ctx.Response.Header.Peek(headers.AccessControlAllowHeaders))) + assert.Equal(t, "3600", string(ctx.Response.Header.Peek(headers.AccessControlMaxAge))) + assert.Equal(t, "text/plain", string(ctx.Response.Header.Peek(headers.ContentType))) + assert.Equal(t, "200", string(ctx.Response.Header.Peek(headers.RetryAfter))) + assert.Equal(t, "Content-Type,X-Rate-Limit", string(ctx.Response.Header.Peek(headers.Vary))) + assert.Equal(t, "123", string(ctx.Response.Header.Peek(headers.XRateLimit))) + assert.Equal(t, "2", string(ctx.Response.Header.Peek(headers.XRateLimitRemaining))) +} diff --git a/router/api/client/healthcheck.go b/service/api/healthcheck.go similarity index 79% rename from router/api/client/healthcheck.go rename to service/api/healthcheck.go index 088bd9e..28d0fff 100644 --- a/router/api/client/healthcheck.go +++ b/service/api/healthcheck.go @@ -1,4 +1,4 @@ -package client +package api import ( "fmt" @@ -11,18 +11,18 @@ import ( var healthcheckURI = []byte("/healthcheck") -func Healthcheck() error { +func (s *HttpService) Healthcheck() error { req := fasthttp.AcquireRequest() defer func() { fasthttp.ReleaseRequest(req) }() req.Header.SetMethod(fasthttp.MethodGet) - setRequestURI(req.URI(), healthcheckURI) + s.setRequestURI(req.URI(), healthcheckURI) req.Header.SetContentTypeBytes(headers.ContentTypeJson) req.Header.SetBytesV(headers.Accept, headers.ContentTypeJson) - logger.DebugRequest(req, Service) + logger.DebugRequest(req, ServiceId) // execute request resp := fasthttp.AcquireResponse() @@ -30,12 +30,12 @@ func Healthcheck() error { fasthttp.ReleaseResponse(resp) }() - err := client.Do(req, resp) + err := s.http.Do(req, resp) if err != nil { return fmt.Errorf("healthckeck API request error: %w", err) } - logger.DebugResponse(resp, Service) + logger.DebugResponse(resp, ServiceId) if resp.StatusCode() != fasthttp.StatusOK { return fmt.Errorf("healthckeck failed [%d], body: %s", resp.StatusCode(), resp.Body()) diff --git a/service/api/healthcheck_test.go b/service/api/healthcheck_test.go new file mode 100644 index 0000000..f99f643 --- /dev/null +++ b/service/api/healthcheck_test.go @@ -0,0 +1,79 @@ +package api + +import ( + "fmt" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" + "go.uber.org/mock/gomock" + + "github.com/cash-track/gateway/config" + "github.com/cash-track/gateway/headers" + "github.com/cash-track/gateway/mocks" +) + +const endpoint = "http://api.test.com" + +func TestHealthcheckOk(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodGet, string(req.Header.Method())) + assert.Equal(t, fmt.Sprintf("%s%s", endpoint, healthcheckURI), req.URI().String()) + assert.Equal(t, string(headers.ContentTypeJson), string(req.Header.ContentType())) + assert.Equal(t, string(headers.ContentTypeJson), string(req.Header.Peek(headers.Accept))) + + return nil + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + err := s.Healthcheck() + + assert.NoError(t, err) +} + +func TestHealthcheckFail(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusInternalServerError) + assert.NotNil(t, req) + return nil + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + err := s.Healthcheck() + + assert.Error(t, err) +} + +func TestHealthcheckError(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).Return(fmt.Errorf("connection reset by peer")) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + err := s.Healthcheck() + + assert.Error(t, err) +} diff --git a/router/api/client/refresh_token.go b/service/api/refresh_token.go similarity index 84% rename from router/api/client/refresh_token.go rename to service/api/refresh_token.go index a8d87c2..59c5c57 100644 --- a/router/api/client/refresh_token.go +++ b/service/api/refresh_token.go @@ -1,4 +1,4 @@ -package client +package api import ( "encoding/json" @@ -13,14 +13,14 @@ import ( var refreshURI = []byte("/auth/refresh") -func refreshToken(auth cookie.Auth) (cookie.Auth, error) { +func (s *HttpService) refreshToken(auth cookie.Auth) (cookie.Auth, error) { req := fasthttp.AcquireRequest() defer func() { fasthttp.ReleaseRequest(req) }() req.Header.SetMethod(fasthttp.MethodPost) - setRequestURI(req.URI(), refreshURI) + s.setRequestURI(req.URI(), refreshURI) req.Header.SetContentTypeBytes(headers.ContentTypeJson) req.Header.SetBytesV(headers.Accept, headers.ContentTypeJson) headers.WriteBearerToken(req, auth.RefreshToken) @@ -28,7 +28,7 @@ func refreshToken(auth cookie.Auth) (cookie.Auth, error) { data, _ := json.Marshal(cookie.Auth{AccessToken: auth.AccessToken}) req.SetBody(data) - logger.DebugRequest(req, Service) + logger.DebugRequest(req, ServiceId) // execute request resp := fasthttp.AcquireResponse() @@ -37,12 +37,12 @@ func refreshToken(auth cookie.Auth) (cookie.Auth, error) { }() newAuth := cookie.Auth{} - err := client.Do(req, resp) + err := s.http.Do(req, resp) if err != nil { return newAuth, fmt.Errorf("refresh token API request error: %w", err) } - logger.DebugResponse(resp, Service) + logger.DebugResponse(resp, ServiceId) if resp.StatusCode() == fasthttp.StatusUnauthorized { // re-login required diff --git a/service/api/refresh_token_test.go b/service/api/refresh_token_test.go new file mode 100644 index 0000000..47c73b9 --- /dev/null +++ b/service/api/refresh_token_test.go @@ -0,0 +1,160 @@ +package api + +import ( + "fmt" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" + "go.uber.org/mock/gomock" + + "github.com/cash-track/gateway/config" + "github.com/cash-track/gateway/headers" + "github.com/cash-track/gateway/headers/cookie" + "github.com/cash-track/gateway/mocks" +) + +func TestRefreshTokenOk(t *testing.T) { + const ( + oldAccessToken = "test_old_access_token" + oldRefreshToken = "test_old_refresh_token" + newAccessToken = "test_new_access_token" + newRefreshToken = "test_new_refresh_token" + ) + + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + resp.SetBodyString(fmt.Sprintf(`{"accessToken":"%s","refreshToken":"%s"}`, newAccessToken, newRefreshToken)) + + assert.NotNil(t, req) + assert.Equal(t, fasthttp.MethodPost, string(req.Header.Method())) + assert.Equal(t, fmt.Sprintf("%s%s", endpoint, refreshURI), req.URI().String()) + assert.Equal(t, string(headers.ContentTypeJson), string(req.Header.ContentType())) + assert.Equal(t, string(headers.ContentTypeJson), string(req.Header.Peek(headers.Accept))) + assert.Equal(t, fmt.Sprintf("Bearer %s", oldRefreshToken), string(req.Header.Peek(headers.Authorization))) + assert.Equal(t, fmt.Sprintf(`{"accessToken":"%s"}`, oldAccessToken), string(req.Body())) + + return nil + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + auth := cookie.Auth{ + RefreshToken: oldRefreshToken, + AccessToken: oldAccessToken, + } + + newAuth, err := s.refreshToken(auth) + + assert.NoError(t, err) + assert.NotEmpty(t, newAuth) + assert.Equal(t, newAccessToken, newAuth.AccessToken) + assert.Equal(t, newRefreshToken, newAuth.RefreshToken) +} + +func TestRefreshTokenFail(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusInternalServerError) + resp.SetBodyString(`{"error":"user deleted"}`) + assert.NotNil(t, req) + return nil + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + auth := cookie.Auth{} + + newAuth, err := s.refreshToken(auth) + + assert.Error(t, err) + assert.Empty(t, newAuth.AccessToken) + assert.Empty(t, newAuth.RefreshToken) +} + +func TestRefreshTokenError(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).Return(fmt.Errorf("context cancelled")) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + auth := cookie.Auth{} + + newAuth, err := s.refreshToken(auth) + + assert.Error(t, err) + assert.Empty(t, newAuth.AccessToken) + assert.Empty(t, newAuth.RefreshToken) +} + +func TestRefreshTokenErrorBadResponse(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusOK) + resp.SetBodyString("{") + assert.NotNil(t, req) + return nil + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + auth := cookie.Auth{} + + newAuth, err := s.refreshToken(auth) + + assert.Error(t, err) + assert.Empty(t, newAuth.AccessToken) + assert.Empty(t, newAuth.RefreshToken) +} + +func TestRefreshTokenErrorLoggedOff(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + h.EXPECT().Do(gomock.Any(), gomock.Any()).DoAndReturn(func(req *fasthttp.Request, resp *fasthttp.Response) error { + resp.SetStatusCode(fasthttp.StatusUnauthorized) + resp.SetBodyString(`{"message":"refresh token expired"}`) + assert.NotNil(t, req) + return nil + }) + + apiUrl, _ := url.Parse(endpoint) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + auth := cookie.Auth{} + + newAuth, err := s.refreshToken(auth) + + assert.NoError(t, err) + assert.Empty(t, newAuth.AccessToken) + assert.Empty(t, newAuth.RefreshToken) +} diff --git a/service/api/service.go b/service/api/service.go new file mode 100644 index 0000000..f92de29 --- /dev/null +++ b/service/api/service.go @@ -0,0 +1,56 @@ +package api + +import ( + "strings" + "time" + + "github.com/valyala/fasthttp" + + "github.com/cash-track/gateway/config" + "github.com/cash-track/gateway/http" +) + +const ( + ServiceId = "API" + httpReadTimeout = 5 * time.Second + httpWriteTimeout = 5 * time.Second +) + +var methodsWithBody = map[string]bool{ + fasthttp.MethodPost: true, + fasthttp.MethodPut: true, + fasthttp.MethodPatch: true, +} + +type Service interface { + ForwardRequest(ctx *fasthttp.RequestCtx, body []byte) error + Healthcheck() error +} + +type HttpService struct { + http http.Client + config config.Config +} + +func NewHttp(http http.Client, config config.Config) *HttpService { + http.WithReadTimeout(httpReadTimeout) + http.WithWriteTimeout(httpWriteTimeout) + + return &HttpService{ + http: http, + config: config, + } +} + +func (s *HttpService) setRequestURI(dest *fasthttp.URI, path []byte) { + _ = dest.Parse([]byte(s.config.ApiUrl), nil) + dest.SetScheme(s.config.ApiURI.Scheme) + dest.SetHost(s.config.ApiURI.Host) + dest.SetPathBytes(path) +} + +func (s *HttpService) copyRequestURI(src, dest *fasthttp.URI) { + path := strings.TrimPrefix(string(src.PathOriginal()), "/api") + s.setRequestURI(dest, []byte(path)) + dest.SetQueryStringBytes(src.QueryString()) +} diff --git a/service/api/service_test.go b/service/api/service_test.go new file mode 100644 index 0000000..d2c7bdd --- /dev/null +++ b/service/api/service_test.go @@ -0,0 +1,63 @@ +package api + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" + "go.uber.org/mock/gomock" + + "github.com/cash-track/gateway/config" + "github.com/cash-track/gateway/mocks" +) + +func TestNewClient(t *testing.T) { + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + + s := NewHttp(h, config.Config{}) + + assert.NotNil(t, s.http) +} + +func TestSetRequestURI(t *testing.T) { + apiUrl, _ := url.Parse("http://api.test.com") + + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + uri := fasthttp.URI{} + + s.setRequestURI(&uri, []byte("/users/create one")) + + assert.Equal(t, "http://api.test.com/users/create%20one", uri.String()) +} + +func TestCopyRequestURI(t *testing.T) { + apiUrl, _ := url.Parse("http://api.test.com") + + ctrl := gomock.NewController(t) + h := mocks.NewHttpClientMock(ctrl) + h.EXPECT().WithReadTimeout(gomock.Eq(httpReadTimeout)) + h.EXPECT().WithWriteTimeout(gomock.Eq(httpWriteTimeout)) + s := NewHttp(h, config.Config{ + ApiURI: apiUrl, + }) + + src := fasthttp.URI{} + src.SetPath("/api/users/create one") + src.SetQueryString("one=two%203") + dest := fasthttp.URI{} + + s.copyRequestURI(&src, &dest) + + assert.Equal(t, "http://api.test.com/users/create%20one?one=two%203", dest.String()) +} From e8308b0c0b0eff4a153a23411211c62707254561 Mon Sep 17 00:00:00 2001 From: vokomarov Date: Sat, 6 Jan 2024 12:17:04 +0200 Subject: [PATCH 4/5] Add api handler tests --- router/api/handler.go | 6 +- router/api/handler_test.go | 248 +++++++++++++++++++++++++++++++++++++ router/api/logout.go | 4 +- router/api/logout_test.go | 3 +- 4 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 router/api/handler_test.go diff --git a/router/api/handler.go b/router/api/handler.go index f974142..8d267d0 100644 --- a/router/api/handler.go +++ b/router/api/handler.go @@ -57,7 +57,7 @@ func (h *HttpHandler) AuthSetHandler(ctx *fasthttp.RequestCtx) { h.FullForwardedHandler(ctx) if err := h.Login(ctx); err != nil { - response.ByError(err).Write(ctx) + response.ByErrorAndStatus(err, fasthttp.StatusBadGateway).Write(ctx) } } @@ -68,9 +68,7 @@ func (h *HttpHandler) AuthResetHandler(ctx *fasthttp.RequestCtx) { RefreshToken: auth.RefreshToken, }) - if err := h.Logout(ctx); err != nil { - response.ByError(err).Write(ctx) - } + h.Logout(ctx) } func (h *HttpHandler) FullForwardedHandler(ctx *fasthttp.RequestCtx) { diff --git a/router/api/handler_test.go b/router/api/handler_test.go new file mode 100644 index 0000000..17c05d9 --- /dev/null +++ b/router/api/handler_test.go @@ -0,0 +1,248 @@ +package api + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" + "go.uber.org/mock/gomock" + + "github.com/cash-track/gateway/config" + "github.com/cash-track/gateway/headers/cookie" + "github.com/cash-track/gateway/mocks" +) + +func TestAuthSetHandler(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.Set("Test", "Value") + + c.EXPECT().Verify(gomock.Any()).Return(true, nil) + s.EXPECT().ForwardRequest(gomock.Any(), nil).DoAndReturn(func(ctx *fasthttp.RequestCtx, body []byte) error { + ctx.Response.SetStatusCode(fasthttp.StatusOK) + ctx.Response.SetBodyString(`{"accessToken":"new_access_token"}`) + return nil + }) + + h.AuthSetHandler(&ctx) + + assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.AccessTokenCookieName)), "new_access_token") +} + +func TestAuthSetHandlerCaptchaFail(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.Set("Test", "Value") + + c.EXPECT().Verify(gomock.Any()).Return(false, nil) + + h.AuthSetHandler(&ctx) + + assert.Equal(t, fasthttp.StatusBadRequest, ctx.Response.StatusCode()) +} + +func TestAuthSetHandlerCaptchaError(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.Set("Test", "Value") + + c.EXPECT().Verify(gomock.Any()).Return(false, fmt.Errorf("captcha api down")) + + h.AuthSetHandler(&ctx) + + assert.Equal(t, fasthttp.StatusInternalServerError, ctx.Response.StatusCode()) +} + +func TestAuthSetHandlerLoginError(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.Set("Test", "Value") + + c.EXPECT().Verify(gomock.Any()).Return(true, nil) + s.EXPECT().ForwardRequest(gomock.Any(), nil).DoAndReturn(func(ctx *fasthttp.RequestCtx, body []byte) error { + ctx.Response.SetStatusCode(fasthttp.StatusOK) + ctx.Response.SetBodyString(`{"accessToken":"new_access_token"`) + return nil + }) + + h.AuthSetHandler(&ctx) + + assert.Equal(t, fasthttp.StatusBadGateway, ctx.Response.StatusCode()) +} + +func TestAuthResetHandler(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.SetCookie(cookie.RefreshTokenCookieName, "refresh_token_test") + + body := []byte(`{"refreshToken":"refresh_token_test"}`) + + s.EXPECT().ForwardRequest(gomock.Any(), body).Return(nil) + + h.AuthResetHandler(&ctx) + + assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.AccessTokenCookieName)), fmt.Sprintf("%s=;", cookie.AccessTokenCookieName)) + assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.RefreshTokenCookieName)), fmt.Sprintf("%s=;", cookie.RefreshTokenCookieName)) +} + +func TestFullForwardedHandler(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.Set("Test", "Value") + + s.EXPECT().ForwardRequest(gomock.Any(), nil).DoAndReturn(func(ctx *fasthttp.RequestCtx, body []byte) error { + assert.Equal(t, fasthttp.MethodPost, string(ctx.Request.Header.Method())) + assert.Equal(t, "Value", string(ctx.Request.Header.Peek("Test"))) + return nil + }) + + h.FullForwardedHandler(&ctx) +} + +func TestFullForwardedHandlerError(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.Set("Test", "Value") + + s.EXPECT().ForwardRequest(gomock.Any(), nil).Return(fmt.Errorf("broken pipe")) + + h.FullForwardedHandler(&ctx) + + assert.Equal(t, fasthttp.StatusBadGateway, ctx.Response.StatusCode()) +} + +func TestFullForwardedHandlerRestrictedMethod(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodConnect) + ctx.Request.Header.Set("Test", "Value") + + h.FullForwardedHandler(&ctx) + + assert.Equal(t, fasthttp.StatusBadRequest, ctx.Response.StatusCode()) +} + +func TestFullForwardedHandlerWithBody(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.Set("Test", "Value") + + body := cookie.Auth{ + AccessToken: "123", + } + bodyJson := []byte(`{"accessToken":"123"}`) + + s.EXPECT().ForwardRequest(gomock.Any(), bodyJson).DoAndReturn(func(ctx *fasthttp.RequestCtx, body []byte) error { + assert.Equal(t, fasthttp.MethodPost, string(ctx.Request.Header.Method())) + assert.Equal(t, "Value", string(ctx.Request.Header.Peek("Test"))) + return nil + }) + + h.FullForwardedHandlerWithBody(&ctx, body) +} + +func TestFullForwardedHandlerWithBodyError(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.Set("Test", "Value") + + s.EXPECT().ForwardRequest(gomock.Any(), []byte(`{"test":"123"}`)).Return(fmt.Errorf("broken pipe")) + + h.FullForwardedHandlerWithBody(&ctx, map[string]string{"test": "123"}) + + assert.Equal(t, fasthttp.StatusBadGateway, ctx.Response.StatusCode()) +} + +func TestFullForwardedHandlerWithBodyJsonError(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodPost) + ctx.Request.Header.Set("Test", "Value") + + var i complex128 + h.FullForwardedHandlerWithBody(&ctx, i) + + assert.Equal(t, fasthttp.StatusInternalServerError, ctx.Response.StatusCode()) +} + +func TestFullForwardedHandlerWithBodyRestrictedMethod(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + ctx := fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fasthttp.MethodConnect) + ctx.Request.Header.Set("Test", "Value") + + h.FullForwardedHandlerWithBody(&ctx, nil) + + assert.Equal(t, fasthttp.StatusBadRequest, ctx.Response.StatusCode()) +} + +func TestHealthcheck(t *testing.T) { + ctrl := gomock.NewController(t) + s := mocks.NewApiServiceMock(ctrl) + c := mocks.NewCaptchaProviderMock(ctrl) + h := NewHttp(config.Config{}, s, c) + + s.EXPECT().Healthcheck().Return(nil) + + err := h.Healthcheck() + + assert.NoError(t, err) +} diff --git a/router/api/logout.go b/router/api/logout.go index dbe8c1c..d33ce5d 100644 --- a/router/api/logout.go +++ b/router/api/logout.go @@ -6,12 +6,10 @@ import ( "github.com/cash-track/gateway/headers/cookie" ) -func (h *HttpHandler) Logout(ctx *fasthttp.RequestCtx) error { +func (h *HttpHandler) Logout(ctx *fasthttp.RequestCtx) { cookie.Auth{}.WriteCookie(ctx) b, _ := h.newWebsiteRedirect().ToJson() ctx.Response.SetBody(b) - - return nil } diff --git a/router/api/logout_test.go b/router/api/logout_test.go index 994485c..3fad487 100644 --- a/router/api/logout_test.go +++ b/router/api/logout_test.go @@ -23,9 +23,8 @@ func TestLogout(t *testing.T) { ctx := fasthttp.RequestCtx{} - err := h.Logout(&ctx) + h.Logout(&ctx) - assert.NoError(t, err) assert.Equal(t, `{"redirectUrl":"https://test.com"}`, string(ctx.Response.Body())) assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.AccessTokenCookieName)), fmt.Sprintf("%s=;", cookie.AccessTokenCookieName)) assert.Contains(t, string(ctx.Response.Header.PeekCookie(cookie.RefreshTokenCookieName)), fmt.Sprintf("%s=;", cookie.RefreshTokenCookieName)) From d6f66d420076658cb30eb458a7cc0fdcd0075b4e Mon Sep 17 00:00:00 2001 From: vokomarov Date: Sat, 6 Jan 2024 12:23:19 +0200 Subject: [PATCH 5/5] Add http client tests --- http/client_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/http/client_test.go b/http/client_test.go index d8d2e46..de81acd 100644 --- a/http/client_test.go +++ b/http/client_test.go @@ -21,3 +21,12 @@ func TestWithReadTimeout(t *testing.T) { assert.Equal(t, 1*time.Second, client.ReadTimeout) } + +func TestWithWriteTimeout(t *testing.T) { + client := FastHttpClient{ + Client: &fasthttp.Client{}, + } + client.WithWriteTimeout(1 * time.Second) + + assert.Equal(t, 1*time.Second, client.WriteTimeout) +}