From 45159e059461cea40b153e4c6ef8eaf715389433 Mon Sep 17 00:00:00 2001 From: Evgeny Abramovich Date: Sun, 21 May 2023 11:10:08 -0300 Subject: [PATCH 1/9] Created draft --- internal/config/config.go | 19 +++--- internal/config/url_mapping.go | 1 + internal/handler/mocked_routes.go | 9 +-- internal/handler/static_routes.go | 46 +++++++------- internal/handler/uncors_handler.go | 62 +++++++++++++++---- .../handler/uncors_handler_internal_test.go | 27 ++++++++ internal/handler/uncors_handler_options.go | 18 ++---- internal/ui/mappings.go | 11 ++-- internal/ui/mappings_test.go | 30 +++------ main.go | 3 +- 10 files changed, 132 insertions(+), 94 deletions(-) create mode 100644 internal/handler/uncors_handler_internal_test.go diff --git a/internal/config/config.go b/internal/config/config.go index 97657cee..bc5d6625 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,17 +15,13 @@ const ( ) type UncorsConfig struct { - // Base config_test_data - HTTPPort int `mapstructure:"http-port" validate:"required"` - Mappings []URLMapping `mapstructure:"mappings" validate:"required"` - Proxy string `mapstructure:"proxy"` - Debug bool `mapstructure:"debug"` - // HTTPS config_test_data - HTTPSPort int `mapstructure:"https-port"` - CertFile string `mapstructure:"cert-file"` - KeyFile string `mapstructure:"key-file"` - // Mocks config_test_data - Mocks []Mock `mapstructure:"mocks"` + HTTPPort int `mapstructure:"http-port" validate:"required"` + Mappings []URLMapping `mapstructure:"mappings" validate:"required"` + Proxy string `mapstructure:"proxy"` + Debug bool `mapstructure:"debug"` + HTTPSPort int `mapstructure:"https-port"` + CertFile string `mapstructure:"cert-file"` + KeyFile string `mapstructure:"key-file"` } func (config *UncorsConfig) IsHTTPSEnabled() bool { @@ -44,7 +40,6 @@ func LoadConfiguration(viperInstance *viper.Viper, args []string) (*UncorsConfig configuration := &UncorsConfig{ Mappings: []URLMapping{}, - Mocks: []Mock{}, } if configPath := viperInstance.GetString("config"); len(configPath) > 0 { diff --git a/internal/config/url_mapping.go b/internal/config/url_mapping.go index bcf6aecc..c9fd1aec 100644 --- a/internal/config/url_mapping.go +++ b/internal/config/url_mapping.go @@ -11,6 +11,7 @@ type URLMapping struct { From string `mapstructure:"from"` To string `mapstructure:"to"` Statics StaticDirMappings `mapstructure:"statics"` + Mocks []Mock `mapstructure:"mocks"` } func (u URLMapping) Clone() URLMapping { diff --git a/internal/handler/mocked_routes.go b/internal/handler/mocked_routes.go index d02d41f1..74ff0c9b 100644 --- a/internal/handler/mocked_routes.go +++ b/internal/handler/mocked_routes.go @@ -2,14 +2,15 @@ package handler import ( "github.com/evg4b/uncors/internal/config" + "github.com/gorilla/mux" ) -func (m *UncorsRequestHandler) makeMockedRoutes() { +func (m *RequestHandler) makeMockedRoutes(router *mux.Router, mocks []config.Mock) { var defaultMocks []config.Mock - for _, mockDef := range m.mocks { + for _, mockDef := range mocks { if len(mockDef.Queries) > 0 || len(mockDef.Headers) > 0 || len(mockDef.Method) > 0 { - route := m.router.NewRoute() + route := router.NewRoute() setPath(route, mockDef.Path) setMethod(route, mockDef.Method) setQueries(route, mockDef.Queries) @@ -21,7 +22,7 @@ func (m *UncorsRequestHandler) makeMockedRoutes() { } for _, mockDef := range defaultMocks { - route := m.router.NewRoute() + route := router.NewRoute() setPath(route, mockDef.Path) route.Handler(m.createHandler(mockDef.Response)) } diff --git a/internal/handler/static_routes.go b/internal/handler/static_routes.go index c8767023..1208fe26 100644 --- a/internal/handler/static_routes.go +++ b/internal/handler/static_routes.go @@ -4,33 +4,31 @@ import ( "net/http" "strings" - "github.com/evg4b/uncors/internal/ui" - + "github.com/evg4b/uncors/internal/config" "github.com/evg4b/uncors/internal/handler/static" - + "github.com/evg4b/uncors/internal/ui" + "github.com/gorilla/mux" "github.com/spf13/afero" ) -func (m *UncorsRequestHandler) makeStaticRoutes(next http.Handler) { - for _, urlMapping := range m.mappings { - for _, staticDir := range urlMapping.Statics { - clearPath := strings.TrimSuffix(staticDir.Path, "/") - path := clearPath + "/" - - redirect := m.router.NewRoute() - redirect.Path(clearPath). - Handler(http.RedirectHandler(path, http.StatusTemporaryRedirect)) - - route := m.router.NewRoute() - handler := static.NewStaticMiddleware( - static.WithFileSystem(afero.NewBasePathFs(m.fs, staticDir.Dir)), - static.WithIndex(staticDir.Index), - static.WithNext(next), - static.WithLogger(ui.StaticLogger), - static.WithPrefix(path), - ) - - route.PathPrefix(path).Handler(handler) - } +func (m *RequestHandler) makeStaticRoutes(router *mux.Router, statics config.StaticDirMappings, next http.Handler) { + for _, staticDir := range statics { + clearPath := strings.TrimSuffix(staticDir.Path, "/") + path := clearPath + "/" + + redirect := router.NewRoute() + redirect.Path(clearPath). + Handler(http.RedirectHandler(path, http.StatusTemporaryRedirect)) + + route := router.NewRoute() + handler := static.NewStaticMiddleware( + static.WithFileSystem(afero.NewBasePathFs(m.fs, staticDir.Dir)), + static.WithIndex(staticDir.Index), + static.WithNext(next), + static.WithLogger(ui.StaticLogger), + static.WithPrefix(path), + ) + + route.PathPrefix(path).Handler(handler) } } diff --git a/internal/handler/uncors_handler.go b/internal/handler/uncors_handler.go index 6301bace..0fdbb686 100644 --- a/internal/handler/uncors_handler.go +++ b/internal/handler/uncors_handler.go @@ -1,7 +1,14 @@ package handler import ( + "errors" + "fmt" "net/http" + "strings" + + "github.com/evg4b/uncors/internal/infra" + + "github.com/evg4b/uncors/pkg/urlx" "github.com/evg4b/uncors/internal/config" "github.com/evg4b/uncors/internal/contracts" @@ -12,20 +19,20 @@ import ( "github.com/spf13/afero" ) -type UncorsRequestHandler struct { +type RequestHandler struct { router *mux.Router fs afero.Fs logger contracts.Logger - mocks []config.Mock mappings []config.URLMapping replacerFactory contracts.URLReplacerFactory httpClient contracts.HTTPClient } -func NewUncorsRequestHandler(options ...UncorsRequestHandlerOption) *UncorsRequestHandler { - handler := &UncorsRequestHandler{ +var errHostNotMapped = errors.New("host not mapped") + +func NewUncorsRequestHandler(options ...UncorsRequestHandlerOption) *RequestHandler { + handler := &RequestHandler{ router: mux.NewRouter(), - mocks: []config.Mock{}, mappings: []config.URLMapping{}, } @@ -39,18 +46,36 @@ func NewUncorsRequestHandler(options ...UncorsRequestHandlerOption) *UncorsReque proxy.WithLogger(ui.ProxyLogger), ) - handler.makeMockedRoutes() - handler.makeStaticRoutes(proxyHandler) - handler.setDefaultHandler(proxyHandler) + for _, mapping := range handler.mappings { + uri, err := urlx.Parse(mapping.From) + if err != nil { + panic(err) + } + + host, _, err := urlx.SplitHostPort(uri) + if err != nil { + panic(err) + } + + router := handler.router.Host(replaceWildcards(host)).Subrouter() + + handler.makeStaticRoutes(router, mapping.Statics, proxyHandler) + handler.makeMockedRoutes(router, mapping.Mocks) + setDefaultHandler(router, proxyHandler) + } + + setDefaultHandler(handler.router, http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + infra.HTTPError(writer, errHostNotMapped) + })) return handler } -func (m *UncorsRequestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { +func (m *RequestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { m.router.ServeHTTP(writer, request) } -func (m *UncorsRequestHandler) createHandler(response config.Response) *mock.Middleware { +func (m *RequestHandler) createHandler(response config.Response) *mock.Middleware { return mock.NewMockMiddleware( mock.WithLogger(ui.MockLogger), mock.WithResponse(response), @@ -58,7 +83,18 @@ func (m *UncorsRequestHandler) createHandler(response config.Response) *mock.Mid ) } -func (m *UncorsRequestHandler) setDefaultHandler(handler http.Handler) { - m.router.NotFoundHandler = handler - m.router.MethodNotAllowedHandler = handler +func setDefaultHandler(router *mux.Router, handler http.Handler) { + router.NotFoundHandler = handler + router.MethodNotAllowedHandler = handler +} + +const wildcard = "*" + +func replaceWildcards(host string) string { + count := strings.Count(host, wildcard) + for i := 1; i <= count; i++ { + host = strings.Replace(host, wildcard, fmt.Sprintf("{p%d}", i), 1) + } + + return host } diff --git a/internal/handler/uncors_handler_internal_test.go b/internal/handler/uncors_handler_internal_test.go new file mode 100644 index 00000000..f0f8ee87 --- /dev/null +++ b/internal/handler/uncors_handler_internal_test.go @@ -0,0 +1,27 @@ +package handler + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReplaceWildcards(t *testing.T) { + tests := []struct { + name string + host string + expected string + }{ + {name: "empty string", host: "", expected: ""}, + {name: "host without wildcard", host: "demo.com", expected: "demo.com"}, + {name: "host with wildcard", host: "*.demo.com", expected: "{p1}.demo.com"}, + {name: "host with multiple wildcard", host: "*.*.demo*.*", expected: "{p1}.{p2}.demo{p3}.{p4}"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := replaceWildcards(tt.host) + + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/internal/handler/uncors_handler_options.go b/internal/handler/uncors_handler_options.go index 4c31daa2..7227d7c2 100644 --- a/internal/handler/uncors_handler_options.go +++ b/internal/handler/uncors_handler_options.go @@ -6,40 +6,34 @@ import ( "github.com/spf13/afero" ) -type UncorsRequestHandlerOption = func(*UncorsRequestHandler) +type UncorsRequestHandlerOption = func(*RequestHandler) func WithLogger(logger contracts.Logger) UncorsRequestHandlerOption { - return func(m *UncorsRequestHandler) { + return func(m *RequestHandler) { m.logger = logger } } -func WithMocks(mocks []config.Mock) UncorsRequestHandlerOption { - return func(m *UncorsRequestHandler) { - m.mocks = mocks - } -} - func WithFileSystem(fs afero.Fs) UncorsRequestHandlerOption { - return func(m *UncorsRequestHandler) { + return func(m *RequestHandler) { m.fs = fs } } func WithURLReplacerFactory(replacerFactory contracts.URLReplacerFactory) UncorsRequestHandlerOption { - return func(m *UncorsRequestHandler) { + return func(m *RequestHandler) { m.replacerFactory = replacerFactory } } func WithHTTPClient(client contracts.HTTPClient) UncorsRequestHandlerOption { - return func(m *UncorsRequestHandler) { + return func(m *RequestHandler) { m.httpClient = client } } func WithMappings(mappings []config.URLMapping) UncorsRequestHandlerOption { - return func(m *UncorsRequestHandler) { + return func(m *RequestHandler) { m.mappings = mappings } } diff --git a/internal/ui/mappings.go b/internal/ui/mappings.go index ce4e6db9..14f0b796 100644 --- a/internal/ui/mappings.go +++ b/internal/ui/mappings.go @@ -7,7 +7,7 @@ import ( "github.com/evg4b/uncors/internal/config" ) -func Mappings(mappings []config.URLMapping, mocksDefs []config.Mock) string { +func Mappings(mappings []config.URLMapping) string { var builder strings.Builder for _, mapping := range mappings { @@ -17,6 +17,9 @@ func Mappings(mappings []config.URLMapping, mocksDefs []config.Mock) string { builder.WriteString(fmt.Sprintf(" static: %s => %s\n", static.Path, static.Dir)) } } + if len(mapping.Mocks) > 0 { + builder.WriteString(fmt.Sprintf("MOCKS: %d mock(s) registered", len(mapping.Mocks))) + } } for _, mapping := range mappings { if strings.HasPrefix(mapping.From, "http:") { @@ -25,9 +28,9 @@ func Mappings(mappings []config.URLMapping, mocksDefs []config.Mock) string { builder.WriteString(fmt.Sprintf(" static: %s => %s\n", static.Path, static.Dir)) } } - } - if len(mocksDefs) > 0 { - builder.WriteString(fmt.Sprintf("MOCKS: %d mock(s) registered", len(mocksDefs))) + if len(mapping.Mocks) > 0 { + builder.WriteString(fmt.Sprintf("MOCKS: %d mock(s) registered", len(mapping.Mocks))) + } } builder.WriteString("\n") diff --git a/internal/ui/mappings_test.go b/internal/ui/mappings_test.go index 492242b2..557608b8 100644 --- a/internal/ui/mappings_test.go +++ b/internal/ui/mappings_test.go @@ -12,10 +12,9 @@ import ( func TestMappings(t *testing.T) { tests := []struct { - name string - mappings []config.URLMapping - mocksDefs []config.Mock - expected string + name string + mappings []config.URLMapping + expected string }{ { name: "no mapping and no mocks", @@ -36,35 +35,20 @@ func TestMappings(t *testing.T) { }, expected: "PROXY: https://localhost => https://github.com\nPROXY: http://localhost => https://github.com\n\n", }, - { - name: "one mock only", - mocksDefs: []config.Mock{ - {}, - }, - expected: "MOCKS: 1 mock(s) registered\n", - }, - { - name: "2 mocks only", - mocksDefs: []config.Mock{ - {}, {}, {}, - }, - expected: "MOCKS: 3 mock(s) registered\n", - }, { name: "mapping and mocks", mappings: []config.URLMapping{ - {From: "http://localhost", To: "https://github.com"}, + {From: "http://localhost", To: "https://github.com", Mocks: []config.Mock{ + {}, {}, {}, + }}, {From: "https://localhost", To: "https://github.com"}, }, - mocksDefs: []config.Mock{ - {}, {}, {}, - }, expected: "PROXY: https://localhost => https://github.com\nPROXY: http://localhost => https://github.com\nMOCKS: 3 mock(s) registered\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual := ui.Mappings(tt.mappings, tt.mocksDefs) + actual := ui.Mappings(tt.mappings) assert.Equal(t, tt.expected, actual) }) } diff --git a/main.go b/main.go index d5735d5f..0328c1d2 100644 --- a/main.go +++ b/main.go @@ -87,7 +87,6 @@ func main() { globalHandler := handler.NewUncorsRequestHandler( handler.WithMappings(mappings), handler.WithLogger(ui.MockLogger), - handler.WithMocks(uncorsConfig.Mocks), handler.WithFileSystem(afero.NewOsFs()), handler.WithURLReplacerFactory(factory), handler.WithHTTPClient(httpClient), @@ -118,7 +117,7 @@ func main() { log.Print("\n") log.Warning(ui.DisclaimerMessage) log.Print("\n") - log.Info(ui.Mappings(mappings, uncorsConfig.Mocks)) + log.Info(ui.Mappings(mappings)) log.Print("\n") go version.CheckNewVersion(ctx, httpClient, Version) From e69b91227c2f03c47dfa25a73edcc7fe22db59d9 Mon Sep 17 00:00:00 2001 From: Evgeny Abramovich Date: Sun, 21 May 2023 11:38:39 -0300 Subject: [PATCH 2/9] Fixed tests --- internal/config/config_test.go | 95 ++++--- .../config/config_test_data/full-config.yaml | 26 +- internal/config/helpers.go | 4 +- internal/handler/uncors_handler_test.go | 245 +++++++++--------- internal/ui/mappings_test.go | 22 +- 5 files changed, 203 insertions(+), 189 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7dc93bfe..462dfc14 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -29,7 +29,6 @@ func TestLoadConfiguration(t *testing.T) { HTTPPort: 80, HTTPSPort: 443, Mappings: []config.URLMapping{}, - Mocks: []config.Mock{}, }, }, { @@ -41,7 +40,6 @@ func TestLoadConfiguration(t *testing.T) { Mappings: []config.URLMapping{ {From: "http://demo", To: "https://demo.com"}, }, - Mocks: []config.Mock{}, }, }, { @@ -51,33 +49,32 @@ func TestLoadConfiguration(t *testing.T) { HTTPPort: 8080, Mappings: []config.URLMapping{ {From: "http://demo1", To: "https://demo1.com"}, - {From: "http://other-demo2", To: "https://demo2.io"}, + {From: "http://other-demo2", To: "https://demo2.io", Mocks: []config.Mock{ + { + Path: "/demo", + Method: "POST", + Queries: map[string]string{ + "foo": "bar", + }, + Headers: map[string]string{ + "accept-encoding": "deflate", + }, + Response: config.Response{ + Code: 201, + Headers: map[string]string{ + "accept-encoding": "deflate", + }, + RawContent: "demo", + File: "/demo.txt", + }, + }, + }}, }, Proxy: "localhost:8080", Debug: true, HTTPSPort: 8081, CertFile: "/cert-file.pem", KeyFile: "/key-file.key", - Mocks: []config.Mock{ - { - Path: "/demo", - Method: "POST", - Queries: map[string]string{ - "foo": "bar", - }, - Headers: map[string]string{ - "accept-encoding": "deflate", - }, - Response: config.Response{ - Code: 201, - Headers: map[string]string{ - "accept-encoding": "deflate", - }, - RawContent: "demo", - File: "/demo.txt", - }, - }, - }, }, }, { @@ -92,7 +89,29 @@ func TestLoadConfiguration(t *testing.T) { HTTPPort: 8080, Mappings: []config.URLMapping{ {From: "http://demo1", To: "https://demo1.com"}, - {From: "http://other-demo2", To: "https://demo2.io"}, + { + From: "http://other-demo2", + To: "https://demo2.io", + Mocks: []config.Mock{ + { + Path: "/demo", + Method: "POST", + Queries: map[string]string{ + "foo": "bar", + }, + Headers: map[string]string{ + "accept-encoding": "deflate", + }, + Response: config.Response{ + Code: 201, + Headers: map[string]string{ + "accept-encoding": "deflate", + }, + RawContent: "demo", + File: "/demo.txt", + }, + }, + }}, {From: mocks.SourceHost1, To: mocks.TargetHost1}, {From: mocks.SourceHost2, To: mocks.TargetHost2}, {From: mocks.SourceHost3, To: mocks.TargetHost3}, @@ -102,35 +121,15 @@ func TestLoadConfiguration(t *testing.T) { HTTPSPort: 8081, CertFile: "/cert-file.pem", KeyFile: "/key-file.key", - Mocks: []config.Mock{ - { - Path: "/demo", - Method: "POST", - Queries: map[string]string{ - "foo": "bar", - }, - Headers: map[string]string{ - "accept-encoding": "deflate", - }, - Response: config.Response{ - Code: 201, - Headers: map[string]string{ - "accept-encoding": "deflate", - }, - RawContent: "demo", - File: "/demo.txt", - }, - }, - }, }, }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - config, err := config.LoadConfiguration(viperInstance, testCase.args) + uncorsConfig, err := config.LoadConfiguration(viperInstance, testCase.args) assert.NoError(t, err) - assert.Equal(t, testCase.expected, config) + assert.Equal(t, testCase.expected, uncorsConfig) }) } }) @@ -222,9 +221,9 @@ func TestLoadConfiguration(t *testing.T) { } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - config, err := config.LoadConfiguration(viperInstance, testCase.args) + uncorsConfig, err := config.LoadConfiguration(viperInstance, testCase.args) - assert.Nil(t, config) + assert.Nil(t, uncorsConfig) for _, expected := range testCase.expected { assert.ErrorContains(t, err, expected) } diff --git a/internal/config/config_test_data/full-config.yaml b/internal/config/config_test_data/full-config.yaml index fed88d23..89c71d06 100644 --- a/internal/config/config_test_data/full-config.yaml +++ b/internal/config/config_test_data/full-config.yaml @@ -3,21 +3,21 @@ mappings: - http://demo1: https://demo1.com - from: http://other-demo2 to: https://demo2.io + mocks: + - path: /demo + method: POST + queries: + foo: bar + headers: + Accept-Encoding: deflate + response: + code: 201 + headers: + Accept-Encoding: deflate + raw-content: demo + file: /demo.txt proxy: localhost:8080 debug: true https-port: 8081 cert-file: /cert-file.pem key-file: /key-file.key -mocks: - - path: /demo - method: POST - queries: - foo: bar - headers: - Accept-Encoding: deflate - response: - code: 201 - headers: - Accept-Encoding: deflate - raw-content: demo - file: /demo.txt diff --git a/internal/config/helpers.go b/internal/config/helpers.go index 0a0b598f..f0db177a 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -8,8 +8,6 @@ import ( "github.com/samber/lo" - "github.com/evg4b/uncors/internal/log" - "github.com/spf13/viper" ) @@ -36,7 +34,7 @@ func readURLMapping(config *viper.Viper, configuration *UncorsConfig) error { }) if ok { - log.Warningf("Mapping for %s from (%s) replaced new value (%s)", key, prev, value) + // log.Warningf("Mapping for %s from (%s) replaced new value (%s)", key, prev, value) prev.To = value } else { configuration.Mappings = append(configuration.Mappings, URLMapping{ diff --git a/internal/handler/uncors_handler_test.go b/internal/handler/uncors_handler_test.go index 2cae4b40..4eae598d 100644 --- a/internal/handler/uncors_handler_test.go +++ b/internal/handler/uncors_handler_test.go @@ -63,36 +63,35 @@ func TestUncorsRequestHandler(t *testing.T) { {Dir: "/assets", Path: "/pnp/", Index: "index.php"}, {Dir: "/images", Path: "/img/"}, }, - }, - } - - mockDefs := []config.Mock{ - { - Path: "/api/mocks/1", - Response: config.Response{ - Code: http.StatusOK, - RawContent: "mock-1", - }, - }, - { - Path: "/api/mocks/2", - Response: config.Response{ - Code: http.StatusOK, - File: "/mock.json", - }, - }, - { - Path: "/api/mocks/3", - Response: config.Response{ - Code: http.StatusMultiStatus, - RawContent: "mock-3", - }, - }, - { - Path: "/api/mocks/4", - Response: config.Response{ - Code: http.StatusOK, - File: "/unknown.json", + Mocks: []config.Mock{ + { + Path: "/api/mocks/1", + Response: config.Response{ + Code: http.StatusOK, + RawContent: "mock-1", + }, + }, + { + Path: "/api/mocks/2", + Response: config.Response{ + Code: http.StatusOK, + File: "/mock.json", + }, + }, + { + Path: "/api/mocks/3", + Response: config.Response{ + Code: http.StatusMultiStatus, + RawContent: "mock-3", + }, + }, + { + Path: "/api/mocks/4", + Response: config.Response{ + Code: http.StatusOK, + File: "/unknown.json", + }, + }, }, }, } @@ -120,7 +119,6 @@ func TestUncorsRequestHandler(t *testing.T) { hand := handler.NewUncorsRequestHandler( handler.WithLogger(mocks.NewLoggerMock(t)), - handler.WithMocks(mockDefs), handler.WithFileSystem(fs), handler.WithURLReplacerFactory(factory), handler.WithHTTPClient(httpMock), @@ -298,13 +296,16 @@ func TestMockMiddleware(t *testing.T) { handler.WithHTTPClient(mocks.NewHTTPClientMock(t)), handler.WithURLReplacerFactory(mocks.NewURLReplacerFactoryMock(t)), handler.WithLogger(logger), - handler.WithMocks([]config.Mock{{ - Path: "/api", - Response: config.Response{ - Code: http.StatusOK, - RawContent: mock1Body, - }, - }}), + handler.WithMappings([]config.URLMapping{ + // TODO: add hosts + {From: "*", To: "*", Mocks: []config.Mock{{ + Path: "/api", + Response: config.Response{ + Code: http.StatusOK, + RawContent: mock1Body, + }, + }}}, + }), ) methods := []string{ @@ -334,9 +335,17 @@ func TestMockMiddleware(t *testing.T) { t.Run("where method is set", func(t *testing.T) { expectedCode := 299 expectedBody := "forwarded" - factory, err := urlreplacer.NewURLReplacerFactory([]config.URLMapping{ - {From: "*", To: "*"}, - }) + mappings := []config.URLMapping{ + {From: "*", To: "*", Mocks: []config.Mock{{ + Path: "/api", + Method: http.MethodPut, + Response: config.Response{ + Code: http.StatusOK, + RawContent: mock1Body, + }, + }}}, + } + factory, err := urlreplacer.NewURLReplacerFactory(mappings) testutils.CheckNoError(t, err) middleware := handler.NewUncorsRequestHandler( @@ -350,14 +359,7 @@ func TestMockMiddleware(t *testing.T) { })), handler.WithURLReplacerFactory(factory), handler.WithLogger(logger), - handler.WithMocks([]config.Mock{{ - Path: "/api", - Method: http.MethodPut, - Response: config.Response{ - Code: http.StatusOK, - RawContent: mock1Body, - }, - }}), + handler.WithMappings(mappings), ) t.Run("method is not matched", func(t *testing.T) { @@ -409,23 +411,8 @@ func TestMockMiddleware(t *testing.T) { t.Run("path handling", func(t *testing.T) { expectedCode := 299 expectedBody := "forwarded" - factory, err := urlreplacer.NewURLReplacerFactory([]config.URLMapping{ - {From: "*", To: "*"}, - }) - testutils.CheckNoError(t, err) - - middleware := handler.NewUncorsRequestHandler( - handler.WithHTTPClient(mocks.NewHTTPClientMock(t).DoMock. - Set(func(req *http.Request) (*http.Response, error) { - return &http.Response{ - Request: req, - StatusCode: expectedCode, - Body: io.NopCloser(strings.NewReader(expectedBody)), - }, nil - })), - handler.WithURLReplacerFactory(factory), - handler.WithLogger(logger), - handler.WithMocks([]config.Mock{ + mappings := []config.URLMapping{ + {From: "*", To: "*", Mocks: []config.Mock{ { Path: userPath, Response: config.Response{ @@ -454,7 +441,23 @@ func TestMockMiddleware(t *testing.T) { RawContent: mock4Body, }, }, - }), + }}, + } + factory, err := urlreplacer.NewURLReplacerFactory(mappings) + testutils.CheckNoError(t, err) + + middleware := handler.NewUncorsRequestHandler( + handler.WithHTTPClient(mocks.NewHTTPClientMock(t).DoMock. + Set(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + Request: req, + StatusCode: expectedCode, + Body: io.NopCloser(strings.NewReader(expectedBody)), + }, nil + })), + handler.WithURLReplacerFactory(factory), + handler.WithLogger(logger), + handler.WithMappings(mappings), ) tests := []struct { @@ -519,35 +522,37 @@ func TestMockMiddleware(t *testing.T) { handler.WithHTTPClient(mocks.NewHTTPClientMock(t)), handler.WithURLReplacerFactory(mocks.NewURLReplacerFactoryMock(t)), handler.WithLogger(logger), - handler.WithMocks([]config.Mock{ - { - Path: userPath, - Response: config.Response{ - Code: http.StatusOK, - RawContent: mock1Body, - }, - }, - { - Path: userPath, - Queries: map[string]string{ - "id": "17", - }, - Response: config.Response{ - Code: http.StatusCreated, - RawContent: mock2Body, + handler.WithMappings([]config.URLMapping{ + {From: "*", To: "*", Mocks: []config.Mock{ + { + Path: userPath, + Response: config.Response{ + Code: http.StatusOK, + RawContent: mock1Body, + }, }, - }, - { - Path: userPath, - Queries: map[string]string{ - "id": "99", - "token": "fe145b54563d9be1b2a476f56b0a412b", + { + Path: userPath, + Queries: map[string]string{ + "id": "17", + }, + Response: config.Response{ + Code: http.StatusCreated, + RawContent: mock2Body, + }, }, - Response: config.Response{ - Code: http.StatusAccepted, - RawContent: mock3Body, + { + Path: userPath, + Queries: map[string]string{ + "id": "99", + "token": "fe145b54563d9be1b2a476f56b0a412b", + }, + Response: config.Response{ + Code: http.StatusAccepted, + RawContent: mock3Body, + }, }, - }, + }}, }), ) @@ -613,35 +618,37 @@ func TestMockMiddleware(t *testing.T) { handler.WithHTTPClient(mocks.NewHTTPClientMock(t)), handler.WithURLReplacerFactory(mocks.NewURLReplacerFactoryMock(t)), handler.WithLogger(logger), - handler.WithMocks([]config.Mock{ - { - Path: userPath, - Response: config.Response{ - Code: http.StatusOK, - RawContent: mock1Body, - }, - }, - { - Path: userPath, - Headers: map[string]string{ - headers.XCSRFToken: "de4e27987d054577b0edc0e828851724", - }, - Response: config.Response{ - Code: http.StatusCreated, - RawContent: mock2Body, + handler.WithMappings([]config.URLMapping{ + {From: "*", To: "*", Mocks: []config.Mock{ + { + Path: userPath, + Response: config.Response{ + Code: http.StatusOK, + RawContent: mock1Body, + }, }, - }, - { - Path: userPath, - Headers: map[string]string{ - userIDHeader: "99", - headers.XCSRFToken: "fe145b54563d9be1b2a476f56b0a412b", + { + Path: userPath, + Headers: map[string]string{ + headers.XCSRFToken: "de4e27987d054577b0edc0e828851724", + }, + Response: config.Response{ + Code: http.StatusCreated, + RawContent: mock2Body, + }, }, - Response: config.Response{ - Code: http.StatusAccepted, - RawContent: mock3Body, + { + Path: userPath, + Headers: map[string]string{ + userIDHeader: "99", + headers.XCSRFToken: "fe145b54563d9be1b2a476f56b0a412b", + }, + Response: config.Response{ + Code: http.StatusAccepted, + RawContent: mock3Body, + }, }, - }, + }}, }), ) diff --git a/internal/ui/mappings_test.go b/internal/ui/mappings_test.go index 557608b8..76823392 100644 --- a/internal/ui/mappings_test.go +++ b/internal/ui/mappings_test.go @@ -14,18 +14,18 @@ func TestMappings(t *testing.T) { tests := []struct { name string mappings []config.URLMapping - expected string + expected []string }{ { name: "no mapping and no mocks", - expected: "\n", + expected: []string{"\n"}, }, { name: "http mapping only", mappings: []config.URLMapping{ {From: "http://localhost", To: "https://github.com"}, }, - expected: "PROXY: http://localhost => https://github.com\n\n", + expected: []string{"PROXY: http://localhost => https://github.com"}, }, { name: "http and https mappings", @@ -33,7 +33,10 @@ func TestMappings(t *testing.T) { {From: "http://localhost", To: "https://github.com"}, {From: "https://localhost", To: "https://github.com"}, }, - expected: "PROXY: https://localhost => https://github.com\nPROXY: http://localhost => https://github.com\n\n", + expected: []string{ + "PROXY: https://localhost => https://github.com", + "PROXY: http://localhost => https://github.com", + }, }, { name: "mapping and mocks", @@ -43,13 +46,20 @@ func TestMappings(t *testing.T) { }}, {From: "https://localhost", To: "https://github.com"}, }, - expected: "PROXY: https://localhost => https://github.com\nPROXY: http://localhost => https://github.com\nMOCKS: 3 mock(s) registered\n", + expected: []string{ + "PROXY: https://localhost => https://github.com", + "PROXY: http://localhost => https://github.com", + "MOCKS: 3 mock(s) registered", + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := ui.Mappings(tt.mappings) - assert.Equal(t, tt.expected, actual) + + for _, expectedLine := range tt.expected { + assert.Contains(t, actual, expectedLine) + } }) } } From 7eaf27bf8fb15b3a8bef6570f7e59d6c43f800c6 Mon Sep 17 00:00:00 2001 From: Evgeny Abramovich Date: Sun, 21 May 2023 11:55:04 -0300 Subject: [PATCH 3/9] Reorganised structure --- internal/config/config.go | 16 +++---- internal/config/config_test.go | 8 ++-- internal/config/helpers.go | 6 ++- .../config/{url_mapping.go => mapping.go} | 32 ++++++------- .../{url_mapping_test.go => mapping_test.go} | 20 ++++---- internal/{ui => config}/mappings.go | 8 ++-- internal/{ui => config}/mappings_test.go | 16 +++---- .../config/{static_mapping.go => static.go} | 16 +++---- ...{static_mapping_test.go => static_test.go} | 22 ++++----- internal/config/validation_test.go | 2 +- internal/handler/proxy/middleware_test.go | 2 +- internal/handler/static_routes.go | 2 +- internal/handler/uncors_handler.go | 4 +- internal/handler/uncors_handler_options.go | 2 +- internal/handler/uncors_handler_test.go | 14 +++--- internal/helpers/mappings.go | 9 +--- internal/helpers/mappings_test.go | 46 +++++++++---------- internal/urlreplacer/factory.go | 2 +- internal/urlreplacer/factory_test.go | 12 ++--- main.go | 2 +- 20 files changed, 118 insertions(+), 123 deletions(-) rename internal/config/{url_mapping.go => mapping.go} (56%) rename internal/config/{url_mapping_test.go => mapping_test.go} (86%) rename internal/{ui => config}/mappings.go (90%) rename internal/{ui => config}/mappings_test.go (85%) rename internal/config/{static_mapping.go => static.go} (73%) rename internal/config/{static_mapping_test.go => static_test.go} (86%) diff --git a/internal/config/config.go b/internal/config/config.go index bc5d6625..be00513a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,13 +15,13 @@ const ( ) type UncorsConfig struct { - HTTPPort int `mapstructure:"http-port" validate:"required"` - Mappings []URLMapping `mapstructure:"mappings" validate:"required"` - Proxy string `mapstructure:"proxy"` - Debug bool `mapstructure:"debug"` - HTTPSPort int `mapstructure:"https-port"` - CertFile string `mapstructure:"cert-file"` - KeyFile string `mapstructure:"key-file"` + HTTPPort int `mapstructure:"http-port" validate:"required"` + Mappings Mappings `mapstructure:"mappings" validate:"required"` + Proxy string `mapstructure:"proxy"` + Debug bool `mapstructure:"debug"` + HTTPSPort int `mapstructure:"https-port"` + CertFile string `mapstructure:"cert-file"` + KeyFile string `mapstructure:"key-file"` } func (config *UncorsConfig) IsHTTPSEnabled() bool { @@ -39,7 +39,7 @@ func LoadConfiguration(viperInstance *viper.Viper, args []string) (*UncorsConfig } configuration := &UncorsConfig{ - Mappings: []URLMapping{}, + Mappings: []Mapping{}, } if configPath := viperInstance.GetString("config"); len(configPath) > 0 { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 462dfc14..9b870b15 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -28,7 +28,7 @@ func TestLoadConfiguration(t *testing.T) { expected: &config.UncorsConfig{ HTTPPort: 80, HTTPSPort: 443, - Mappings: []config.URLMapping{}, + Mappings: []config.Mapping{}, }, }, { @@ -37,7 +37,7 @@ func TestLoadConfiguration(t *testing.T) { expected: &config.UncorsConfig{ HTTPPort: 8080, HTTPSPort: 443, - Mappings: []config.URLMapping{ + Mappings: []config.Mapping{ {From: "http://demo", To: "https://demo.com"}, }, }, @@ -47,7 +47,7 @@ func TestLoadConfiguration(t *testing.T) { args: []string{params.Config, "/full-config.yaml"}, expected: &config.UncorsConfig{ HTTPPort: 8080, - Mappings: []config.URLMapping{ + Mappings: []config.Mapping{ {From: "http://demo1", To: "https://demo1.com"}, {From: "http://other-demo2", To: "https://demo2.io", Mocks: []config.Mock{ { @@ -87,7 +87,7 @@ func TestLoadConfiguration(t *testing.T) { }, expected: &config.UncorsConfig{ HTTPPort: 8080, - Mappings: []config.URLMapping{ + Mappings: []config.Mapping{ {From: "http://demo1", To: "https://demo1.com"}, { From: "http://other-demo2", diff --git a/internal/config/helpers.go b/internal/config/helpers.go index f0db177a..d91027e5 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -2,6 +2,7 @@ package config import ( "errors" + "github.com/evg4b/uncors/internal/config/hooks" "strings" "github.com/mitchellh/mapstructure" @@ -29,7 +30,7 @@ func readURLMapping(config *viper.Viper, configuration *UncorsConfig) error { for index, key := range from { value := to[index] - prev, ok := lo.Find(configuration.Mappings, func(item URLMapping) bool { + prev, ok := lo.Find(configuration.Mappings, func(item Mapping) bool { return strings.EqualFold(item.From, key) }) @@ -37,7 +38,7 @@ func readURLMapping(config *viper.Viper, configuration *UncorsConfig) error { // log.Warningf("Mapping for %s from (%s) replaced new value (%s)", key, prev, value) prev.To = value } else { - configuration.Mappings = append(configuration.Mappings, URLMapping{ + configuration.Mappings = append(configuration.Mappings, Mapping{ From: key, To: value, }) @@ -49,6 +50,7 @@ func readURLMapping(config *viper.Viper, configuration *UncorsConfig) error { func decodeConfig[T any](data any, mapping *T, decodeFuncs ...mapstructure.DecodeHookFunc) error { hook := mapstructure.ComposeDecodeHookFunc( + hooks.StringToTimeDurationHookFunc(), mapstructure.StringToSliceHookFunc(","), mapstructure.ComposeDecodeHookFunc(decodeFuncs...), ) diff --git a/internal/config/url_mapping.go b/internal/config/mapping.go similarity index 56% rename from internal/config/url_mapping.go rename to internal/config/mapping.go index c9fd1aec..1d5bec1f 100644 --- a/internal/config/url_mapping.go +++ b/internal/config/mapping.go @@ -7,45 +7,45 @@ import ( "github.com/samber/lo" ) -type URLMapping struct { - From string `mapstructure:"from"` - To string `mapstructure:"to"` - Statics StaticDirMappings `mapstructure:"statics"` - Mocks []Mock `mapstructure:"mocks"` +type Mapping struct { + From string `mapstructure:"from"` + To string `mapstructure:"to"` + Statics StaticDirs `mapstructure:"statics"` + Mocks []Mock `mapstructure:"mocks"` } -func (u URLMapping) Clone() URLMapping { - return URLMapping{ +func (u Mapping) Clone() Mapping { + return Mapping{ From: u.From, To: u.To, - Statics: lo.If(u.Statics == nil, StaticDirMappings(nil)). - ElseF(func() StaticDirMappings { - return lo.Map(u.Statics, func(item StaticDirMapping, index int) StaticDirMapping { + Statics: lo.If(u.Statics == nil, StaticDirs(nil)). + ElseF(func() StaticDirs { + return lo.Map(u.Statics, func(item StaticDir, index int) StaticDir { return item.Clone() }) }), } } -var urlMappingType = reflect.TypeOf(URLMapping{}) -var urlMappingFields = getTagValues(urlMappingType, "mapstructure") +var mappingType = reflect.TypeOf(Mapping{}) +var mappingFields = getTagValues(mappingType, "mapstructure") func URLMappingHookFunc() mapstructure.DecodeHookFunc { return func(f reflect.Type, t reflect.Type, rawData any) (any, error) { - if t != urlMappingType || f.Kind() != reflect.Map { + if t != mappingType || f.Kind() != reflect.Map { return rawData, nil } if data, ok := rawData.(map[string]any); ok { - availableFields, _ := lo.Difference(lo.Keys(data), urlMappingFields) + availableFields, _ := lo.Difference(lo.Keys(data), mappingFields) if len(data) == 1 && len(availableFields) == 1 { - return URLMapping{ + return Mapping{ From: availableFields[0], To: data[availableFields[0]].(string), // nolint: forcetypeassert }, nil } - mapping := URLMapping{} + mapping := Mapping{} err := decodeConfig(data, &mapping, StaticDirMappingHookFunc()) return mapping, err diff --git a/internal/config/url_mapping_test.go b/internal/config/mapping_test.go similarity index 86% rename from internal/config/url_mapping_test.go rename to internal/config/mapping_test.go index 40c1f024..e37fcdc2 100644 --- a/internal/config/url_mapping_test.go +++ b/internal/config/mapping_test.go @@ -21,12 +21,12 @@ func TestURLMappingHookFunc(t *testing.T) { tests := []struct { name string config string - expected config.URLMapping + expected config.Mapping }{ { name: "simple key-value mapping", config: "http://localhost:4200: https://github.com", - expected: config.URLMapping{ + expected: config.Mapping{ From: "http://localhost:4200", To: "https://github.com", }, @@ -34,7 +34,7 @@ func TestURLMappingHookFunc(t *testing.T) { { name: "full object mapping", config: "{ from: http://localhost:3000, to: https://google.com }", - expected: config.URLMapping{ + expected: config.Mapping{ From: "http://localhost:3000", To: "https://google.com", }, @@ -50,7 +50,7 @@ func TestURLMappingHookFunc(t *testing.T) { err := viperInstance.ReadInConfig() testutils.CheckNoError(t, err) - actual := config.URLMapping{} + actual := config.Mapping{} err = viperInstance.Unmarshal(&actual, viper.DecodeHook( config.URLMappingHookFunc(), @@ -66,31 +66,31 @@ func TestURLMappingHookFunc(t *testing.T) { func TestURLMappingClone(t *testing.T) { tests := []struct { name string - expected config.URLMapping + expected config.Mapping }{ { name: "empty structure", - expected: config.URLMapping{}, + expected: config.Mapping{}, }, { name: "structure with 1 field", - expected: config.URLMapping{ + expected: config.Mapping{ From: localhost, }, }, { name: "structure with 2 field", - expected: config.URLMapping{ + expected: config.Mapping{ From: localhost, To: localhostSecure, }, }, { name: "structure with inner collections", - expected: config.URLMapping{ + expected: config.Mapping{ From: localhost, To: localhostSecure, - Statics: []config.StaticDirMapping{ + Statics: []config.StaticDir{ {Path: "/cc", Dir: "cc"}, }, }, diff --git a/internal/ui/mappings.go b/internal/config/mappings.go similarity index 90% rename from internal/ui/mappings.go rename to internal/config/mappings.go index 14f0b796..2de8e272 100644 --- a/internal/ui/mappings.go +++ b/internal/config/mappings.go @@ -1,13 +1,13 @@ -package ui +package config import ( "fmt" "strings" - - "github.com/evg4b/uncors/internal/config" ) -func Mappings(mappings []config.URLMapping) string { +type Mappings []Mapping + +func (mappings Mappings) String() string { var builder strings.Builder for _, mapping := range mappings { diff --git a/internal/ui/mappings_test.go b/internal/config/mappings_test.go similarity index 85% rename from internal/ui/mappings_test.go rename to internal/config/mappings_test.go index 76823392..a94a220a 100644 --- a/internal/ui/mappings_test.go +++ b/internal/config/mappings_test.go @@ -1,19 +1,17 @@ //nolint:lll -package ui_test +package config_test import ( - "testing" - "github.com/evg4b/uncors/internal/config" + "testing" - "github.com/evg4b/uncors/internal/ui" "github.com/stretchr/testify/assert" ) func TestMappings(t *testing.T) { tests := []struct { name string - mappings []config.URLMapping + mappings config.Mappings expected []string }{ { @@ -22,14 +20,14 @@ func TestMappings(t *testing.T) { }, { name: "http mapping only", - mappings: []config.URLMapping{ + mappings: config.Mappings{ {From: "http://localhost", To: "https://github.com"}, }, expected: []string{"PROXY: http://localhost => https://github.com"}, }, { name: "http and https mappings", - mappings: []config.URLMapping{ + mappings: config.Mappings{ {From: "http://localhost", To: "https://github.com"}, {From: "https://localhost", To: "https://github.com"}, }, @@ -40,7 +38,7 @@ func TestMappings(t *testing.T) { }, { name: "mapping and mocks", - mappings: []config.URLMapping{ + mappings: config.Mappings{ {From: "http://localhost", To: "https://github.com", Mocks: []config.Mock{ {}, {}, {}, }}, @@ -55,7 +53,7 @@ func TestMappings(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual := ui.Mappings(tt.mappings) + actual := tt.mappings.String() for _, expectedLine := range tt.expected { assert.Contains(t, actual, expectedLine) diff --git a/internal/config/static_mapping.go b/internal/config/static.go similarity index 73% rename from internal/config/static_mapping.go rename to internal/config/static.go index cabdb36c..d14469ad 100644 --- a/internal/config/static_mapping.go +++ b/internal/config/static.go @@ -6,23 +6,23 @@ import ( "github.com/mitchellh/mapstructure" ) -type StaticDirMappings = []StaticDirMapping +type StaticDirs = []StaticDir -type StaticDirMapping struct { +type StaticDir struct { Path string `mapstructure:"path"` Dir string `mapstructure:"dir"` Index string `mapstructure:"index"` } -func (s StaticDirMapping) Clone() StaticDirMapping { - return StaticDirMapping{ +func (s StaticDir) Clone() StaticDir { + return StaticDir{ Path: s.Path, Dir: s.Dir, Index: s.Index, } } -var staticDirMappingsType = reflect.TypeOf(StaticDirMappings{}) +var staticDirMappingsType = reflect.TypeOf(StaticDirs{}) func StaticDirMappingHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn return func(f reflect.Type, t reflect.Type, rawData any) (any, error) { @@ -35,10 +35,10 @@ func StaticDirMappingHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn return rawData, nil } - var mappings StaticDirMappings + var mappings StaticDirs for path, mappingDef := range mappingsDefs { if def, ok := mappingDef.(string); ok { - mappings = append(mappings, StaticDirMapping{ + mappings = append(mappings, StaticDir{ Path: path, Dir: def, }) @@ -46,7 +46,7 @@ func StaticDirMappingHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn continue } - mapping := StaticDirMapping{} + mapping := StaticDir{} err := decodeConfig(mappingDef, &mapping) if err != nil { return nil, err diff --git a/internal/config/static_mapping_test.go b/internal/config/static_test.go similarity index 86% rename from internal/config/static_mapping_test.go rename to internal/config/static_test.go index 7b1df0c6..4aca8b29 100644 --- a/internal/config/static_mapping_test.go +++ b/internal/config/static_test.go @@ -19,13 +19,13 @@ const ( func TestStaticDirMappingHookFunc(t *testing.T) { const configFile = "config.yaml" type testType struct { - Statics config.StaticDirMappings `mapstructure:"statics"` + Statics config.StaticDirs `mapstructure:"statics"` } tests := []struct { name string config string - expected config.StaticDirMappings + expected config.StaticDirs }{ { name: "decode plan mapping", @@ -34,7 +34,7 @@ statics: /path: /static-dir /another-path: /another-static-dir `, - expected: config.StaticDirMappings{ + expected: config.StaticDirs{ {Path: anotherPath, Dir: anotherStaticDir}, {Path: path, Dir: staticDir}, }, @@ -46,7 +46,7 @@ statics: /path: { dir: /static-dir } /another-path: { dir: /another-static-dir } `, - expected: config.StaticDirMappings{ + expected: config.StaticDirs{ {Path: path, Dir: staticDir}, {Path: anotherPath, Dir: anotherStaticDir}, }, @@ -58,7 +58,7 @@ statics: /path: { dir: /static-dir, index: index.html } /another-path: { dir: /another-static-dir, index: default.html } `, - expected: config.StaticDirMappings{ + expected: config.StaticDirs{ {Path: path, Dir: staticDir, Index: "index.html"}, {Path: anotherPath, Dir: anotherStaticDir, Index: "default.html"}, }, @@ -70,7 +70,7 @@ statics: /path: { dir: /static-dir, index: index.html } /another-path: /another-static-dir `, - expected: config.StaticDirMappings{ + expected: config.StaticDirs{ {Path: path, Dir: staticDir, Index: "index.html"}, {Path: anotherPath, Dir: anotherStaticDir}, }, @@ -101,28 +101,28 @@ statics: func TestStaticDirMappingClone(t *testing.T) { tests := []struct { name string - expected config.StaticDirMapping + expected config.StaticDir }{ { name: "empty structure", - expected: config.StaticDirMapping{}, + expected: config.StaticDir{}, }, { name: "structure with 1 field", - expected: config.StaticDirMapping{ + expected: config.StaticDir{ Dir: "dir", }, }, { name: "structure with 2 field", - expected: config.StaticDirMapping{ + expected: config.StaticDir{ Dir: "dir", Path: "/some-path", }, }, { name: "structure with all field", - expected: config.StaticDirMapping{ + expected: config.StaticDir{ Dir: "dir", Path: "/one-more-path", Index: "index.html", diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go index 0ef1b4b6..cf0b09d1 100644 --- a/internal/config/validation_test.go +++ b/internal/config/validation_test.go @@ -16,7 +16,7 @@ func TestValidate(t *testing.T) { { name: "invalid http-port", config: &config.UncorsConfig{ - Mappings: []config.URLMapping{}, + Mappings: []config.Mapping{}, }, expected: "Key: 'UncorsConfig.HTTPPort' Error:Field validation for 'HTTPPort' failed on the 'required' tag", }, diff --git a/internal/handler/proxy/middleware_test.go b/internal/handler/proxy/middleware_test.go index de7296bc..870e0f0a 100644 --- a/internal/handler/proxy/middleware_test.go +++ b/internal/handler/proxy/middleware_test.go @@ -22,7 +22,7 @@ import ( ) func TestProxyMiddleware(t *testing.T) { - replacerFactory, err := urlreplacer.NewURLReplacerFactory([]config.URLMapping{ + replacerFactory, err := urlreplacer.NewURLReplacerFactory([]config.Mapping{ {From: "http://premium.local.com", To: "https://premium.api.com"}, }) testutils.CheckNoError(t, err) diff --git a/internal/handler/static_routes.go b/internal/handler/static_routes.go index 1208fe26..1d3ec9a1 100644 --- a/internal/handler/static_routes.go +++ b/internal/handler/static_routes.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/afero" ) -func (m *RequestHandler) makeStaticRoutes(router *mux.Router, statics config.StaticDirMappings, next http.Handler) { +func (m *RequestHandler) makeStaticRoutes(router *mux.Router, statics config.StaticDirs, next http.Handler) { for _, staticDir := range statics { clearPath := strings.TrimSuffix(staticDir.Path, "/") path := clearPath + "/" diff --git a/internal/handler/uncors_handler.go b/internal/handler/uncors_handler.go index 0fdbb686..0095c76a 100644 --- a/internal/handler/uncors_handler.go +++ b/internal/handler/uncors_handler.go @@ -23,7 +23,7 @@ type RequestHandler struct { router *mux.Router fs afero.Fs logger contracts.Logger - mappings []config.URLMapping + mappings config.Mappings replacerFactory contracts.URLReplacerFactory httpClient contracts.HTTPClient } @@ -33,7 +33,7 @@ var errHostNotMapped = errors.New("host not mapped") func NewUncorsRequestHandler(options ...UncorsRequestHandlerOption) *RequestHandler { handler := &RequestHandler{ router: mux.NewRouter(), - mappings: []config.URLMapping{}, + mappings: config.Mappings{}, } for _, option := range options { diff --git a/internal/handler/uncors_handler_options.go b/internal/handler/uncors_handler_options.go index 7227d7c2..ac3bdaec 100644 --- a/internal/handler/uncors_handler_options.go +++ b/internal/handler/uncors_handler_options.go @@ -32,7 +32,7 @@ func WithHTTPClient(client contracts.HTTPClient) UncorsRequestHandlerOption { } } -func WithMappings(mappings []config.URLMapping) UncorsRequestHandlerOption { +func WithMappings(mappings config.Mappings) UncorsRequestHandlerOption { return func(m *RequestHandler) { m.mappings = mappings } diff --git a/internal/handler/uncors_handler_test.go b/internal/handler/uncors_handler_test.go index 4eae598d..58682010 100644 --- a/internal/handler/uncors_handler_test.go +++ b/internal/handler/uncors_handler_test.go @@ -54,11 +54,11 @@ func TestUncorsRequestHandler(t *testing.T) { "/mock.json": mockJSON, }) - mappings := []config.URLMapping{ + mappings := []config.Mapping{ { From: localhost, To: localhostSecure, - Statics: []config.StaticDirMapping{ + Statics: []config.StaticDir{ {Dir: "/assets", Path: "/cc/", Index: indexHTML}, {Dir: "/assets", Path: "/pnp/", Index: "index.php"}, {Dir: "/images", Path: "/img/"}, @@ -296,7 +296,7 @@ func TestMockMiddleware(t *testing.T) { handler.WithHTTPClient(mocks.NewHTTPClientMock(t)), handler.WithURLReplacerFactory(mocks.NewURLReplacerFactoryMock(t)), handler.WithLogger(logger), - handler.WithMappings([]config.URLMapping{ + handler.WithMappings([]config.Mapping{ // TODO: add hosts {From: "*", To: "*", Mocks: []config.Mock{{ Path: "/api", @@ -335,7 +335,7 @@ func TestMockMiddleware(t *testing.T) { t.Run("where method is set", func(t *testing.T) { expectedCode := 299 expectedBody := "forwarded" - mappings := []config.URLMapping{ + mappings := []config.Mapping{ {From: "*", To: "*", Mocks: []config.Mock{{ Path: "/api", Method: http.MethodPut, @@ -411,7 +411,7 @@ func TestMockMiddleware(t *testing.T) { t.Run("path handling", func(t *testing.T) { expectedCode := 299 expectedBody := "forwarded" - mappings := []config.URLMapping{ + mappings := []config.Mapping{ {From: "*", To: "*", Mocks: []config.Mock{ { Path: userPath, @@ -522,7 +522,7 @@ func TestMockMiddleware(t *testing.T) { handler.WithHTTPClient(mocks.NewHTTPClientMock(t)), handler.WithURLReplacerFactory(mocks.NewURLReplacerFactoryMock(t)), handler.WithLogger(logger), - handler.WithMappings([]config.URLMapping{ + handler.WithMappings([]config.Mapping{ {From: "*", To: "*", Mocks: []config.Mock{ { Path: userPath, @@ -618,7 +618,7 @@ func TestMockMiddleware(t *testing.T) { handler.WithHTTPClient(mocks.NewHTTPClientMock(t)), handler.WithURLReplacerFactory(mocks.NewURLReplacerFactoryMock(t)), handler.WithLogger(logger), - handler.WithMappings([]config.URLMapping{ + handler.WithMappings([]config.Mapping{ {From: "*", To: "*", Mocks: []config.Mock{ { Path: userPath, diff --git a/internal/helpers/mappings.go b/internal/helpers/mappings.go index be8506f4..6ce227f4 100644 --- a/internal/helpers/mappings.go +++ b/internal/helpers/mappings.go @@ -22,13 +22,8 @@ const ( defaultHTTPSPort = 443 ) -func NormaliseMappings( - mappings []config.URLMapping, - httpPort, - httpsPort int, - useHTTPS bool, -) ([]config.URLMapping, error) { - var processedMappings []config.URLMapping +func NormaliseMappings(mappings config.Mappings, httpPort, httpsPort int, useHTTPS bool) (config.Mappings, error) { + var processedMappings config.Mappings for _, mapping := range mappings { sourceURL, err := urlx.Parse(mapping.From) if err != nil { diff --git a/internal/helpers/mappings_test.go b/internal/helpers/mappings_test.go index b0c1c25d..e74d3e2e 100644 --- a/internal/helpers/mappings_test.go +++ b/internal/helpers/mappings_test.go @@ -15,16 +15,16 @@ func TestNormaliseMappings(t *testing.T) { httpPort, httpsPort := 3000, 3001 testsCases := []struct { name string - mappings []config.URLMapping - expected []config.URLMapping + mappings []config.Mapping + expected []config.Mapping useHTTPS bool }{ { name: "correctly set http and https ports", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "localhost", To: "github.com"}, }, - expected: []config.URLMapping{ + expected: []config.Mapping{ {From: "http://localhost:3000", To: "github.com"}, {From: "https://localhost:3001", To: "github.com"}, }, @@ -32,33 +32,33 @@ func TestNormaliseMappings(t *testing.T) { }, { name: "correctly set http port", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "http://localhost", To: "https://github.com"}, }, - expected: []config.URLMapping{ + expected: []config.Mapping{ {From: "http://localhost:3000", To: "https://github.com"}, }, useHTTPS: true, }, { name: "correctly set https port", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "https://localhost", To: "https://github.com"}, }, - expected: []config.URLMapping{ + expected: []config.Mapping{ {From: "https://localhost:3001", To: "https://github.com"}, }, useHTTPS: true, }, { name: "correctly set mixed schemes", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "host1", To: "https://github.com"}, {From: "host2", To: "http://github.com"}, {From: "http://host3", To: "http://api.github.com"}, {From: "https://host4", To: "https://api.github.com"}, }, - expected: []config.URLMapping{ + expected: []config.Mapping{ {From: "http://host1:3000", To: "https://github.com"}, {From: "https://host1:3001", To: "https://github.com"}, {From: "http://host2:3000", To: "http://github.com"}, @@ -88,16 +88,16 @@ func TestNormaliseMappings(t *testing.T) { httpPort, httpsPort := 80, 443 testsCases := []struct { name string - mappings []config.URLMapping - expected []config.URLMapping + mappings []config.Mapping + expected []config.Mapping useHTTPS bool }{ { name: "correctly set http and https ports", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "localhost", To: "github.com"}, }, - expected: []config.URLMapping{ + expected: []config.Mapping{ {From: "http://localhost", To: "github.com"}, {From: "https://localhost", To: "github.com"}, }, @@ -105,33 +105,33 @@ func TestNormaliseMappings(t *testing.T) { }, { name: "correctly set http port", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "http://localhost", To: "https://github.com"}, }, - expected: []config.URLMapping{ + expected: []config.Mapping{ {From: "http://localhost", To: "https://github.com"}, }, useHTTPS: true, }, { name: "correctly set https port", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "https://localhost", To: "https://github.com"}, }, - expected: []config.URLMapping{ + expected: []config.Mapping{ {From: "https://localhost", To: "https://github.com"}, }, useHTTPS: true, }, { name: "correctly set mixed schemes", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "host1", To: "https://github.com"}, {From: "host2", To: "http://github.com"}, {From: "http://host3", To: "http://api.github.com"}, {From: "https://host4", To: "https://api.github.com"}, }, - expected: []config.URLMapping{ + expected: []config.Mapping{ {From: "http://host1", To: "https://github.com"}, {From: "https://host1", To: "https://github.com"}, {From: "http://host2", To: "http://github.com"}, @@ -160,7 +160,7 @@ func TestNormaliseMappings(t *testing.T) { t.Run("incorrect mappings", func(t *testing.T) { testsCases := []struct { name string - mappings []config.URLMapping + mappings []config.Mapping httpPort int httpsPort int useHTTPS bool @@ -168,7 +168,7 @@ func TestNormaliseMappings(t *testing.T) { }{ { name: "incorrect source url", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "loca^host", To: "github.com"}, }, httpPort: 3000, @@ -178,7 +178,7 @@ func TestNormaliseMappings(t *testing.T) { }, { name: "incorrect port in source url", - mappings: []config.URLMapping{ + mappings: []config.Mapping{ {From: "localhost:", To: "github.com"}, }, httpPort: -1, diff --git a/internal/urlreplacer/factory.go b/internal/urlreplacer/factory.go index 3ae2d83c..49a389d2 100644 --- a/internal/urlreplacer/factory.go +++ b/internal/urlreplacer/factory.go @@ -24,7 +24,7 @@ var ( ErrMappingNotSpecified = errors.New("you must specify at least one mapping") ) -func NewURLReplacerFactory(urlMappings []config.URLMapping) (*Factory, error) { +func NewURLReplacerFactory(urlMappings config.Mappings) (*Factory, error) { if len(urlMappings) < 1 { return nil, ErrMappingNotSpecified } diff --git a/internal/urlreplacer/factory_test.go b/internal/urlreplacer/factory_test.go index 93e27ae8..9ea0d8a5 100644 --- a/internal/urlreplacer/factory_test.go +++ b/internal/urlreplacer/factory_test.go @@ -15,21 +15,21 @@ func TestNewUrlReplacerFactory(t *testing.T) { t.Run("should return error when", func(t *testing.T) { tests := []struct { name string - mapping []config.URLMapping + mapping []config.Mapping }{ { name: "mappings is empty", - mapping: make([]config.URLMapping, 0), + mapping: make([]config.Mapping, 0), }, { name: "source url is incorrect", - mapping: []config.URLMapping{ + mapping: []config.Mapping{ {From: string(rune(0x7f)), To: "https://github.com"}, }, }, { name: "target url is incorrect ", - mapping: []config.URLMapping{ + mapping: []config.Mapping{ {From: "localhost", To: string(rune(0x7f))}, }, }, @@ -45,7 +45,7 @@ func TestNewUrlReplacerFactory(t *testing.T) { }) t.Run("should return replacers", func(t *testing.T) { - actual, err := urlreplacer.NewURLReplacerFactory([]config.URLMapping{ + actual, err := urlreplacer.NewURLReplacerFactory([]config.Mapping{ {From: "localhost", To: "https://github.com"}, }) @@ -55,7 +55,7 @@ func TestNewUrlReplacerFactory(t *testing.T) { } func TestFactoryMake(t *testing.T) { - factory, err := urlreplacer.NewURLReplacerFactory([]config.URLMapping{ + factory, err := urlreplacer.NewURLReplacerFactory([]config.Mapping{ {From: "http://server1.com", To: "https://mappedserver1.com"}, {From: "https://server2.com", To: "https://mappedserver2.com"}, }) diff --git a/main.go b/main.go index 0328c1d2..3d9569cd 100644 --- a/main.go +++ b/main.go @@ -117,7 +117,7 @@ func main() { log.Print("\n") log.Warning(ui.DisclaimerMessage) log.Print("\n") - log.Info(ui.Mappings(mappings)) + log.Info(mappings.String()) log.Print("\n") go version.CheckNewVersion(ctx, httpClient, Version) From 7ec31f68ea496113e674498bb93e6d05403ace10 Mon Sep 17 00:00:00 2001 From: Evgeny Abramovich Date: Sun, 21 May 2023 12:15:42 -0300 Subject: [PATCH 4/9] Fixed lint issues --- internal/config/helpers.go | 4 +--- internal/config/mappings_test.go | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/config/helpers.go b/internal/config/helpers.go index d91027e5..a464bd23 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -2,13 +2,11 @@ package config import ( "errors" - "github.com/evg4b/uncors/internal/config/hooks" "strings" + "github.com/evg4b/uncors/internal/config/hooks" "github.com/mitchellh/mapstructure" - "github.com/samber/lo" - "github.com/spf13/viper" ) diff --git a/internal/config/mappings_test.go b/internal/config/mappings_test.go index a94a220a..53f1152b 100644 --- a/internal/config/mappings_test.go +++ b/internal/config/mappings_test.go @@ -2,9 +2,10 @@ package config_test import ( - "github.com/evg4b/uncors/internal/config" "testing" + "github.com/evg4b/uncors/internal/config" + "github.com/stretchr/testify/assert" ) From ed2506536113ed9a5347f2a5cf41892ab0032753 Mon Sep 17 00:00:00 2001 From: Evgeny Abramovich Date: Sun, 21 May 2023 14:12:21 -0300 Subject: [PATCH 5/9] Updated mapping formatting --- internal/config/mapping.go | 6 +++++ internal/config/mappings.go | 45 +++++++++++++++++++------------- internal/config/mappings_test.go | 40 ++++++++++++++++++++++------ internal/config/model.go | 20 ++++++++++++++ 4 files changed, 85 insertions(+), 26 deletions(-) diff --git a/internal/config/mapping.go b/internal/config/mapping.go index 1d5bec1f..9b679106 100644 --- a/internal/config/mapping.go +++ b/internal/config/mapping.go @@ -24,6 +24,12 @@ func (u Mapping) Clone() Mapping { return item.Clone() }) }), + Mocks: lo.If(u.Mocks == nil, []Mock(nil)). + ElseF(func() []Mock { + return lo.Map(u.Mocks, func(item Mock, index int) Mock { + return item.Clone() + }) + }), } } diff --git a/internal/config/mappings.go b/internal/config/mappings.go index 2de8e272..ac1369b2 100644 --- a/internal/config/mappings.go +++ b/internal/config/mappings.go @@ -3,6 +3,9 @@ package config import ( "fmt" "strings" + + "github.com/evg4b/uncors/pkg/urlx" + "github.com/samber/lo" ) type Mappings []Mapping @@ -10,26 +13,18 @@ type Mappings []Mapping func (mappings Mappings) String() string { var builder strings.Builder - for _, mapping := range mappings { - if strings.HasPrefix(mapping.From, "https:") { - builder.WriteString(fmt.Sprintf("PROXY: %s => %s\n", mapping.From, mapping.To)) - for _, static := range mapping.Statics { - builder.WriteString(fmt.Sprintf(" static: %s => %s\n", static.Path, static.Dir)) - } - } - if len(mapping.Mocks) > 0 { - builder.WriteString(fmt.Sprintf("MOCKS: %d mock(s) registered", len(mapping.Mocks))) + groups := lo.GroupBy(mappings, extractHost) + for _, group := range groups { + for _, mapping := range group { + builder.WriteString(fmt.Sprintf("%s => %s\n", mapping.From, mapping.To)) } - } - for _, mapping := range mappings { - if strings.HasPrefix(mapping.From, "http:") { - builder.WriteString(fmt.Sprintf("PROXY: %s => %s\n", mapping.From, mapping.To)) - for _, static := range mapping.Statics { - builder.WriteString(fmt.Sprintf(" static: %s => %s\n", static.Path, static.Dir)) - } + + mapping := group[0] + for _, mock := range mapping.Mocks { + builder.WriteString(fmt.Sprintf(" mock: [%s %d] %s\n", mock.Method, mock.Response.Code, mock.Path)) } - if len(mapping.Mocks) > 0 { - builder.WriteString(fmt.Sprintf("MOCKS: %d mock(s) registered", len(mapping.Mocks))) + for _, static := range mapping.Statics { + builder.WriteString(fmt.Sprintf(" static: %s => %s\n", static.Path, static.Dir)) } } @@ -37,3 +32,17 @@ func (mappings Mappings) String() string { return builder.String() } + +func extractHost(item Mapping) string { + uri, err := urlx.Parse(item.From) + if err != nil { + panic(err) + } + + host, _, err := urlx.SplitHostPort(uri) + if err != nil { + panic(err) + } + + return host +} diff --git a/internal/config/mappings_test.go b/internal/config/mappings_test.go index 53f1152b..4f2d28f1 100644 --- a/internal/config/mappings_test.go +++ b/internal/config/mappings_test.go @@ -24,7 +24,7 @@ func TestMappings(t *testing.T) { mappings: config.Mappings{ {From: "http://localhost", To: "https://github.com"}, }, - expected: []string{"PROXY: http://localhost => https://github.com"}, + expected: []string{"http://localhost => https://github.com"}, }, { name: "http and https mappings", @@ -33,21 +33,45 @@ func TestMappings(t *testing.T) { {From: "https://localhost", To: "https://github.com"}, }, expected: []string{ - "PROXY: https://localhost => https://github.com", - "PROXY: http://localhost => https://github.com", + "https://localhost => https://github.com", + "http://localhost => https://github.com", }, }, { name: "mapping and mocks", mappings: config.Mappings{ - {From: "http://localhost", To: "https://github.com", Mocks: []config.Mock{ - {}, {}, {}, - }}, + { + From: "http://localhost", + To: "https://github.com", + Mocks: []config.Mock{ + { + Path: "", + Method: "", + Queries: nil, + Headers: nil, + Response: config.Response{}, + }, + { + Path: "", + Method: "", + Queries: nil, + Headers: nil, + Response: config.Response{}, + }, + { + Path: "", + Method: "", + Queries: nil, + Headers: nil, + Response: config.Response{}, + }, + }, + }, {From: "https://localhost", To: "https://github.com"}, }, expected: []string{ - "PROXY: https://localhost => https://github.com", - "PROXY: http://localhost => https://github.com", + "https://localhost => https://github.com", + "http://localhost => https://github.com", "MOCKS: 3 mock(s) registered", }, }, diff --git a/internal/config/model.go b/internal/config/model.go index 97d71838..5989ed88 100644 --- a/internal/config/model.go +++ b/internal/config/model.go @@ -12,6 +12,16 @@ type Response struct { Delay time.Duration `mapstructure:"delay"` } +func (r Response) Clone() Response { + return Response{ + Code: r.Code, + Headers: r.Headers, + RawContent: r.RawContent, + File: r.File, + Delay: r.Delay, + } +} + type Mock struct { Path string `mapstructure:"path"` Method string `mapstructure:"method"` @@ -19,3 +29,13 @@ type Mock struct { Headers map[string]string `mapstructure:"headers"` Response Response `mapstructure:"response"` } + +func (m Mock) Clone() Mock { + return Mock{ + Path: m.Path, + Method: m.Method, + Queries: m.Queries, + Headers: m.Headers, + Response: m.Response.Clone(), + } +} From 227ee2178544033ae3d0802ebd9042139ab87be7 Mon Sep 17 00:00:00 2001 From: Evgeny Abramovich Date: Mon, 22 May 2023 07:54:45 -0300 Subject: [PATCH 6/9] Fixed tests --- internal/config/mappings_test.go | 44 ++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/internal/config/mappings_test.go b/internal/config/mappings_test.go index 4f2d28f1..7f3846e5 100644 --- a/internal/config/mappings_test.go +++ b/internal/config/mappings_test.go @@ -2,6 +2,7 @@ package config_test import ( + "net/http" "testing" "github.com/evg4b/uncors/internal/config" @@ -45,25 +46,34 @@ func TestMappings(t *testing.T) { To: "https://github.com", Mocks: []config.Mock{ { - Path: "", - Method: "", - Queries: nil, - Headers: nil, - Response: config.Response{}, + Path: "/endpoint-1", + Method: http.MethodPost, + Response: config.Response{ + Code: http.StatusOK, + RawContent: "OK", + }, }, { - Path: "", - Method: "", - Queries: nil, - Headers: nil, - Response: config.Response{}, + Path: "/demo", + Method: http.MethodGet, + Queries: map[string]string{ + "param1": "value1", + }, + Response: config.Response{ + Code: http.StatusInternalServerError, + RawContent: "ERROR", + }, }, { - Path: "", - Method: "", - Queries: nil, - Headers: nil, - Response: config.Response{}, + Path: "/healthcheck", + Method: http.MethodGet, + Headers: map[string]string{ + "param1": "value1", + }, + Response: config.Response{ + Code: http.StatusForbidden, + RawContent: "ERROR", + }, }, }, }, @@ -72,7 +82,9 @@ func TestMappings(t *testing.T) { expected: []string{ "https://localhost => https://github.com", "http://localhost => https://github.com", - "MOCKS: 3 mock(s) registered", + "mock: [POST 200] /endpoint-1", + "mock: [GET 500] /demo", + "mock: [GET 403] /healthcheck", }, }, } From 21113327a5bc52a1735c2cf17aa22057bc43456d Mon Sep 17 00:00:00 2001 From: Evgeny Abramovich Date: Sat, 27 May 2023 11:16:29 -0300 Subject: [PATCH 7/9] Added CloneMap helper --- internal/helpers/clone.go | 16 +++++ internal/helpers/clone_test.go | 117 +++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 internal/helpers/clone.go create mode 100644 internal/helpers/clone_test.go diff --git a/internal/helpers/clone.go b/internal/helpers/clone.go new file mode 100644 index 00000000..e4f7ef25 --- /dev/null +++ b/internal/helpers/clone.go @@ -0,0 +1,16 @@ +package helpers + +import "github.com/samber/lo" + +func CloneMap[K comparable, V any](data map[K]V) map[K]V { + cloned := make(map[K]V, len(data)) + for key, value := range data { + if cloneable, ok := any(value).(lo.Clonable[V]); ok { + cloned[key] = cloneable.Clone() + } else { + cloned[key] = value + } + } + + return cloned +} diff --git a/internal/helpers/clone_test.go b/internal/helpers/clone_test.go new file mode 100644 index 00000000..f288a85c --- /dev/null +++ b/internal/helpers/clone_test.go @@ -0,0 +1,117 @@ +package helpers_test + +import ( + "testing" + "time" + + "github.com/evg4b/uncors/internal/helpers" + + "github.com/stretchr/testify/assert" +) + +type clonableTestStruct struct{ Value string } + +func (t clonableTestStruct) Clone() clonableTestStruct { + return clonableTestStruct{Value: "Cloned:" + t.Value} +} + +type nonClonableTestStruct struct{ Value string } + +func TestCloneMap(t *testing.T) { + t.Run("base types", func(t *testing.T) { + t.Run("clone map[string]string", func(t *testing.T) { + assertClone(t, map[string]string{ + "1": "2", + "2": "3", + "3": "4", + "4": "1", + }) + }) + + t.Run("clone map[string]int", func(t *testing.T) { + assertClone(t, map[string]int{ + "1": 2, + "2": 3, + "3": 4, + "4": 1, + }) + }) + + t.Run("clone map[string]any", func(t *testing.T) { + assertClone(t, map[string]any{ + "1": 2, + "2": "2", + "3": time.Hour, + "4": []int{1, 2, 3}, + }) + }) + + t.Run("clone map[int]string", func(t *testing.T) { + assertClone(t, map[int]string{ + 1: "2", + 2: "3", + 3: "4", + 4: "1", + }) + }) + + t.Run("clone map[int]int", func(t *testing.T) { + assertClone(t, map[int]int{ + 1: 2, + 2: 3, + 3: 4, + 4: 1, + }) + }) + + t.Run("clone map[int]any", func(t *testing.T) { + assertClone(t, map[int]any{ + 1: 2, + 2: "2", + 3: time.Hour, + 4: []int{1, 2, 3}, + }) + }) + }) + + t.Run("clonable objects", func(t *testing.T) { + data := map[string]clonableTestStruct{ + "1": {Value: "demo"}, + "2": {Value: "demo"}, + "3": {Value: "demo"}, + } + + expected := map[string]clonableTestStruct{ + "1": {Value: "Cloned:demo"}, + "2": {Value: "Cloned:demo"}, + "3": {Value: "Cloned:demo"}, + } + + actual := helpers.CloneMap(data) + + assert.NotSame(t, &data, &actual) + assert.EqualValues(t, &expected, &actual) + }) + + t.Run("non clonable objects", func(t *testing.T) { + data := map[string]nonClonableTestStruct{ + "1": {Value: "demo"}, + "2": {Value: "demo"}, + "3": {Value: "demo"}, + } + + actual := helpers.CloneMap(data) + + assert.NotSame(t, &data, &actual) + assert.EqualValues(t, &data, &actual) + }) +} + +func assertClone[K comparable, V any](t *testing.T, data map[K]V) { + t.Helper() + + actual := helpers.CloneMap(data) + + assert.EqualValues(t, data, actual) + assert.NotSame(t, &data, &actual) +} From 08b97ec45cc6cbd3d0808121e5a1d8b4e0f6cb9d Mon Sep 17 00:00:00 2001 From: Evgeny Abramovich Date: Sat, 27 May 2023 11:17:07 -0300 Subject: [PATCH 8/9] Move NormaliseMappings to config package --- internal/config/helpers.go | 57 +++++++++++++++ .../helpers_test.go} | 9 ++- internal/helpers/mappings.go | 69 ------------------- main.go | 4 +- 4 files changed, 62 insertions(+), 77 deletions(-) rename internal/{helpers/mappings_test.go => config/helpers_test.go} (96%) delete mode 100644 internal/helpers/mappings.go diff --git a/internal/config/helpers.go b/internal/config/helpers.go index a464bd23..65f9d335 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -2,8 +2,14 @@ package config import ( "errors" + "fmt" + "net" + "net/url" + "strconv" "strings" + "github.com/evg4b/uncors/pkg/urlx" + "github.com/evg4b/uncors/internal/config/hooks" "github.com/mitchellh/mapstructure" "github.com/samber/lo" @@ -67,3 +73,54 @@ func decodeConfig[T any](data any, mapping *T, decodeFuncs ...mapstructure.Decod return err //nolint:wrapcheck } + +const ( + httpScheme = "http" + httpsScheme = "https" +) + +func NormaliseMappings(mappings Mappings, httpPort, httpsPort int, useHTTPS bool) (Mappings, error) { + var processedMappings Mappings + for _, mapping := range mappings { + sourceURL, err := urlx.Parse(mapping.From) + if err != nil { + return nil, fmt.Errorf("failed to parse source url: %w", err) + } + + if isApplicableScheme(sourceURL.Scheme, httpScheme) { + httpMapping := mapping.Clone() + httpMapping.From = assignPortAndScheme(*sourceURL, httpScheme, httpPort) + processedMappings = append(processedMappings, httpMapping) + } + + if useHTTPS && isApplicableScheme(sourceURL.Scheme, httpsScheme) { + httpsMapping := mapping.Clone() + httpsMapping.From = assignPortAndScheme(*sourceURL, httpsScheme, httpsPort) + processedMappings = append(processedMappings, httpsMapping) + } + } + + return processedMappings, nil +} + +func assignPortAndScheme(parsedURL url.URL, scheme string, port int) string { + host, _, _ := urlx.SplitHostPort(&parsedURL) + parsedURL.Scheme = scheme + + if !(isDefaultPort(scheme, port)) { + parsedURL.Host = net.JoinHostPort(host, strconv.Itoa(port)) + } else { + parsedURL.Host = host + } + + return parsedURL.String() +} + +func isDefaultPort(scheme string, port int) bool { + return strings.EqualFold(httpScheme, scheme) && port == defaultHTTPPort || + strings.EqualFold(httpsScheme, scheme) && port == defaultHTTPSPort +} + +func isApplicableScheme(scheme, expectedScheme string) bool { + return strings.EqualFold(scheme, expectedScheme) || len(scheme) == 0 +} diff --git a/internal/helpers/mappings_test.go b/internal/config/helpers_test.go similarity index 96% rename from internal/helpers/mappings_test.go rename to internal/config/helpers_test.go index e74d3e2e..1a377652 100644 --- a/internal/helpers/mappings_test.go +++ b/internal/config/helpers_test.go @@ -1,11 +1,10 @@ // nolint: dupl -package helpers_test +package config_test import ( "testing" "github.com/evg4b/uncors/internal/config" - "github.com/evg4b/uncors/internal/helpers" "github.com/stretchr/testify/assert" ) @@ -71,7 +70,7 @@ func TestNormaliseMappings(t *testing.T) { } for _, testCase := range testsCases { t.Run(testCase.name, func(t *testing.T) { - actual, err := helpers.NormaliseMappings( + actual, err := config.NormaliseMappings( testCase.mappings, httpPort, httpsPort, @@ -144,7 +143,7 @@ func TestNormaliseMappings(t *testing.T) { } for _, testCase := range testsCases { t.Run(testCase.name, func(t *testing.T) { - actual, err := helpers.NormaliseMappings( + actual, err := config.NormaliseMappings( testCase.mappings, httpPort, httpsPort, @@ -189,7 +188,7 @@ func TestNormaliseMappings(t *testing.T) { } for _, testCase := range testsCases { t.Run(testCase.name, func(t *testing.T) { - _, err := helpers.NormaliseMappings( + _, err := config.NormaliseMappings( testCase.mappings, testCase.httpPort, testCase.httpsPort, diff --git a/internal/helpers/mappings.go b/internal/helpers/mappings.go deleted file mode 100644 index 6ce227f4..00000000 --- a/internal/helpers/mappings.go +++ /dev/null @@ -1,69 +0,0 @@ -package helpers - -import ( - "fmt" - "net" - "net/url" - "strconv" - "strings" - - "github.com/evg4b/uncors/internal/config" - - "github.com/evg4b/uncors/pkg/urlx" -) - -const ( - httpScheme = "http" - defaultHTTPPort = 80 -) - -const ( - httpsScheme = "https" - defaultHTTPSPort = 443 -) - -func NormaliseMappings(mappings config.Mappings, httpPort, httpsPort int, useHTTPS bool) (config.Mappings, error) { - var processedMappings config.Mappings - for _, mapping := range mappings { - sourceURL, err := urlx.Parse(mapping.From) - if err != nil { - return nil, fmt.Errorf("failed to parse source url: %w", err) - } - - if isApplicableScheme(sourceURL.Scheme, httpScheme) { - httpMapping := mapping.Clone() - httpMapping.From = assignPortAndScheme(*sourceURL, httpScheme, httpPort) - processedMappings = append(processedMappings, httpMapping) - } - - if useHTTPS && isApplicableScheme(sourceURL.Scheme, httpsScheme) { - httpsMapping := mapping.Clone() - httpsMapping.From = assignPortAndScheme(*sourceURL, httpsScheme, httpsPort) - processedMappings = append(processedMappings, httpsMapping) - } - } - - return processedMappings, nil -} - -func assignPortAndScheme(parsedURL url.URL, scheme string, port int) string { - host, _, _ := urlx.SplitHostPort(&parsedURL) - parsedURL.Scheme = scheme - - if !(isDefaultPort(scheme, port)) { - parsedURL.Host = net.JoinHostPort(host, strconv.Itoa(port)) - } else { - parsedURL.Host = host - } - - return parsedURL.String() -} - -func isDefaultPort(scheme string, port int) bool { - return strings.EqualFold(httpScheme, scheme) && port == defaultHTTPPort || - strings.EqualFold(httpsScheme, scheme) && port == defaultHTTPSPort -} - -func isApplicableScheme(scheme, expectedScheme string) bool { - return strings.EqualFold(scheme, expectedScheme) || len(scheme) == 0 -} diff --git a/main.go b/main.go index 3d9569cd..67d0c58e 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,6 @@ import ( "github.com/evg4b/uncors/internal/handler" - "github.com/evg4b/uncors/internal/helpers" - "github.com/evg4b/uncors/internal/version" "github.com/evg4b/uncors/internal/server" @@ -60,7 +58,7 @@ func main() { log.Debug("Enabled debug messages") } - mappings, err := helpers.NormaliseMappings( + mappings, err := config.NormaliseMappings( uncorsConfig.Mappings, uncorsConfig.HTTPPort, uncorsConfig.HTTPSPort, From 5a5acad5eb514410b5bd1c9bfad2d92367654e04 Mon Sep 17 00:00:00 2001 From: Evgeny Abramovich Date: Sat, 27 May 2023 11:32:06 -0300 Subject: [PATCH 9/9] Added tests for config models --- internal/config/model.go | 8 ++-- internal/config/model_test.go | 79 ++++++++++++++++++++++++++++++++++ internal/helpers/clone.go | 4 ++ internal/helpers/clone_test.go | 6 +++ 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 internal/config/model_test.go diff --git a/internal/config/model.go b/internal/config/model.go index 5989ed88..3d5e59cf 100644 --- a/internal/config/model.go +++ b/internal/config/model.go @@ -2,6 +2,8 @@ package config import ( "time" + + "github.com/evg4b/uncors/internal/helpers" ) type Response struct { @@ -15,7 +17,7 @@ type Response struct { func (r Response) Clone() Response { return Response{ Code: r.Code, - Headers: r.Headers, + Headers: helpers.CloneMap(r.Headers), RawContent: r.RawContent, File: r.File, Delay: r.Delay, @@ -34,8 +36,8 @@ func (m Mock) Clone() Mock { return Mock{ Path: m.Path, Method: m.Method, - Queries: m.Queries, - Headers: m.Headers, + Queries: helpers.CloneMap(m.Queries), + Headers: helpers.CloneMap(m.Headers), Response: m.Response.Clone(), } } diff --git a/internal/config/model_test.go b/internal/config/model_test.go new file mode 100644 index 00000000..2c1eb65b --- /dev/null +++ b/internal/config/model_test.go @@ -0,0 +1,79 @@ +package config_test + +import ( + "net/http" + "testing" + "time" + + "github.com/evg4b/uncors/internal/config" + "github.com/go-http-utils/headers" + "github.com/stretchr/testify/assert" +) + +func TestResponseClone(t *testing.T) { + object := config.Response{ + Code: http.StatusOK, + Headers: map[string]string{ + headers.ContentType: "plain/text", + headers.CacheControl: "none", + }, + RawContent: "this is plain text", + File: "~/projects/uncors/response/demo.json", + Delay: time.Hour, + } + + actual := object.Clone() + + t.Run("not same", func(t *testing.T) { + assert.NotSame(t, &object, &actual) + }) + + t.Run("equals values", func(t *testing.T) { + assert.EqualValues(t, object, actual) + }) + + t.Run("not same Headers map", func(t *testing.T) { + assert.NotSame(t, &object.Headers, &actual.Headers) + }) +} + +func TestMockClone(t *testing.T) { + object := config.Mock{ + Path: "/constants", + Method: http.MethodGet, + Queries: map[string]string{ + "page": "10", + "size": "50", + }, + Headers: map[string]string{ + headers.ContentType: "plain/text", + headers.CacheControl: "none", + }, + Response: config.Response{ + Code: http.StatusOK, + RawContent: `{ "status": "ok" }`, + }, + } + + actual := object.Clone() + + t.Run("not same", func(t *testing.T) { + assert.NotSame(t, &object, &actual) + }) + + t.Run("equals values", func(t *testing.T) { + assert.EqualValues(t, object, actual) + }) + + t.Run("not same Headers map", func(t *testing.T) { + assert.NotSame(t, &object.Headers, &actual.Headers) + }) + + t.Run("not same Queries map", func(t *testing.T) { + assert.NotSame(t, &object.Headers, &actual.Headers) + }) + + t.Run("not same Response", func(t *testing.T) { + assert.NotSame(t, &object.Response, &actual.Response) + }) +} diff --git a/internal/helpers/clone.go b/internal/helpers/clone.go index e4f7ef25..8ea5858a 100644 --- a/internal/helpers/clone.go +++ b/internal/helpers/clone.go @@ -3,6 +3,10 @@ package helpers import "github.com/samber/lo" func CloneMap[K comparable, V any](data map[K]V) map[K]V { + if data == nil { + return nil + } + cloned := make(map[K]V, len(data)) for key, value := range data { if cloneable, ok := any(value).(lo.Clonable[V]); ok { diff --git a/internal/helpers/clone_test.go b/internal/helpers/clone_test.go index f288a85c..2bb859f1 100644 --- a/internal/helpers/clone_test.go +++ b/internal/helpers/clone_test.go @@ -105,6 +105,12 @@ func TestCloneMap(t *testing.T) { assert.NotSame(t, &data, &actual) assert.EqualValues(t, &data, &actual) }) + + t.Run("nil", func(t *testing.T) { + actual := helpers.CloneMap[string, string](nil) + + assert.Nil(t, actual) + }) } func assertClone[K comparable, V any](t *testing.T, data map[K]V) {