diff --git a/.golangci.yml b/.golangci.yml index cc9e041b..25c9e906 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,3 @@ -run: - skip-dirs: - - testing - - dist linters: enable-all: true disable: @@ -26,16 +22,18 @@ linters: - exhaustruct - wsl - gci - - gofmt - - gofumpt - nolintlint - - tagliatelle - maintidx + - ireturn + - bodyclose linters-settings: varnamelen: ignore-names: - form - to + - ok + - fs + - ca presets: - bugs - comment diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 45109ba7..50cc6674 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,3 +1,5 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json + project_name: uncors before: hooks: diff --git a/README.md b/README.md index 48e6e04b..17ec7bf8 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,20 @@

- A simple dev HTTP/HTTPS proxy for replacing CORS headers. + A simple dev HTTP/HTTPS proxy for replacing CORS headers.

- - Go version - - - GitHub version - + + Go version + + + GitHub version + License - - Coverage + + Coverage Go Report Card @@ -37,14 +37,16 @@ # Core features - CORS header replacement -- HTTPS support -- Wildcard URL request mapping -- Simple request/response mocking -- HTTP/HTTPS proxy support -- *Static file serving ([coming soon...](./ROADMAP.md))* +- [Wildcard host mapping](https://github.com/evg4b/uncors/wiki/2.-Configuration#wilcard-mapping) +- [HTTPS support](https://github.com/evg4b/uncors/wiki/2.-Configuration#https-configuration) +- [Response mocking](https://github.com/evg4b/uncors/wiki/3.-Response-mocksing) +- [HTTP/HTTPS proxy support](https://github.com/evg4b/uncors/wiki/2.-Configuration#proxy-configuration) +- [Static file serving](https://github.com/evg4b/uncors/wiki/4.-Static-file-serving) - *Response caching ([coming soon...](./ROADMAP.md))* -Other new features you can find in [UNCORS roadmap](https://github.com/evg4b/uncors/blob/main/ROADMAP.md) +Other new features you can find in [roadmap](https://github.com/evg4b/uncors/blob/main/ROADMAP.md). + +Full documentation you can found on [wiki pages](https://github.com/evg4b/uncors/wiki). # Quick Install @@ -82,6 +84,22 @@ Via yarn: yarn add uncors --dev ``` +## Docker + +We currently offer images for [Docker](https://hub.docker.com/r/evg4b/uncors) + +```bash +docker run -p 80:3000 evg4b/uncors --from 'http://local.github.com' --to 'https://github.com' +``` + +## Stew (Cross-platform) + +Also, you can install binaris using [Stew](https://github.com/marwanhawari/) with the following commands: + +```bash +stew install evg4b/uncors +``` + ## Binary (Cross-platform) Download the appropriate version for your platform @@ -91,14 +109,6 @@ This works well for shared hosts and other systems where you don’t have a priv Ideally, you should install it somewhere in your `PATH` for easy use. `/usr/local/bin` is the most probable location. -## Docker - -We currently offer images for [Docker](https://hub.docker.com/r/evg4b/uncors) - -```bash -docker run -p 80:3000 evg4b/uncors --from 'http://local.github.com' --to 'https://github.com' -``` - ## Build from source **Prerequisite Tools** @@ -123,140 +133,14 @@ If you are a Windows user, substitute the $HOME environment variable above with # Usage -The following command can be used to start the Uncors proxy server: +The following command can be used to start the UNCORS proxy server: ``` -uncors --http-port 8080 --to 'https://github.com' --from 'http://localhost' +uncors --from 'http://localhost' --to 'https://github.com' --http-port 8080 ``` -## CLI Parameters - -The following command-line parameters can be used to configure the Uncors proxy server: - -* `--from` - Specifies the local host with protocol for the resource from which proxying will take place. -* `--to` - Specifies the target host with protocol for the resource to be proxied. -* `--http-port` or `-p` - Specifies the local HTTP listening port. -* `--https-port` or `-s` - Specifies the local HTTPS listening port. -* `--cert-file` - Specifies the path to the HTTPS certificate file. -* `--key-file` - Specifies the path to the matching certificate private key. -* `--proxy` - Specifies the HTTP/HTTPS proxy to provide requests to the real server (system default is used by default). -* `--config` - Specifies the path to the [configuration file](#configuration-file). -* `--debug` - Enables debug output. - -Any configuration parameters passed via CLI (except for `--from` and `--to`) will override the corresponding -parameters specified in the configuration file. The `--from` and `--to` parameters will add an additional mapping -to the configuration. - -## Configuration file - -Uncors supports a YAML file configuration with the following options: - -```yaml -# Base configuration -http-port: 8080 # Local HTTP listened port. -mappings: - http://localhost:3000: https://githib.com -debug: false # Show debug output. -proxy: localhost:8080 - -# HTTPS configuration -https-port: 8081 # Local HTTPS listened port. -cert-file: ~/server.crt # Path to HTTPS certificate file. -key-file: ~/server.key # Path to matching for certificate private key. - -# Mock definitions are used to generate fake responses for certain endpoints. -mocks: - - path: /hello-word - response: - code: 200 - raw-content: 'Hello word' -``` +More information about configuration and usage you can fiund on [UNCORS wiki](https://github.com/evg4b/uncors/wiki). -#### Mocks configuration - -The mocks configuration section in Uncors allows you to define specific endpoints to be mocked, including the response -data and other parameters. Currently, mocks are defined globally for all mappings. Available path, method, queries, and headers filters, -which utilize the [gorilla/mux route matching system](https://github.com/gorilla/mux#matching-routes). - -Each endpoint mock requires a path parameter, which defines the URL path for the endpoint. You can also use the method -parameter to define a specific HTTP method for the endpoint. - -The queries and headers parameters can be used to specify more detailed URLs that will be mocked. The queries parameter -allows you to define specific query parameters for the URL, while the headers parameter allows you to define specific -HTTP headers. - -Here is the structure of the mock configuration: - -```yaml -mocks: - - path: - method: - queries: - : - # ... - headers: - : - # ... - response: - code: - headers: - : - # ... - delay: - raw-content: - file: -``` +# ⚠️ Caution -- `path` (required) - This property is used to define the URL path that should be mocked. The value should be a string, - such as `/example`. The path can include variables, such as `/users/{id}`, which will match any URL that starts - with `/users/` and has a variable `id` in it. -- `method` (optional) - This property is used to define the HTTP method that should be mocked. - The value should be a string. If this property is not specified, the mock will match any HTTP method. -- `queries` (optional) - This property is used to define specific query parameters that should be matched against the - request URL. The value should be a mapping of query parameters and their values, such as `{"param1": "value1", " - param2": "value2"}`. If this property is not specified, the mock will match any query parameter. -- `headers` (optional): This property is used to define specific HTTP headers that should be matched against the request - headers. The value should be a mapping of header names and their values, such as `{"Content-Type": "application/json"}`. - If this property is not specified, the mock will match any HTTP header. -- `response` (required): This property is used to define the mock response. It should be a mapping that contains the - following properties: - - `code` (optional): This property is used to define the HTTP status code that should be returned in the mock - response. The value should be an integer, such as 200 or 404. If this property is not specified, the mock will use - 200 OK status code. - - `headers` (optional): This property is used to define specific HTTP headers that should be returned in the mock - response. The value should be a mapping of header names and their values, such as `{"Content-Type": " - application/json"}`. If this property is not specified, the mock response will have no extra headers. - - `delay` (optional): This property is used to define a delay before sending the mock response. The value should be a - string in the format ` ...`, where `` is a positive integer and `` is time units. - Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. For example, `1m 30s` would delay the response by 1 minute and 30 - seconds. If this property is not specified, the mock response will be sent immediately. - - `raw-content` (optional): This property is used to define the raw content that should be returned in the mock - response. The value should be a string, such as `Hello, world!`. If this property is not specified, the mock - response will be empty. - - `file` (optional): This property is used to define the path to a file that contains the mock response content. The - file content will be used as the response content. The value should be a string that specifies the file path, such - as `~/mocks/example.json`. If this property is not specified, the mock response will be empty. - -## How it works - -```mermaid -sequenceDiagram - participant Client - participant Uncors - participant Server - - - alt Handling OPTIONS queries - Client ->> Uncors: Access-Control-Request - Uncors ->> Client: Allow-Control-Request - end - - alt Handling Data queries - Client ->> Uncors: GET, POST, PUT... query - Note over Uncors: Replacing url with target
in headers and cookies - Uncors-->>Server: Real GET, POST, PUT... query - Server->>Uncors: Real response - Note over Uncors: Replacing url with source
in headers and cookies - Uncors-->>Client: Data response - end -``` +Please note that removing or replacing CORS headers can pose potential security vulnerabilities. This tool is specifically designed to streamline the development and testing workflow and should not be used in a production environment or as a remote proxy server. It has not undergone a thorough security review, so caution should be exercised when utilizing it. diff --git a/ROADMAP.md b/ROADMAP.md index 97b81234..b3cc2ac3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,14 +2,14 @@ ## [MVP (Pre-release)](https://github.com/evg4b/uncors/releases/tag/v0.0.0) -- [X] Base reverce proxy server functionality +- [X] Base reverse proxy server functionality - [X] Url mapping ## [0.0.1 Release](https://github.com/evg4b/uncors/releases/tag/v0.0.1) -- [X] Cusrom port hadnling -- [X] New URL replcer functionality -- [X] Requset printing +- [X] Custom port handling +- [X] New URL replacer functionality +- [X] Request printing ## [0.0.2 Release](https://github.com/evg4b/uncors/releases/tag/v0.0.2) @@ -25,20 +25,20 @@ ## [0.0.4 Release](https://github.com/evg4b/uncors/releases/tag/v0.0.4) -- [X] Proxy from envairment support +- [X] Proxy from environment support - [X] URL replacer v2 - [PR](https://github.com/evg4b/uncors/pull/2) ## [0.0.6 Release](https://github.com/evg4b/uncors/releases/tag/v0.0.6) - [X] Added [scoop bucket](https://github.com/evg4b/scoop-bucket) support -- [X] Responce mocking v1 - [PR](https://github.com/evg4b/uncors/pull/3) +- [X] Response mocking v1 - [PR](https://github.com/evg4b/uncors/pull/3) ## [0.0.8 Release](https://github.com/evg4b/uncors/releases/tag/v0.0.8) - [X] Disclaimer message -- [X] Mock responce from file +- [X] Mock response from file - [X] Viper configuration -- [X] Multile CLI url mapping +- [X] Multiple CLI url mapping ## [0.0.8-beta Release](https://github.com/evg4b/uncors/releases/tag/v0.0.8-beta) @@ -52,12 +52,14 @@ ## Next Release -- [ ] Separated mock for each url mapping -- [ ] Static file serving +- [X] Static file serving [PR](https://github.com/evg4b/uncors/pull/15) +- [X] Own error page for uncors internal errors +- [ ] Separated mock for each url mapping [PR](https://github.com/evg4b/uncors/pull/16) ## Future features - [ ] Informative error messages - [PR](https://github.com/evg4b/uncors/pull/10) - [ ] Occupied port handling +- [ ] Collecting all request/response to har file - [ ] Response caching - [ ] Content URl replacing (HTML, JSON, TEXT and other) diff --git a/go.mod b/go.mod index 52f6e2a6..d82cfd02 100644 --- a/go.mod +++ b/go.mod @@ -6,45 +6,49 @@ require ( github.com/PuerkitoBio/purell v1.2.0 github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a github.com/go-playground/assert/v2 v2.2.0 - github.com/go-playground/validator/v10 v10.12.0 - github.com/gojuno/minimock/v3 v3.1.2 + github.com/go-playground/validator/v10 v10.14.0 + github.com/gojuno/minimock/v3 v3.1.3 github.com/gorilla/mux v1.8.0 github.com/hashicorp/go-version v1.6.0 github.com/mitchellh/mapstructure v1.5.0 github.com/pseidemann/finish v1.2.0 - github.com/pterm/pterm v0.12.57 + github.com/pterm/pterm v0.12.62 + github.com/samber/lo v1.38.1 github.com/spf13/afero v1.9.5 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.15.0 - github.com/stretchr/testify v1.8.2 - golang.org/x/net v0.8.0 + github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/net v0.10.0 ) require ( atomicgo.dev/cursor v0.1.1 // indirect atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.0.2 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gookit/color v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/leodido/go-urn v1.2.2 // indirect - github.com/lithammer/fuzzysearch v1.1.5 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/pelletier/go-toml/v2 v2.0.7 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 59e026ba..bf3bc0ea 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ atomicgo.dev/cursor v0.1.1 h1:0t9sxQomCTRh5ug+hAMCs59x/UmC9QL6Ci5uosINKD4= atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.0.2 h1:2e/4KY6t3wokja01Cyty6qgkQM8MotJzjtqCH70oX2Q= +atomicgo.dev/schedule v0.0.2/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -74,9 +76,11 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -88,10 +92,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= -github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= -github.com/gojuno/minimock/v3 v3.1.2 h1:T5ZU0pB0VMvTdiWUdWmMKwCbMVA0Z+EqbMuf4CihYX0= -github.com/gojuno/minimock/v3 v3.1.2/go.mod h1:WylRuaQInND/eg0HqP0/6etOdtv67AIfOgPW1z8QtKU= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/gojuno/minimock/v3 v3.1.3 h1:9jakBeOqffZvR9BGBTulphLwiUfiju1w7JspU5eX/fY= +github.com/gojuno/minimock/v3 v3.1.3/go.mod h1:WylRuaQInND/eg0HqP0/6etOdtv67AIfOgPW1z8QtKU= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -170,14 +174,14 @@ github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuOb github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4= -github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ= -github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c= -github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -185,8 +189,8 @@ github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWV github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= -github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -201,28 +205,27 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.56 h1:3mrF0ytaltvWc32inyGD1Xw4Bpa/20gjGry2rImVIUc= -github.com/pterm/pterm v0.12.56/go.mod h1:7rswprkyxYOse1IMh79w42jvReNHxro4z9oHfqjIdzM= -github.com/pterm/pterm v0.12.57 h1:HTjDUmILmh6hIsEidRdpxQAiqcoHCdvRCxIR3KZ0/XE= -github.com/pterm/pterm v0.12.57/go.mod h1:7rswprkyxYOse1IMh79w42jvReNHxro4z9oHfqjIdzM= +github.com/pterm/pterm v0.12.62 h1:Xjj5Wl6UR4Il9xOiDUOZRwReRTdO75if/JdWsn9I59s= +github.com/pterm/pterm v0.12.62/go.mod h1:+c3ujjE7N5qmNx6eKAa7YVSC6m/gCorJJKhzwYTbL90= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= -github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -233,9 +236,10 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= @@ -245,6 +249,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -257,9 +262,10 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -270,7 +276,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -294,6 +301,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -326,8 +335,10 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -347,6 +358,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -386,15 +399,19 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -403,8 +420,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -455,6 +473,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/configuration/config.go b/internal/config/config.go similarity index 62% rename from internal/configuration/config.go rename to internal/config/config.go index dead3bcd..c352aee2 100644 --- a/internal/configuration/config.go +++ b/internal/config/config.go @@ -1,10 +1,9 @@ -package configuration +package config import ( - "fmt" + fmt "fmt" - "github.com/evg4b/uncors/internal/configuration/hooks" - "github.com/evg4b/uncors/internal/middlewares/mock" + "github.com/evg4b/uncors/internal/config/hooks" "github.com/mitchellh/mapstructure" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -16,17 +15,13 @@ const ( ) type UncorsConfig struct { - // Base config_test_data - HTTPPort int `mapstructure:"http-port" validate:"required"` - Mappings map[string]string `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.Mock `mapstructure:"mocks"` + 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 { @@ -44,23 +39,24 @@ func LoadConfiguration(viperInstance *viper.Viper, args []string) (*UncorsConfig } configuration := &UncorsConfig{ - Mappings: map[string]string{}, - Mocks: []mock.Mock{}, + Mappings: []Mapping{}, } - configPath := viperInstance.GetString("config") - if len(configPath) > 0 { + if configPath := viperInstance.GetString("config"); len(configPath) > 0 { viperInstance.SetConfigFile(configPath) if err := viperInstance.ReadInConfig(); err != nil { return nil, fmt.Errorf("filed to read config file '%s': %w", configPath, err) } } + configOption := viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( - hooks.StringToTimeDurationHookFunc(), mapstructure.StringToSliceHookFunc(","), + hooks.StringToTimeDurationHookFunc(), + URLMappingHookFunc(), )) + if err := viperInstance.Unmarshal(configuration, configOption); err != nil { - return nil, fmt.Errorf("filed parsing configuraion: %w", err) + return nil, fmt.Errorf("filed parsing config: %w", err) } if err := readURLMapping(viperInstance, configuration); err != nil { @@ -73,15 +69,15 @@ func LoadConfiguration(viperInstance *viper.Viper, args []string) (*UncorsConfig func defineFlags() *pflag.FlagSet { flags := pflag.NewFlagSet("uncors", pflag.ContinueOnError) flags.Usage = pflag.Usage - flags.StringSlice("to", []string{}, "Target host with protocol for to the resource to be proxy") - flags.StringSlice("from", []string{}, "Local host with protocol for to the resource from which proxying will take place") //nolint: lll + flags.StringSliceP("to", "t", []string{}, "Target host with protocol for to the resource to be proxy") + flags.StringSliceP("from", "f", []string{}, "Local host with protocol for to the resource from which proxying will take place") //nolint: lll flags.UintP("http-port", "p", defaultHTTPPort, "Local HTTP listening port") flags.UintP("https-port", "s", defaultHTTPSPort, "Local HTTPS listening port") flags.String("cert-file", "", "Path to HTTPS certificate file") flags.String("key-file", "", "Path to matching for certificate private key") flags.String("proxy", "", "HTTP/HTTPS proxy to provide requests to real server (used system by default)") flags.Bool("debug", false, "Show debug output") - flags.StringP("config", "c", "", "Show debug output") + flags.StringP("config", "c", "", "Path to the configuration file") return flags } diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 00000000..ec2c3cbf --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,352 @@ +package config_test + +import ( + "testing" + + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/testing/testconstants" + "github.com/evg4b/uncors/testing/testutils" + "github.com/evg4b/uncors/testing/testutils/params" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +const acceptEncoding = "accept-encoding" + +const ( + corruptedConfigPath = "/corrupted-config.yaml" + corruptedConfig = `http-port: 8080 +mappings& + - http://demo: https://demo.com +` +) + +const ( + fullConfigPath = "/full-config.yaml" + fullConfig = ` +http-port: 8080 +mappings: + - http://localhost: https://github.com + - from: http://localhost2 + to: https://stackoverflow.com + mocks: + - path: /demo + method: POST + queries: + foo: bar + headers: + Accept-Encoding: deflate + response: + code: 201 + headers: + Accept-Encoding: deflate + raw: demo + file: /demo.txt +proxy: localhost:8080 +debug: true +https-port: 8081 +cert-file: /etc/certificates/cert-file.pem +key-file: /etc/certificates/key-file.key +` +) + +const ( + incorrectConfigPath = "/incorrect-config.yaml" + incorrectConfig = `http-port: xxx +mappings: + - http://localhost: https://github.com +` +) + +const ( + minimalConfigPath = "/minimal-config.yaml" + minimalConfig = ` +http-port: 8080 +mappings: + - http://localhost: https://github.com +` +) + +func TestLoadConfiguration(t *testing.T) { + viperInstance := viper.New() + viperInstance.SetFs(testutils.FsFromMap(t, map[string]string{ + corruptedConfigPath: corruptedConfig, + fullConfigPath: fullConfig, + incorrectConfigPath: incorrectConfig, + minimalConfigPath: minimalConfig, + })) + + t.Run("correctly parse config", func(t *testing.T) { + tests := []struct { + name string + args []string + expected *config.UncorsConfig + }{ + { + name: "return default config", + args: []string{}, + expected: &config.UncorsConfig{ + HTTPPort: 80, + HTTPSPort: 443, + Mappings: config.Mappings{}, + }, + }, + { + name: "minimal config is set", + args: []string{params.Config, minimalConfigPath}, + expected: &config.UncorsConfig{ + HTTPPort: 8080, + HTTPSPort: 443, + Mappings: config.Mappings{ + {From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub}, + }, + }, + }, + { + name: "read all fields from config file config is set", + args: []string{params.Config, fullConfigPath}, + expected: &config.UncorsConfig{ + HTTPPort: 8080, + Mappings: config.Mappings{ + {From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub}, + { + From: testconstants.HTTPLocalhost2, + To: testconstants.HTTPSStackoverflow, + Mocks: config.Mocks{ + { + Path: "/demo", + Method: "POST", + Queries: map[string]string{ + "foo": "bar", + }, + Headers: map[string]string{ + acceptEncoding: "deflate", + }, + Response: config.Response{ + Code: 201, + Headers: map[string]string{ + acceptEncoding: "deflate", + }, + Raw: "demo", + File: "/demo.txt", + }, + }, + }, + }, + }, + Proxy: "localhost:8080", + Debug: true, + HTTPSPort: 8081, + CertFile: testconstants.CertFilePath, + KeyFile: testconstants.KeyFilePath, + }, + }, + { + name: "read all fields from config file config is set", + args: []string{ + params.Config, fullConfigPath, + params.From, testconstants.SourceHost1, params.To, testconstants.TargetHost1, + params.From, testconstants.SourceHost2, params.To, testconstants.TargetHost2, + params.From, testconstants.SourceHost3, params.To, testconstants.TargetHost3, + }, + expected: &config.UncorsConfig{ + HTTPPort: 8080, + Mappings: config.Mappings{ + {From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub}, + { + From: testconstants.HTTPLocalhost2, + To: testconstants.HTTPSStackoverflow, + Mocks: config.Mocks{ + { + Path: "/demo", + Method: "POST", + Queries: map[string]string{ + "foo": "bar", + }, + Headers: map[string]string{ + acceptEncoding: "deflate", + }, + Response: config.Response{ + Code: 201, + Headers: map[string]string{ + acceptEncoding: "deflate", + }, + Raw: "demo", + File: "/demo.txt", + }, + }, + }, + }, + {From: testconstants.SourceHost1, To: testconstants.TargetHost1}, + {From: testconstants.SourceHost2, To: testconstants.TargetHost2}, + {From: testconstants.SourceHost3, To: testconstants.TargetHost3}, + }, + Proxy: "localhost:8080", + Debug: true, + HTTPSPort: 8081, + CertFile: testconstants.CertFilePath, + KeyFile: testconstants.KeyFilePath, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + uncorsConfig, err := config.LoadConfiguration(viperInstance, testCase.args) + + assert.NoError(t, err) + assert.Equal(t, testCase.expected, uncorsConfig) + }) + } + }) + + t.Run("parse config with error", func(t *testing.T) { + tests := []struct { + name string + args []string + expected []string + }{ + { + name: "incorrect flag provided", + args: []string{ + "--incorrect-flag", + }, + expected: []string{ + "filed parsing flags: unknown flag: --incorrect-flag", + }, + }, + { + name: "return default config", + args: []string{ + params.To, testconstants.TargetHost1, + }, + expected: []string{ + "recognize url mapping: `from` values are not set for every `to`", + }, + }, + { + name: "count of from values great then count of to", + args: []string{ + params.From, testconstants.SourceHost1, params.To, testconstants.TargetHost1, + params.From, testconstants.SourceHost2, + }, + expected: []string{ + "recognize url mapping: `to` values are not set for every `from`", + }, + }, + { + name: "count of to values great then count of from", + args: []string{ + params.From, testconstants.SourceHost1, params.To, testconstants.TargetHost1, + params.To, testconstants.TargetHost2, + }, + expected: []string{ + "recognize url mapping: `from` values are not set for every `to`", + }, + }, + { + name: "config file doesn't exist", + args: []string{ + params.Config, "/not-exist-config.yaml", + }, + expected: []string{ + "filed to read config file '/not-exist-config.yaml': open ", + "open /not-exist-config.yaml: file does not exist", + }, + }, + { + name: "config file is corrupted", + args: []string{ + params.Config, corruptedConfigPath, + }, + expected: []string{ + "filed to read config file '/corrupted-config.yaml': " + + "While parsing config: yaml: line 2: could not find expected ':'", + }, + }, + { + name: "incorrect param type", + args: []string{ + params.HTTPPort, "xxx", + }, + expected: []string{ + "filed parsing flags: invalid argument \"xxx\" for \"-p, --http-port\" flag: " + + "strconv.ParseUint: parsing \"xxx\": invalid syntax", + }, + }, + { + name: "incorrect type in config file", + args: []string{ + params.Config, incorrectConfigPath, + }, + expected: []string{ + "filed parsing config: 1 error(s) decoding:\n\n* cannot parse 'http-port' as int:" + + " strconv.ParseInt: parsing \"xxx\": invalid syntax", + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + uncorsConfig, err := config.LoadConfiguration(viperInstance, testCase.args) + + assert.Nil(t, uncorsConfig) + for _, expected := range testCase.expected { + assert.ErrorContains(t, err, expected) + } + }) + } + }) +} + +func TestUncorsConfigIsHTTPSEnabled(t *testing.T) { + tests := []struct { + name string + config *config.UncorsConfig + expected bool + }{ + { + name: "false by default", + config: &config.UncorsConfig{}, + expected: false, + }, + { + name: "true when https configured", + config: &config.UncorsConfig{ + HTTPSPort: 443, + CertFile: testconstants.CertFilePath, + KeyFile: testconstants.KeyFilePath, + }, + expected: true, + }, + { + name: "false when https port is not configured", + config: &config.UncorsConfig{ + CertFile: testconstants.CertFilePath, + KeyFile: testconstants.KeyFilePath, + }, + expected: false, + }, + { + name: "false when cert file is not configured", + config: &config.UncorsConfig{ + HTTPSPort: 443, + KeyFile: testconstants.KeyFilePath, + }, + expected: false, + }, + { + name: "false when key file is not configured", + config: &config.UncorsConfig{ + HTTPSPort: 443, + CertFile: testconstants.CertFilePath, + }, + expected: false, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + actual := testCase.config.IsHTTPSEnabled() + + assert.Equal(t, testCase.expected, actual) + }) + } +} diff --git a/internal/config/helpers.go b/internal/config/helpers.go new file mode 100644 index 00000000..858163c9 --- /dev/null +++ b/internal/config/helpers.go @@ -0,0 +1,122 @@ +package config + +import ( + "errors" + "fmt" + "net" + "net/url" + "strconv" + "strings" + + "github.com/evg4b/uncors/internal/config/hooks" + "github.com/evg4b/uncors/pkg/urlx" + "github.com/mitchellh/mapstructure" + "github.com/samber/lo" + "github.com/spf13/viper" +) + +var ( + ErrNoToPair = errors.New("`to` values are not set for every `from`") + ErrNoFromPair = errors.New("`from` values are not set for every `to`") +) + +func readURLMapping(config *viper.Viper, configuration *UncorsConfig) error { + from, to := config.GetStringSlice("from"), config.GetStringSlice("to") + + if len(from) > len(to) { + return ErrNoToPair + } + + if len(to) > len(from) { + return ErrNoFromPair + } + + for index, key := range from { + prev, ok := lo.Find(configuration.Mappings, func(item Mapping) bool { + return strings.EqualFold(item.From, key) + }) + + if ok { + prev.To = to[index] + } else { + configuration.Mappings = append(configuration.Mappings, Mapping{ + From: key, + To: to[index], + }) + } + } + + return nil +} + +func decodeConfig[T any](data any, mapping *T, decodeFuncs ...mapstructure.DecodeHookFunc) error { + hook := mapstructure.ComposeDecodeHookFunc( + hooks.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + mapstructure.ComposeDecodeHookFunc(decodeFuncs...), + ) + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: mapping, + DecodeHook: hook, + ErrorUnused: true, + IgnoreUntaggedFields: true, + }) + if err != nil { + return err //nolint:wrapcheck + } + + err = decoder.Decode(data) + + 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/config/helpers_test.go b/internal/config/helpers_test.go new file mode 100644 index 00000000..75c087f5 --- /dev/null +++ b/internal/config/helpers_test.go @@ -0,0 +1,206 @@ +// nolint: dupl +package config_test + +import ( + "testing" + + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/testing/testconstants" + "github.com/stretchr/testify/assert" +) + +const ( + httpPort = 3000 + httpsPort = 3001 +) + +func TestNormaliseMappings(t *testing.T) { + t.Run("custom port handling", func(t *testing.T) { + testsCases := []struct { + name string + mappings config.Mappings + expected config.Mappings + useHTTPS bool + }{ + { + name: "correctly set http and https ports", + mappings: config.Mappings{ + {From: testconstants.Localhost, To: testconstants.Github}, + }, + expected: config.Mappings{ + {From: testconstants.HTTPLocalhostWithPort(httpPort), To: testconstants.Github}, + {From: testconstants.HTTPSLocalhostWithPort(httpsPort), To: testconstants.Github}, + }, + useHTTPS: true, + }, + { + name: "correctly set http port", + mappings: config.Mappings{ + {From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub}, + }, + expected: config.Mappings{ + {From: testconstants.HTTPLocalhostWithPort(httpPort), To: testconstants.HTTPSGithub}, + }, + useHTTPS: true, + }, + { + name: "correctly set https port", + mappings: config.Mappings{ + {From: testconstants.HTTPSLocalhost, To: testconstants.HTTPSGithub}, + }, + expected: config.Mappings{ + {From: testconstants.HTTPSLocalhostWithPort(httpsPort), To: testconstants.HTTPSGithub}, + }, + useHTTPS: true, + }, + { + name: "correctly set mixed schemes", + mappings: config.Mappings{ + {From: testconstants.Localhost1, To: testconstants.HTTPSGithub}, + {From: testconstants.Localhost2, To: testconstants.HTTPGithub}, + {From: testconstants.HTTPLocalhost3, To: testconstants.HTTPAPIGithub}, + {From: testconstants.HTTPSLocalhost4, To: testconstants.HTTPSAPIGithub}, + }, + expected: config.Mappings{ + {From: testconstants.HTTPLocalhost1WithPort(httpPort), To: testconstants.HTTPSGithub}, + {From: testconstants.HTTPSLocalhost1WithPort(httpsPort), To: testconstants.HTTPSGithub}, + {From: testconstants.HTTPLocalhost2WithPort(httpPort), To: testconstants.HTTPGithub}, + {From: testconstants.HTTPSLocalhost2WithPort(httpsPort), To: testconstants.HTTPGithub}, + {From: testconstants.HTTPLocalhost3WithPort(httpPort), To: testconstants.HTTPAPIGithub}, + {From: testconstants.HTTPSLocalhost4WithPort(httpsPort), To: testconstants.HTTPSAPIGithub}, + }, + useHTTPS: true, + }, + } + for _, testCase := range testsCases { + t.Run(testCase.name, func(t *testing.T) { + actual, err := config.NormaliseMappings( + testCase.mappings, + httpPort, + httpsPort, + testCase.useHTTPS, + ) + + assert.NoError(t, err) + assert.EqualValues(t, testCase.expected, actual) + }) + } + }) + + t.Run("default port handling", func(t *testing.T) { + httpPort, httpsPort := 80, 443 + testsCases := []struct { + name string + mappings config.Mappings + expected config.Mappings + useHTTPS bool + }{ + { + name: "correctly set http and https ports", + mappings: config.Mappings{ + {From: testconstants.Localhost, To: testconstants.Github}, + }, + expected: config.Mappings{ + {From: testconstants.HTTPLocalhost, To: testconstants.Github}, + {From: testconstants.HTTPSLocalhost, To: testconstants.Github}, + }, + useHTTPS: true, + }, + { + name: "correctly set http port", + mappings: config.Mappings{ + {From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub}, + }, + expected: config.Mappings{ + {From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub}, + }, + useHTTPS: true, + }, + { + name: "correctly set https port", + mappings: config.Mappings{ + {From: testconstants.HTTPSLocalhost, To: testconstants.HTTPSGithub}, + }, + expected: config.Mappings{ + {From: testconstants.HTTPSLocalhost, To: testconstants.HTTPSGithub}, + }, + useHTTPS: true, + }, + { + name: "correctly set mixed schemes", + mappings: config.Mappings{ + {From: testconstants.Localhost1, To: testconstants.HTTPSGithub}, + {From: testconstants.Localhost2, To: testconstants.HTTPGithub}, + {From: testconstants.HTTPLocalhost3, To: testconstants.HTTPAPIGithub}, + {From: testconstants.HTTPSLocalhost4, To: testconstants.HTTPSAPIGithub}, + }, + expected: config.Mappings{ + {From: testconstants.HTTPLocalhost1, To: testconstants.HTTPSGithub}, + {From: testconstants.HTTPSLocalhost1, To: testconstants.HTTPSGithub}, + {From: testconstants.HTTPLocalhost2, To: testconstants.HTTPGithub}, + {From: testconstants.HTTPSLocalhost2, To: testconstants.HTTPGithub}, + {From: testconstants.HTTPLocalhost3, To: testconstants.HTTPAPIGithub}, + {From: testconstants.HTTPSLocalhost4, To: testconstants.HTTPSAPIGithub}, + }, + useHTTPS: true, + }, + } + for _, testCase := range testsCases { + t.Run(testCase.name, func(t *testing.T) { + actual, err := config.NormaliseMappings( + testCase.mappings, + httpPort, + httpsPort, + testCase.useHTTPS, + ) + + assert.NoError(t, err) + assert.EqualValues(t, testCase.expected, actual) + }) + } + }) + + t.Run("incorrect mappings", func(t *testing.T) { + testsCases := []struct { + name string + mappings config.Mappings + httpPort int + httpsPort int + useHTTPS bool + expectedErr string + }{ + { + name: "incorrect source url", + mappings: config.Mappings{ + {From: "loca^host", To: testconstants.Github}, + }, + httpPort: httpPort, + httpsPort: httpsPort, + useHTTPS: true, + expectedErr: "failed to parse source url: parse \"//loca^host\": invalid character \"^\" in host name", + }, + { + name: "incorrect port in source url", + mappings: config.Mappings{ + {From: "localhost:", To: testconstants.Github}, + }, + httpPort: -1, + httpsPort: httpsPort, + useHTTPS: true, + expectedErr: "failed to parse source url: port \"//localhost:\": empty port", + }, + } + for _, testCase := range testsCases { + t.Run(testCase.name, func(t *testing.T) { + _, err := config.NormaliseMappings( + testCase.mappings, + testCase.httpPort, + testCase.httpsPort, + testCase.useHTTPS, + ) + + assert.EqualError(t, err, testCase.expectedErr) + }) + } + }) +} diff --git a/internal/config/hooks/time_decode_hook.go b/internal/config/hooks/time_decode_hook.go new file mode 100644 index 00000000..1968e084 --- /dev/null +++ b/internal/config/hooks/time_decode_hook.go @@ -0,0 +1,22 @@ +package hooks + +import ( + "reflect" + "strings" + "time" + + "github.com/mitchellh/mapstructure" +) + +func StringToTimeDurationHookFunc() mapstructure.DecodeHookFunc { + return func(f reflect.Type, t reflect.Type, data any) (any, error) { + if f.Kind() != reflect.String || t != reflect.TypeOf(time.Second) { + return data, nil + } + + //nolint:wrapcheck + return time.ParseDuration( + strings.ReplaceAll(data.(string), " ", ""), //nolint: forcetypeassert + ) + } +} diff --git a/internal/configuration/hooks/time_decode_hook_test.go b/internal/config/hooks/time_decode_hook_test.go similarity index 97% rename from internal/configuration/hooks/time_decode_hook_test.go rename to internal/config/hooks/time_decode_hook_test.go index 69b230f3..0d36e656 100644 --- a/internal/configuration/hooks/time_decode_hook_test.go +++ b/internal/config/hooks/time_decode_hook_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/evg4b/uncors/internal/configuration/hooks" + "github.com/evg4b/uncors/internal/config/hooks" "github.com/evg4b/uncors/testing/testutils" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" diff --git a/internal/config/mapping.go b/internal/config/mapping.go new file mode 100644 index 00000000..dd51394e --- /dev/null +++ b/internal/config/mapping.go @@ -0,0 +1,62 @@ +package config + +import ( + "reflect" + + "github.com/mitchellh/mapstructure" + "github.com/samber/lo" +) + +type Mapping struct { + From string `mapstructure:"from"` + To string `mapstructure:"to"` + Statics StaticDirectories `mapstructure:"statics"` + Mocks Mocks `mapstructure:"mocks"` +} + +func (u *Mapping) Clone() Mapping { + return Mapping{ + From: u.From, + To: u.To, + Statics: u.Statics.Clone(), + Mocks: u.Mocks.Clone(), + } +} + +var ( + mappingType = reflect.TypeOf(Mapping{}) + mappingFields = getTagValues(mappingType, "mapstructure") +) + +func URLMappingHookFunc() mapstructure.DecodeHookFunc { + return func(f reflect.Type, t reflect.Type, rawData any) (any, error) { + if t != mappingType || f.Kind() != reflect.Map { + return rawData, nil + } + + if data, ok := rawData.(map[string]any); ok { + availableFields, _ := lo.Difference(lo.Keys(data), mappingFields) + if len(data) == 1 && len(availableFields) == 1 { + return Mapping{ + From: availableFields[0], + To: data[availableFields[0]].(string), // nolint: forcetypeassert + }, nil + } + + mapping := Mapping{} + err := decodeConfig(data, &mapping, StaticDirMappingHookFunc()) + + return mapping, err + } + + return rawData, nil + } +} + +func getTagValues(typeValue reflect.Type, tag string) []string { + fields := reflect.VisibleFields(typeValue) + + return lo.FilterMap(fields, func(field reflect.StructField, index int) (string, bool) { + return field.Tag.Lookup(tag) + }) +} diff --git a/internal/config/mapping_test.go b/internal/config/mapping_test.go new file mode 100644 index 00000000..d897654e --- /dev/null +++ b/internal/config/mapping_test.go @@ -0,0 +1,106 @@ +package config_test + +import ( + "testing" + + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/testing/testconstants" + "github.com/evg4b/uncors/testing/testutils" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +var localhostSecure = "https://localhost:9090" + +func TestURLMappingHookFunc(t *testing.T) { + const configFile = "config.yaml" + + t.Run("positive cases", func(t *testing.T) { + tests := []struct { + name string + config string + expected config.Mapping + }{ + { + name: "simple key-value mapping", + config: "http://localhost:4200: https://github.com", + expected: config.Mapping{ + From: testconstants.HTTPLocalhostWithPort(4200), + To: testconstants.HTTPSGithub, + }, + }, + { + name: "full object mapping", + config: "{ from: http://localhost:3000, to: https://api.github.com }", + expected: config.Mapping{ + From: testconstants.HTTPLocalhostWithPort(3000), + To: testconstants.HTTPSAPIGithub, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + viperInstance := viper.GetViper() + viperInstance.SetFs(testutils.FsFromMap(t, map[string]string{ + configFile: testCase.config, + })) + viperInstance.SetConfigFile(configFile) + err := viperInstance.ReadInConfig() + testutils.CheckNoError(t, err) + + actual := config.Mapping{} + + err = viperInstance.Unmarshal(&actual, viper.DecodeHook( + config.URLMappingHookFunc(), + )) + testutils.CheckNoError(t, err) + + assert.Equal(t, actual, testCase.expected) + }) + } + }) +} + +func TestURLMappingClone(t *testing.T) { + tests := []struct { + name string + expected config.Mapping + }{ + { + name: "empty structure", + expected: config.Mapping{}, + }, + { + name: "structure with 1 field", + expected: config.Mapping{ + From: testconstants.HTTPLocalhost, + }, + }, + { + name: "structure with 2 field", + expected: config.Mapping{ + From: testconstants.HTTPLocalhost, + To: localhostSecure, + }, + }, + { + name: "structure with inner collections", + expected: config.Mapping{ + From: testconstants.HTTPLocalhost, + To: localhostSecure, + Statics: []config.StaticDirectory{ + {Path: "/cc", Dir: "cc"}, + }, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + actual := testCase.expected.Clone() + + assert.NotSame(t, testCase.expected, actual) + assert.Equal(t, testCase.expected, actual) + assert.NotSame(t, testCase.expected.Statics, actual.Statics) + }) + } +} diff --git a/internal/config/mappings.go b/internal/config/mappings.go new file mode 100644 index 00000000..939b3114 --- /dev/null +++ b/internal/config/mappings.go @@ -0,0 +1,47 @@ +package config + +import ( + "strings" + + "github.com/evg4b/uncors/internal/sfmt" + "github.com/evg4b/uncors/pkg/urlx" + "github.com/samber/lo" +) + +type Mappings []Mapping + +func (mappings Mappings) String() string { + var builder strings.Builder + + for _, group := range lo.GroupBy(mappings, extractHost) { + for _, mapping := range group { + builder.WriteString(sfmt.Sprintf("%s => %s\n", mapping.From, mapping.To)) + } + + mapping := group[0] + for _, mock := range mapping.Mocks { + builder.WriteString(sfmt.Sprintf(" mock: [%s %d] %s\n", mock.Method, mock.Response.Code, mock.Path)) + } + for _, static := range mapping.Statics { + builder.WriteString(sfmt.Sprintf(" static: %s => %s\n", static.Path, static.Dir)) + } + } + + builder.WriteString("\n") + + 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 new file mode 100644 index 00000000..af77203b --- /dev/null +++ b/internal/config/mappings_test.go @@ -0,0 +1,104 @@ +//nolint:lll +package config_test + +import ( + "net/http" + "testing" + + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/testing/testconstants" + "github.com/stretchr/testify/assert" +) + +func TestMappings(t *testing.T) { + tests := []struct { + name string + mappings config.Mappings + expected []string + }{ + { + name: "http mapping only", + mappings: config.Mappings{ + {From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub}, + }, + expected: []string{"http://localhost => https://github.com"}, + }, + { + name: "http and https mappings", + mappings: config.Mappings{ + {From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub}, + {From: testconstants.HTTPSLocalhost, To: testconstants.HTTPSGithub}, + }, + expected: []string{ + "https://localhost => https://github.com", + "http://localhost => https://github.com", + }, + }, + { + name: "mapping and mocks", + mappings: config.Mappings{ + { + From: testconstants.HTTPLocalhost, + To: testconstants.HTTPSGithub, + Mocks: config.Mocks{ + { + Path: "/endpoint-1", + Method: http.MethodPost, + Response: config.Response{ + Code: http.StatusOK, + Raw: "OK", + }, + }, + { + Path: "/demo", + Method: http.MethodGet, + Queries: map[string]string{ + "param1": "value1", + }, + Response: config.Response{ + Code: http.StatusInternalServerError, + Raw: "ERROR", + }, + }, + { + Path: "/healthcheck", + Method: http.MethodGet, + Headers: map[string]string{ + "param1": "value1", + }, + Response: config.Response{ + Code: http.StatusForbidden, + Raw: "ERROR", + }, + }, + }, + }, + {From: testconstants.HTTPSLocalhost, To: testconstants.HTTPSGithub}, + }, + expected: []string{ + "https://localhost => https://github.com", + "http://localhost => https://github.com", + "mock: [POST 200] /endpoint-1", + "mock: [GET 500] /demo", + "mock: [GET 403] /healthcheck", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := tt.mappings.String() + + for _, expectedLine := range tt.expected { + assert.Contains(t, actual, expectedLine) + } + }) + } + + t.Run("empty", func(t *testing.T) { + var mappings config.Mappings + + actual := mappings.String() + + assert.Equal(t, "\n", actual) + }) +} diff --git a/internal/config/model.go b/internal/config/model.go new file mode 100644 index 00000000..136991bb --- /dev/null +++ b/internal/config/model.go @@ -0,0 +1,56 @@ +package config + +import ( + "time" + + "github.com/evg4b/uncors/internal/helpers" + "github.com/samber/lo" +) + +type Response struct { + Code int `mapstructure:"code"` + Headers map[string]string `mapstructure:"headers"` + Raw string `mapstructure:"raw"` + File string `mapstructure:"file"` + Delay time.Duration `mapstructure:"delay"` +} + +func (r *Response) Clone() Response { + return Response{ + Code: r.Code, + Headers: helpers.CloneMap(r.Headers), + Raw: r.Raw, + File: r.File, + Delay: r.Delay, + } +} + +type Mock struct { + Path string `mapstructure:"path"` + Method string `mapstructure:"method"` + Queries map[string]string `mapstructure:"queries"` + Headers map[string]string `mapstructure:"headers"` + Response Response `mapstructure:"response"` +} + +func (m *Mock) Clone() Mock { + return Mock{ + Path: m.Path, + Method: m.Method, + Queries: helpers.CloneMap(m.Queries), + Headers: helpers.CloneMap(m.Headers), + Response: m.Response.Clone(), + } +} + +type Mocks []Mock + +func (m Mocks) Clone() Mocks { + if m == nil { + return nil + } + + return lo.Map(m, func(item Mock, index int) Mock { + return item.Clone() + }) +} diff --git a/internal/config/model_test.go b/internal/config/model_test.go new file mode 100644 index 00000000..699db26b --- /dev/null +++ b/internal/config/model_test.go @@ -0,0 +1,91 @@ +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) { + response := config.Response{ + Code: http.StatusOK, + Headers: map[string]string{ + headers.ContentType: "plain/text", + headers.CacheControl: "none", + }, + Raw: "this is plain text", + File: "~/projects/uncors/response/demo.json", + Delay: time.Hour, + } + + clonedResponse := response.Clone() + + t.Run("not same", func(t *testing.T) { + assert.NotSame(t, &response, &clonedResponse) + }) + + t.Run("equals values", func(t *testing.T) { + assert.EqualValues(t, response, clonedResponse) + }) + + t.Run("not same Headers map", func(t *testing.T) { + assert.NotSame(t, &response.Headers, &clonedResponse.Headers) + }) +} + +func TestMockClone(t *testing.T) { + mock := 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, + Raw: `{ "status": "ok" }`, + }, + } + + clonedMock := mock.Clone() + + t.Run("not same", func(t *testing.T) { + assert.NotSame(t, &mock, &clonedMock) + }) + + t.Run("equals values", func(t *testing.T) { + assert.EqualValues(t, mock, clonedMock) + }) + + t.Run("not same headers map", func(t *testing.T) { + assert.NotSame(t, &mock.Headers, &clonedMock.Headers) + }) + + t.Run("equals headers map", func(t *testing.T) { + assert.EqualValues(t, mock.Headers, clonedMock.Headers) + }) + + t.Run("not same queries map", func(t *testing.T) { + assert.NotSame(t, &mock.Queries, &clonedMock.Queries) + }) + + t.Run("equals queries map values", func(t *testing.T) { + assert.EqualValues(t, mock.Queries, clonedMock.Queries) + }) + + t.Run("not same Response", func(t *testing.T) { + assert.NotSame(t, &mock.Response, &clonedMock.Response) + }) + + t.Run("equals Response values", func(t *testing.T) { + assert.EqualValues(t, mock.Response, clonedMock.Response) + }) +} diff --git a/internal/config/static.go b/internal/config/static.go new file mode 100644 index 00000000..8e6818cf --- /dev/null +++ b/internal/config/static.go @@ -0,0 +1,72 @@ +package config + +import ( + "reflect" + + "github.com/mitchellh/mapstructure" + "github.com/samber/lo" +) + +type StaticDirectory struct { + Path string `mapstructure:"path"` + Dir string `mapstructure:"dir"` + Index string `mapstructure:"index"` +} + +func (s *StaticDirectory) Clone() StaticDirectory { + return StaticDirectory{ + Path: s.Path, + Dir: s.Dir, + Index: s.Index, + } +} + +type StaticDirectories []StaticDirectory + +func (d StaticDirectories) Clone() StaticDirectories { + if d == nil { + return nil + } + + return lo.Map(d, func(item StaticDirectory, index int) StaticDirectory { + return item.Clone() + }) +} + +var staticDirMappingsType = reflect.TypeOf(StaticDirectories{}) + +func StaticDirMappingHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn + return func(f reflect.Type, t reflect.Type, rawData any) (any, error) { + if t != staticDirMappingsType || f.Kind() != reflect.Map { + return rawData, nil + } + + mappingsDefs, ok := rawData.(map[string]any) + if !ok { + return rawData, nil + } + + var mappings StaticDirectories + for path, mappingDef := range mappingsDefs { + if def, ok := mappingDef.(string); ok { + mappings = append(mappings, StaticDirectory{ + Path: path, + Dir: def, + }) + + continue + } + + mapping := StaticDirectory{} + err := decodeConfig(mappingDef, &mapping) + if err != nil { + return nil, err + } + + mapping.Path = path + mappings = append(mappings, mapping) + } + + return mappings, nil + } +} diff --git a/internal/config/static_test.go b/internal/config/static_test.go new file mode 100644 index 00000000..b64ca8bc --- /dev/null +++ b/internal/config/static_test.go @@ -0,0 +1,140 @@ +package config_test + +import ( + "testing" + + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/testing/testutils" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +const ( + anotherStaticDir = "/another-static-dir" + anotherPath = "/another-path" + path = "/path" + staticDir = "/static-dir" +) + +func TestStaticDirMappingHookFunc(t *testing.T) { + const configFile = "config.yaml" + type testType struct { + Statics config.StaticDirectories `mapstructure:"statics"` + } + + tests := []struct { + name string + config string + expected config.StaticDirectories + }{ + { + name: "decode plan mapping", + config: ` +statics: + /path: /static-dir + /another-path: /another-static-dir +`, + expected: config.StaticDirectories{ + {Path: anotherPath, Dir: anotherStaticDir}, + {Path: path, Dir: staticDir}, + }, + }, + { + name: "decode object mappings", + config: ` +statics: + /path: { dir: /static-dir } + /another-path: { dir: /another-static-dir } +`, + expected: config.StaticDirectories{ + {Path: path, Dir: staticDir}, + {Path: anotherPath, Dir: anotherStaticDir}, + }, + }, + { + name: "decode object mappings with index", + config: ` +statics: + /path: { dir: /static-dir, index: index.html } + /another-path: { dir: /another-static-dir, index: default.html } +`, + expected: config.StaticDirectories{ + {Path: path, Dir: staticDir, Index: "index.html"}, + {Path: anotherPath, Dir: anotherStaticDir, Index: "default.html"}, + }, + }, + { + name: "decode mixed mappings with index", + config: ` +statics: + /path: { dir: /static-dir, index: index.html } + /another-path: /another-static-dir +`, + expected: config.StaticDirectories{ + {Path: path, Dir: staticDir, Index: "index.html"}, + {Path: anotherPath, Dir: anotherStaticDir}, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + viperInstance := viper.GetViper() + viperInstance.SetFs(testutils.FsFromMap(t, map[string]string{ + configFile: testCase.config, + })) + viperInstance.SetConfigFile(configFile) + err := viperInstance.ReadInConfig() + testutils.CheckNoError(t, err) + + actual := testType{} + + err = viperInstance.Unmarshal(&actual, viper.DecodeHook( + config.StaticDirMappingHookFunc(), + )) + testutils.CheckNoError(t, err) + + assert.ElementsMatch(t, actual.Statics, testCase.expected) + }) + } +} + +func TestStaticDirMappingClone(t *testing.T) { + tests := []struct { + name string + expected config.StaticDirectory + }{ + { + name: "empty structure", + expected: config.StaticDirectory{}, + }, + { + name: "structure with 1 field", + expected: config.StaticDirectory{ + Dir: "dir", + }, + }, + { + name: "structure with 2 field", + expected: config.StaticDirectory{ + Dir: "dir", + Path: "/some-path", + }, + }, + { + name: "structure with all field", + expected: config.StaticDirectory{ + Dir: "dir", + Path: "/one-more-path", + Index: "index.html", + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + actual := testCase.expected.Clone() + + assert.NotSame(t, testCase.expected, actual) + assert.Equal(t, testCase.expected, actual) + }) + } +} diff --git a/internal/configuration/validation.go b/internal/config/validation.go similarity index 89% rename from internal/configuration/validation.go rename to internal/config/validation.go index f43976c0..919cebf3 100644 --- a/internal/configuration/validation.go +++ b/internal/config/validation.go @@ -1,4 +1,4 @@ -package configuration +package config import ( "github.com/go-playground/validator/v10" diff --git a/internal/configuration/validation_test.go b/internal/config/validation_test.go similarity index 65% rename from internal/configuration/validation_test.go rename to internal/config/validation_test.go index 6446f542..a23e9987 100644 --- a/internal/configuration/validation_test.go +++ b/internal/config/validation_test.go @@ -1,29 +1,29 @@ -package configuration_test +package config_test import ( "testing" - "github.com/evg4b/uncors/internal/configuration" + "github.com/evg4b/uncors/internal/config" "github.com/stretchr/testify/assert" ) func TestValidate(t *testing.T) { tests := []struct { name string - config *configuration.UncorsConfig + config *config.UncorsConfig expected string }{ { name: "invalid http-port", - config: &configuration.UncorsConfig{ - Mappings: map[string]string{}, + config: &config.UncorsConfig{ + Mappings: config.Mappings{}, }, expected: "Key: 'UncorsConfig.HTTPPort' Error:Field validation for 'HTTPPort' failed on the 'required' tag", }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - err := configuration.Validate(testCase.config) + err := config.Validate(testCase.config) assert.EqualError(t, err, testCase.expected) }) diff --git a/internal/configuration/config_test.go b/internal/configuration/config_test.go deleted file mode 100644 index cb0738ce..00000000 --- a/internal/configuration/config_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package configuration_test - -import ( - "testing" - - "github.com/evg4b/uncors/internal/configuration" - "github.com/evg4b/uncors/internal/middlewares/mock" - "github.com/evg4b/uncors/testing/mocks" - "github.com/evg4b/uncors/testing/testutils" - "github.com/evg4b/uncors/testing/testutils/params" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestLoadConfiguration(t *testing.T) { - fs := testutils.PrepareFsForTests(t, "config_test_data") - viperInstance := viper.New() - viperInstance.SetFs(fs) - - t.Run("correctly parse configuration", func(t *testing.T) { - tests := []struct { - name string - args []string - expected *configuration.UncorsConfig - }{ - { - name: "return default config", - args: []string{}, - expected: &configuration.UncorsConfig{ - HTTPPort: 80, - HTTPSPort: 443, - Mappings: map[string]string{}, - Mocks: []mock.Mock{}, - }, - }, - { - name: "minimal config is set", - args: []string{params.Config, "/minimal-config.yaml"}, - expected: &configuration.UncorsConfig{ - HTTPPort: 8080, - HTTPSPort: 443, - Mappings: map[string]string{ - "http://demo": "https://demo.com", - }, - Mocks: []mock.Mock{}, - }, - }, - { - name: "read all fields from config file config is set", - args: []string{params.Config, "/full-config.yaml"}, - expected: &configuration.UncorsConfig{ - HTTPPort: 8080, - Mappings: map[string]string{ - "http://demo1": "https://demo1.com", - "http://other-demo2": "https://demo2.io", - }, - Proxy: "localhost:8080", - Debug: true, - HTTPSPort: 8081, - CertFile: "/cert-file.pem", - KeyFile: "/key-file.key", - Mocks: []mock.Mock{ - { - Path: "/demo", - Method: "POST", - Queries: map[string]string{ - "foo": "bar", - }, - Headers: map[string]string{ - "accept-encoding": "deflate", - }, - Response: mock.Response{ - Code: 201, - Headers: map[string]string{ - "accept-encoding": "deflate", - }, - RawContent: "demo", - File: "/demo.txt", - }, - }, - }, - }, - }, - { - name: "read all fields from config file config is set", - args: []string{ - params.Config, "/full-config.yaml", - params.From, mocks.SourceHost1, params.To, mocks.TargetHost1, - params.From, mocks.SourceHost2, params.To, mocks.TargetHost2, - params.From, mocks.SourceHost3, params.To, mocks.TargetHost3, - }, - expected: &configuration.UncorsConfig{ - HTTPPort: 8080, - Mappings: map[string]string{ - "http://demo1": "https://demo1.com", - "http://other-demo2": "https://demo2.io", - mocks.SourceHost1: mocks.TargetHost1, - mocks.SourceHost2: mocks.TargetHost2, - mocks.SourceHost3: mocks.TargetHost3, - }, - Proxy: "localhost:8080", - Debug: true, - HTTPSPort: 8081, - CertFile: "/cert-file.pem", - KeyFile: "/key-file.key", - Mocks: []mock.Mock{ - { - Path: "/demo", - Method: "POST", - Queries: map[string]string{ - "foo": "bar", - }, - Headers: map[string]string{ - "accept-encoding": "deflate", - }, - Response: mock.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 := configuration.LoadConfiguration(viperInstance, testCase.args) - - assert.NoError(t, err) - assert.Equal(t, testCase.expected, config) - }) - } - }) - - t.Run("parse configuration with error", func(t *testing.T) { - tests := []struct { - name string - args []string - expected []string - }{ - { - name: "incorrect flag provided", - args: []string{ - "--incorrect-flag", - }, - expected: []string{ - "filed parsing flags: unknown flag: --incorrect-flag", - }, - }, - { - name: "return default config", - args: []string{ - params.To, mocks.TargetHost1, - }, - expected: []string{ - "recognize url mapping: `from` values are not set for every `to`", - }, - }, - { - name: "count of from values great then count of to", - args: []string{ - params.From, mocks.SourceHost1, params.To, mocks.TargetHost1, - params.From, mocks.SourceHost2, - }, - expected: []string{ - "recognize url mapping: `to` values are not set for every `from`", - }, - }, - { - name: "count of to values great then count of from", - args: []string{ - params.From, mocks.SourceHost1, params.To, mocks.TargetHost1, - params.To, mocks.TargetHost2, - }, - expected: []string{ - "recognize url mapping: `from` values are not set for every `to`", - }, - }, - { - name: "configuration file doesn't exist", - args: []string{ - params.Config, "/not-exist-config.yaml", - }, - expected: []string{ - "filed to read config file '/not-exist-config.yaml': open ", - "test_data/not-exist-config.yaml: no such file or directory", - }, - }, - { - name: "configuration file is corrupted", - args: []string{ - params.Config, "/corrupted-config.yaml", - }, - expected: []string{ - "filed to read config file '/corrupted-config.yaml': " + - "While parsing config: yaml: line 2: could not find expected ':'", - }, - }, - { - name: "incorrect param type", - args: []string{ - params.HttpPort, "xxx", - }, - expected: []string{ - "filed parsing flags: invalid argument \"xxx\" for \"-p, --http-port\" flag: " + - "strconv.ParseUint: parsing \"xxx\": invalid syntax", - }, - }, - { - name: "incorrect type in config file", - args: []string{ - params.Config, "/incorrect-config.yaml", - }, - expected: []string{ - "filed parsing configuraion: 1 error(s) decoding:\n\n* cannot parse 'http-port' as int:" + - " strconv.ParseInt: parsing \"xxx\": invalid syntax", - }, - }, - } - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - config, err := configuration.LoadConfiguration(viperInstance, testCase.args) - - assert.Nil(t, config) - for _, expected := range testCase.expected { - assert.ErrorContains(t, err, expected) - } - }) - } - }) -} - -func TestUncorsConfigIsHTTPSEnabled(t *testing.T) { - tests := []struct { - name string - config *configuration.UncorsConfig - expected bool - }{ - { - name: "false by default", - config: &configuration.UncorsConfig{}, - expected: false, - }, - { - name: "false by default", - config: &configuration.UncorsConfig{}, - expected: false, - }, - } - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.expected, testCase.config.IsHTTPSEnabled()) - }) - } -} diff --git a/internal/configuration/config_test_data/corrupted-config.yaml b/internal/configuration/config_test_data/corrupted-config.yaml deleted file mode 100644 index 5224732a..00000000 --- a/internal/configuration/config_test_data/corrupted-config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -http-port: 8080 -mappings& - http://demo: https://demo.com diff --git a/internal/configuration/config_test_data/full-config.yaml b/internal/configuration/config_test_data/full-config.yaml deleted file mode 100644 index 6546a997..00000000 --- a/internal/configuration/config_test_data/full-config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -http-port: 8080 -mappings: - http://demo1: https://demo1.com - http://other-demo2: https://demo2.io -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/configuration/config_test_data/incorrect-config.yaml b/internal/configuration/config_test_data/incorrect-config.yaml deleted file mode 100644 index cc2a1d3f..00000000 --- a/internal/configuration/config_test_data/incorrect-config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -http-port: xxx -mappings: - http://demo: https://demo.com diff --git a/internal/configuration/config_test_data/minimal-config.yaml b/internal/configuration/config_test_data/minimal-config.yaml deleted file mode 100644 index aefa4a04..00000000 --- a/internal/configuration/config_test_data/minimal-config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -http-port: 8080 -mappings: - http://demo: https://demo.com diff --git a/internal/configuration/helpers.go b/internal/configuration/helpers.go deleted file mode 100644 index 0a7a679d..00000000 --- a/internal/configuration/helpers.go +++ /dev/null @@ -1,37 +0,0 @@ -package configuration - -import ( - "errors" - - "github.com/evg4b/uncors/internal/log" - - "github.com/spf13/viper" -) - -var ( - ErrNoToPair = errors.New("`to` values are not set for every `from`") - ErrNoFromPair = errors.New("`from` values are not set for every `to`") -) - -func readURLMapping(config *viper.Viper, configuration *UncorsConfig) error { - from, to := config.GetStringSlice("from"), config.GetStringSlice("to") - - if len(from) > len(to) { - return ErrNoToPair - } - - if len(to) > len(from) { - return ErrNoFromPair - } - - for index, key := range from { - value := to[index] - if prev, ok := configuration.Mappings[key]; ok { - log.Warningf("Mapping for %s from (%s) replaced new value (%s)", key, prev, value) - } - - configuration.Mappings[key] = value - } - - return nil -} diff --git a/internal/configuration/hooks/time_decode_hook.go b/internal/configuration/hooks/time_decode_hook.go deleted file mode 100644 index be28daac..00000000 --- a/internal/configuration/hooks/time_decode_hook.go +++ /dev/null @@ -1,25 +0,0 @@ -package hooks - -import ( - "reflect" - "strings" - "time" - - "github.com/mitchellh/mapstructure" -) - -func StringToTimeDurationHookFunc() mapstructure.DecodeHookFunc { //nolint: ireturn - return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { - if f.Kind() != reflect.String { - return data, nil - } - - if t != reflect.TypeOf(time.Second) { - return data, nil - } - - trimmed := strings.ReplaceAll(data.(string), " ", "") //nolint: forcetypeassert - - return time.ParseDuration(trimmed) //nolint:wrapcheck - } -} diff --git a/internal/middlewares/proxy/replacer_factory.go b/internal/contracts/handler.go similarity index 91% rename from internal/middlewares/proxy/replacer_factory.go rename to internal/contracts/handler.go index bdd80410..061c26bf 100644 --- a/internal/middlewares/proxy/replacer_factory.go +++ b/internal/contracts/handler.go @@ -1,4 +1,4 @@ -package proxy +package contracts import ( "net/url" diff --git a/internal/handler/helpres.go b/internal/handler/helpres.go new file mode 100644 index 00000000..2b6a98dd --- /dev/null +++ b/internal/handler/helpres.go @@ -0,0 +1,33 @@ +package handler + +import ( + "github.com/gorilla/mux" +) + +func setPath(route *mux.Route, path string) { + if len(path) > 0 { + route.Path(path) + } +} + +func setMethod(route *mux.Route, methods string) { + if len(methods) > 0 { + route.Methods(methods) + } +} + +func setQueries(route *mux.Route, queries map[string]string) { + if len(queries) > 0 { + for key, value := range queries { + route.Queries(key, value) + } + } +} + +func setHeaders(route *mux.Route, headers map[string]string) { + if len(headers) > 0 { + for key, value := range headers { + route.Headers(key, value) + } + } +} diff --git a/internal/handler/mock/middleware.go b/internal/handler/mock/middleware.go new file mode 100644 index 00000000..fdd909b4 --- /dev/null +++ b/internal/handler/mock/middleware.go @@ -0,0 +1,83 @@ +package mock + +import ( + "net/http" + "time" + + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/internal/contracts" + "github.com/evg4b/uncors/internal/infra" + "github.com/spf13/afero" +) + +type Middleware struct { + response config.Response + logger contracts.Logger + fs afero.Fs + after func(duration time.Duration) <-chan time.Time +} + +func NewMockMiddleware(options ...MiddlewareOption) *Middleware { + middleware := &Middleware{} + + for _, option := range options { + option(middleware) + } + + return middleware +} + +func (m *Middleware) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + response := m.response + header := writer.Header() + + if response.Delay > 0 { + m.logger.Debugf("Delay %s for %s", response.Delay, request.URL.RequestURI()) + ctx := request.Context() + + url := request.URL.RequestURI() + waitingLoop: + for { + select { + case <-ctx.Done(): + writer.WriteHeader(http.StatusServiceUnavailable) + m.logger.Debugf("Delay is canceled (url: %s)", url) + + return + case <-m.after(response.Delay): + m.logger.Debugf("Delay is complete (url: %s)", url) + + break waitingLoop + } + } + } + + infra.WriteCorsHeaders(header) + for key, value := range response.Headers { + header.Set(key, value) + } + + if len(m.response.File) > 0 { + err := m.serveFileContent(writer, request) + if err != nil { + infra.HTTPError(writer, err) + + return + } + } else { + m.serveRawContent(writer) + } + + m.logger.PrintResponse(&http.Response{ + Request: request, + StatusCode: response.Code, + }) +} + +func normaliseCode(code int) int { + if code == 0 { + return http.StatusOK + } + + return code +} diff --git a/internal/handler/mock/middleware_options.go b/internal/handler/mock/middleware_options.go new file mode 100644 index 00000000..688e2328 --- /dev/null +++ b/internal/handler/mock/middleware_options.go @@ -0,0 +1,35 @@ +package mock + +import ( + "time" + + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/internal/contracts" + "github.com/spf13/afero" +) + +type MiddlewareOption = func(*Middleware) + +func WithLogger(logger contracts.Logger) MiddlewareOption { + return func(m *Middleware) { + m.logger = logger + } +} + +func WithResponse(response config.Response) MiddlewareOption { + return func(m *Middleware) { + m.response = response + } +} + +func WithFileSystem(fs afero.Fs) MiddlewareOption { + return func(m *Middleware) { + m.fs = fs + } +} + +func WithAfter(after func(duration time.Duration) <-chan time.Time) MiddlewareOption { + return func(m *Middleware) { + m.after = after + } +} diff --git a/internal/middlewares/mock/handler_internal_test.go b/internal/handler/mock/middleware_test.go similarity index 66% rename from internal/middlewares/mock/handler_internal_test.go rename to internal/handler/mock/middleware_test.go index acd33da4..59a1c433 100644 --- a/internal/middlewares/mock/handler_internal_test.go +++ b/internal/handler/mock/middleware_test.go @@ -1,4 +1,4 @@ -package mock +package mock_test import ( "context" @@ -8,7 +8,10 @@ import ( "testing" "time" + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/internal/handler/mock" "github.com/evg4b/uncors/testing/mocks" + "github.com/evg4b/uncors/testing/testconstants" "github.com/evg4b/uncors/testing/testutils" "github.com/go-http-utils/headers" "github.com/stretchr/testify/assert" @@ -41,37 +44,33 @@ func TestHandler(t *testing.T) { pngFile: pngContent, }) - var makeHandler = func(t *testing.T, response Response) *internalHandler { - return &internalHandler{ - logger: mocks.NewNoopLogger(t), - response: response, - fs: fileSystem, - after: func(duration time.Duration) <-chan time.Time { - return time.After(time.Nanosecond) - }, - } - } - t.Run("mock content", func(t *testing.T) { tests := []struct { name string - response Response + response config.Response expected string }{ { name: "raw content", - response: Response{RawContent: jsonContent}, + response: config.Response{Raw: jsonContent}, expected: jsonContent, }, { name: "file content", - response: Response{File: jsonFile}, + response: config.Response{File: jsonFile}, expected: jsonContent, }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - handler := makeHandler(t, testCase.response) + handler := mock.NewMockMiddleware( + mock.WithLogger(mocks.NewNoopLogger(t)), + mock.WithResponse(testCase.response), + mock.WithFileSystem(fileSystem), + mock.WithAfter(func(duration time.Duration) <-chan time.Time { + return time.After(time.Nanosecond) + }), + ) recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -86,53 +85,60 @@ func TestHandler(t *testing.T) { t.Run("content type detection", func(t *testing.T) { tests := []struct { name string - response Response + response config.Response expected string }{ { name: "raw content with plain text", - response: Response{RawContent: textContent}, + response: config.Response{Raw: textContent}, expected: textPlain, }, { name: "raw content with json", - response: Response{RawContent: jsonContent}, + response: config.Response{Raw: jsonContent}, expected: textPlain, }, { name: "raw content with html", - response: Response{RawContent: htmlContent}, + response: config.Response{Raw: htmlContent}, expected: "text/html; charset=utf-8", }, { name: "raw content with png", - response: Response{RawContent: pngContent}, + response: config.Response{Raw: pngContent}, expected: imagePng, }, { name: "file with plain text", - response: Response{File: textFile}, + response: config.Response{File: textFile}, expected: textPlain, }, { name: "file with json", - response: Response{File: jsonFile}, + response: config.Response{File: jsonFile}, expected: "application/json", }, { name: "file with html", - response: Response{File: htmlFile}, + response: config.Response{File: htmlFile}, expected: "text/html; charset=utf-8", }, { name: "file with png", - response: Response{File: pngFile}, + response: config.Response{File: pngFile}, expected: imagePng, }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - handler := makeHandler(t, testCase.response) + handler := mock.NewMockMiddleware( + mock.WithLogger(mocks.NewNoopLogger(t)), + mock.WithResponse(testCase.response), + mock.WithFileSystem(fileSystem), + mock.WithAfter(func(duration time.Duration) <-chan time.Time { + return time.After(time.Nanosecond) + }), + ) recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/", nil) @@ -147,74 +153,81 @@ func TestHandler(t *testing.T) { t.Run("headers settings", func(t *testing.T) { tests := []struct { name string - response Response + response config.Response expected http.Header }{ { name: "should put default CORS headers", - response: Response{ - Code: http.StatusOK, - RawContent: textContent, + response: config.Response{ + Code: http.StatusOK, + Raw: textContent, }, expected: map[string][]string{ headers.AccessControlAllowOrigin: {"*"}, headers.AccessControlAllowCredentials: {"true"}, headers.ContentType: {textPlain}, - headers.AccessControlAllowMethods: {mocks.AllMethods}, + headers.AccessControlAllowMethods: {testconstants.AllMethods}, }, }, { name: "should set response code", - response: Response{ - Code: http.StatusOK, - RawContent: textContent, + response: config.Response{ + Code: http.StatusOK, + Raw: textContent, }, expected: map[string][]string{ headers.AccessControlAllowOrigin: {"*"}, headers.AccessControlAllowCredentials: {"true"}, headers.ContentType: {textPlain}, - headers.AccessControlAllowMethods: {mocks.AllMethods}, + headers.AccessControlAllowMethods: {testconstants.AllMethods}, }, }, { name: "should set custom headers", - response: Response{ + response: config.Response{ Code: http.StatusOK, Headers: map[string]string{ "X-Key": "X-Key-Value", }, - RawContent: textContent, + Raw: textContent, }, expected: map[string][]string{ headers.AccessControlAllowOrigin: {"*"}, headers.AccessControlAllowCredentials: {"true"}, headers.ContentType: {textPlain}, "X-Key": {"X-Key-Value"}, - headers.AccessControlAllowMethods: {mocks.AllMethods}, + headers.AccessControlAllowMethods: {testconstants.AllMethods}, }, }, { name: "should override default headers", - response: Response{ + response: config.Response{ Code: http.StatusOK, Headers: map[string]string{ - headers.AccessControlAllowOrigin: "localhost", + headers.AccessControlAllowOrigin: testconstants.Localhost, headers.AccessControlAllowCredentials: "false", headers.ContentType: "none", }, - RawContent: textContent, + Raw: textContent, }, expected: map[string][]string{ - headers.AccessControlAllowOrigin: {"localhost"}, + headers.AccessControlAllowOrigin: {testconstants.Localhost}, headers.AccessControlAllowCredentials: {"false"}, headers.ContentType: {"none"}, - headers.AccessControlAllowMethods: {mocks.AllMethods}, + headers.AccessControlAllowMethods: {testconstants.AllMethods}, }, }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - handler := makeHandler(t, testCase.response) + handler := mock.NewMockMiddleware( + mock.WithLogger(mocks.NewNoopLogger(t)), + mock.WithResponse(testCase.response), + mock.WithFileSystem(fileSystem), + mock.WithAfter(func(duration time.Duration) <-chan time.Time { + return time.After(time.Nanosecond) + }), + ) request := httptest.NewRequest(http.MethodGet, "/", nil) recorder := httptest.NewRecorder() @@ -230,32 +243,39 @@ func TestHandler(t *testing.T) { t.Run("status code", func(t *testing.T) { tests := []struct { name string - response Response + response config.Response expected int }{ { name: "provide 201 code", - response: Response{ + response: config.Response{ Code: http.StatusCreated, }, expected: http.StatusCreated, }, { name: "provide 503 code", - response: Response{ + response: config.Response{ Code: http.StatusServiceUnavailable, }, expected: http.StatusServiceUnavailable, }, { name: "automatically provide 200 code", - response: Response{}, + response: config.Response{}, expected: http.StatusOK, }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - handler := makeHandler(t, testCase.response) + handler := mock.NewMockMiddleware( + mock.WithLogger(mocks.NewNoopLogger(t)), + mock.WithResponse(testCase.response), + mock.WithFileSystem(fileSystem), + mock.WithAfter(func(duration time.Duration) <-chan time.Time { + return time.After(time.Nanosecond) + }), + ) request := httptest.NewRequest(http.MethodGet, "/", nil) recorder := httptest.NewRecorder() @@ -271,13 +291,13 @@ func TestHandler(t *testing.T) { t.Run("correctly handle delay", func(t *testing.T) { tests := []struct { name string - response Response + response config.Response shouldBeCalled bool expected time.Duration }{ { name: "3s delay", - response: Response{ + response: config.Response{ Code: http.StatusCreated, Delay: 3 * time.Second, }, @@ -286,7 +306,7 @@ func TestHandler(t *testing.T) { }, { name: "15h delay", - response: Response{ + response: config.Response{ Code: http.StatusCreated, Delay: 15 * time.Hour, }, @@ -295,7 +315,7 @@ func TestHandler(t *testing.T) { }, { name: "0s delay", - response: Response{ + response: config.Response{ Code: http.StatusCreated, Delay: 0 * time.Second, }, @@ -303,14 +323,14 @@ func TestHandler(t *testing.T) { }, { name: "delay is not set", - response: Response{ + response: config.Response{ Code: http.StatusCreated, }, shouldBeCalled: false, }, { name: "incorrect delay", - response: Response{ + response: config.Response{ Code: http.StatusCreated, Delay: -13 * time.Minute, }, @@ -320,13 +340,17 @@ func TestHandler(t *testing.T) { for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { called := false - handler := makeHandler(t, testCase.response) - handler.after = func(duration time.Duration) <-chan time.Time { - assert.Equal(t, duration, testCase.expected) - called = true - - return time.After(time.Nanosecond) - } + handler := mock.NewMockMiddleware( + mock.WithLogger(mocks.NewNoopLogger(t)), + mock.WithResponse(testCase.response), + mock.WithFileSystem(fileSystem), + mock.WithAfter(func(duration time.Duration) <-chan time.Time { + assert.Equal(t, duration, testCase.expected) + called = true + + return time.After(time.Nanosecond) + }), + ) request := httptest.NewRequest(http.MethodGet, "/", nil) recorder := httptest.NewRecorder() @@ -339,12 +363,16 @@ func TestHandler(t *testing.T) { }) t.Run("correctly cancel delay", func(t *testing.T) { - handler := makeHandler(t, Response{ - Code: http.StatusOK, - Delay: 1 * time.Hour, - RawContent: "Text content", - }) - handler.after = time.After + handler := mock.NewMockMiddleware( + mock.WithLogger(mocks.NewNoopLogger(t)), + mock.WithResponse(config.Response{ + Code: http.StatusOK, + Delay: 1 * time.Hour, + Raw: "Text content", + }), + mock.WithFileSystem(fileSystem), + mock.WithAfter(time.After), + ) request := httptest.NewRequest(http.MethodGet, "/", nil) ctx, cancel := context.WithCancel(context.Background()) diff --git a/internal/middlewares/mock/serve_file_content.go b/internal/handler/mock/serve_file_content.go similarity index 61% rename from internal/middlewares/mock/serve_file_content.go rename to internal/handler/mock/serve_file_content.go index c40104f1..eb5377f6 100644 --- a/internal/middlewares/mock/serve_file_content.go +++ b/internal/handler/mock/serve_file_content.go @@ -6,9 +6,9 @@ import ( "os" ) -func (handler *internalHandler) serveFileContent(writer http.ResponseWriter, request *http.Request) error { - fileName := handler.response.File - file, err := handler.fs.OpenFile(fileName, os.O_RDONLY, os.ModePerm) +func (m *Middleware) serveFileContent(writer http.ResponseWriter, request *http.Request) error { + fileName := m.response.File + file, err := m.fs.OpenFile(fileName, os.O_RDONLY, os.ModePerm) if err != nil { return fmt.Errorf("filed to opent file %s: %w", fileName, err) } diff --git a/internal/handler/mock/serve_raw_content.go b/internal/handler/mock/serve_raw_content.go new file mode 100644 index 00000000..c0db4cbb --- /dev/null +++ b/internal/handler/mock/serve_raw_content.go @@ -0,0 +1,20 @@ +package mock + +import ( + "net/http" + + "github.com/evg4b/uncors/internal/sfmt" + "github.com/go-http-utils/headers" +) + +func (m *Middleware) serveRawContent(writer http.ResponseWriter) { + response := m.response + header := writer.Header() + if len(header.Get(headers.ContentType)) == 0 { + contentType := http.DetectContentType([]byte(response.Raw)) + header.Set(headers.ContentType, contentType) + } + + writer.WriteHeader(normaliseCode(response.Code)) + sfmt.Fprint(writer, response.Raw) +} diff --git a/internal/handler/mocked_routes.go b/internal/handler/mocked_routes.go new file mode 100644 index 00000000..4e5c4b39 --- /dev/null +++ b/internal/handler/mocked_routes.go @@ -0,0 +1,29 @@ +package handler + +import ( + "github.com/evg4b/uncors/internal/config" + "github.com/gorilla/mux" +) + +func (m *RequestHandler) makeMockedRoutes(router *mux.Router, mocks config.Mocks) { + var defaultMocks config.Mocks + + for _, mockDef := range mocks { + if len(mockDef.Queries) > 0 || len(mockDef.Headers) > 0 || len(mockDef.Method) > 0 { + route := router.NewRoute() + setPath(route, mockDef.Path) + setMethod(route, mockDef.Method) + setQueries(route, mockDef.Queries) + setHeaders(route, mockDef.Headers) + route.Handler(m.createHandler(mockDef.Response)) + } else { + defaultMocks = append(defaultMocks, mockDef) + } + } + + for _, mockDef := range defaultMocks { + route := router.NewRoute() + setPath(route, mockDef.Path) + route.Handler(m.createHandler(mockDef.Response)) + } +} diff --git a/internal/middlewares/proxy/helpers.go b/internal/handler/proxy/helpers.go similarity index 100% rename from internal/middlewares/proxy/helpers.go rename to internal/handler/proxy/helpers.go diff --git a/internal/middlewares/proxy/middleware.go b/internal/handler/proxy/middleware.go similarity index 72% rename from internal/middlewares/proxy/middleware.go rename to internal/handler/proxy/middleware.go index 6c55ec52..8211526c 100644 --- a/internal/middlewares/proxy/middleware.go +++ b/internal/handler/proxy/middleware.go @@ -7,18 +7,18 @@ import ( "github.com/evg4b/uncors/internal/contracts" "github.com/evg4b/uncors/internal/helpers" + "github.com/evg4b/uncors/internal/infra" "github.com/evg4b/uncors/internal/urlreplacer" - "github.com/pterm/pterm" ) -type Middleware struct { - replacers URLReplacerFactory +type Handler struct { + replacers contracts.URLReplacerFactory http contracts.HTTPClient logger contracts.Logger } -func NewProxyMiddleware(options ...MiddlewareOption) *Middleware { - middleware := &Middleware{} +func NewProxyHandler(options ...HandlerOption) *Handler { + middleware := &Handler{} for _, option := range options { option(middleware) @@ -31,17 +31,13 @@ func NewProxyMiddleware(options ...MiddlewareOption) *Middleware { return middleware } -func (m *Middleware) ServeHTTP(response http.ResponseWriter, request *http.Request) { - updateRequest(request) - +func (m *Handler) ServeHTTP(response http.ResponseWriter, request *http.Request) { if err := m.handle(response, request); err != nil { - pterm.Error.Printfln("UNCORS error: %v", err) - response.WriteHeader(http.StatusInternalServerError) - fmt.Fprintln(response, "UNCORS error:", err.Error()) + infra.HTTPError(response, err) } } -func (m *Middleware) handle(resp http.ResponseWriter, req *http.Request) error { +func (m *Handler) handle(resp http.ResponseWriter, req *http.Request) error { if strings.EqualFold(req.Method, http.MethodOptions) { return m.makeOptionsResponse(resp, req) } @@ -61,7 +57,7 @@ func (m *Middleware) handle(resp http.ResponseWriter, req *http.Request) error { return err } - defer originalResponse.Body.Close() + defer helpers.CloseSafe(originalResponse.Body) err = m.makeUncorsResponse(originalResponse, resp, sourceReplacer) if err != nil { @@ -71,7 +67,7 @@ func (m *Middleware) handle(resp http.ResponseWriter, req *http.Request) error { return nil } -func (m *Middleware) executeQuery(request *http.Request) (*http.Response, error) { +func (m *Handler) executeQuery(request *http.Request) (*http.Response, error) { originalResponse, err := m.http.Do(request) if err != nil { return nil, fmt.Errorf("failed to do reuest: %w", err) @@ -102,13 +98,3 @@ func copyCookiesToTarget(source *http.Request, replacer *urlreplacer.Replacer, t return nil } - -func updateRequest(request *http.Request) { - request.URL.Host = request.Host - - if request.TLS != nil { - request.URL.Scheme = "https" - } else { - request.URL.Scheme = "http" - } -} diff --git a/internal/middlewares/proxy/middleware_test.go b/internal/handler/proxy/middleware_test.go similarity index 89% rename from internal/middlewares/proxy/middleware_test.go rename to internal/handler/proxy/middleware_test.go index b798ad37..077c5628 100644 --- a/internal/middlewares/proxy/middleware_test.go +++ b/internal/handler/proxy/middleware_test.go @@ -8,18 +8,21 @@ import ( "strings" "testing" - "github.com/evg4b/uncors/internal/middlewares/proxy" + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/internal/handler/proxy" + "github.com/evg4b/uncors/internal/helpers" "github.com/evg4b/uncors/internal/urlreplacer" "github.com/evg4b/uncors/pkg/urlx" "github.com/evg4b/uncors/testing/mocks" + "github.com/evg4b/uncors/testing/testconstants" "github.com/evg4b/uncors/testing/testutils" "github.com/go-http-utils/headers" "github.com/stretchr/testify/assert" ) func TestProxyMiddleware(t *testing.T) { - replacerFactory, err := urlreplacer.NewURLReplacerFactory(map[string]string{ - "http://premium.local.com": "https://premium.api.com", + replacerFactory, err := urlreplacer.NewURLReplacerFactory(config.Mappings{ + {From: "http://premium.local.com", To: "https://premium.api.com"}, }) testutils.CheckNoError(t, err) @@ -62,7 +65,7 @@ func TestProxyMiddleware(t *testing.T) { } }) - proc := proxy.NewProxyMiddleware( + proc := proxy.NewProxyHandler( proxy.WithHTTPClient(httpClient), proxy.WithURLReplacerFactory(replacerFactory), proxy.WithLogger(mocks.NewNoopLogger(t)), @@ -90,7 +93,7 @@ func TestProxyMiddleware(t *testing.T) { headerKey string }{ { - name: "transform Location", + name: "transform location", URL: "https://premium.api.com/app", expectedURL: "http://premium.local.com/app", headerKey: headers.Location, @@ -115,7 +118,7 @@ func TestProxyMiddleware(t *testing.T) { } }) - proc := proxy.NewProxyMiddleware( + proc := proxy.NewProxyHandler( proxy.WithHTTPClient(httpClient), proxy.WithURLReplacerFactory(replacerFactory), proxy.WithLogger(mocks.NewNoopLogger(t)), @@ -123,10 +126,10 @@ func TestProxyMiddleware(t *testing.T) { req, err := http.NewRequestWithContext(context.TODO(), http.MethodPost, expectedURL.Path, nil) testutils.CheckNoError(t, err) - req.URL.Scheme = expectedURL.Scheme req.Host = expectedURL.Host req.URL.Path = expectedURL.Path + helpers.NormaliseRequest(req) recorder := httptest.NewRecorder() @@ -149,7 +152,7 @@ func TestProxyMiddleware(t *testing.T) { } }) - proc := proxy.NewProxyMiddleware( + proc := proxy.NewProxyHandler( proxy.WithHTTPClient(httpClient), proxy.WithURLReplacerFactory(replacerFactory), proxy.WithLogger(mocks.NewNoopLogger(t)), @@ -157,9 +160,9 @@ func TestProxyMiddleware(t *testing.T) { req, err := http.NewRequestWithContext(context.TODO(), http.MethodPost, "/", nil) testutils.CheckNoError(t, err) - req.URL.Scheme = "http" req.Host = "premium.local.com" + helpers.NormaliseRequest(req) recorder := httptest.NewRecorder() @@ -170,13 +173,13 @@ func TestProxyMiddleware(t *testing.T) { assert.Equal(t, "true", header.Get(headers.AccessControlAllowCredentials)) assert.Equal( t, - mocks.AllMethods, + testconstants.AllMethods, header.Get(headers.AccessControlAllowMethods), ) }) t.Run("OPTIONS request handling", func(t *testing.T) { - middleware := proxy.NewProxyMiddleware( + middleware := proxy.NewProxyHandler( proxy.WithHTTPClient(http.DefaultClient), proxy.WithURLReplacerFactory(replacerFactory), proxy.WithLogger(mocks.NewNoopLogger(t)), @@ -194,7 +197,7 @@ func TestProxyMiddleware(t *testing.T) { expected: map[string][]string{ headers.AccessControlAllowOrigin: {"*"}, headers.AccessControlAllowCredentials: {"true"}, - headers.AccessControlAllowMethods: {mocks.AllMethods}, + headers.AccessControlAllowMethods: {testconstants.AllMethods}, }, }, { @@ -211,7 +214,7 @@ func TestProxyMiddleware(t *testing.T) { "X-Hey-Header": {"123"}, headers.AccessControlAllowOrigin: {"*"}, headers.AccessControlAllowCredentials: {"true"}, - headers.AccessControlAllowMethods: {mocks.AllMethods}, + headers.AccessControlAllowMethods: {testconstants.AllMethods}, }, }, { @@ -227,7 +230,7 @@ func TestProxyMiddleware(t *testing.T) { "Custom-Header": {"true"}, headers.AccessControlAllowOrigin: {"*"}, headers.AccessControlAllowCredentials: {"true"}, - headers.AccessControlAllowMethods: {mocks.AllMethods}, + headers.AccessControlAllowMethods: {testconstants.AllMethods}, }, }, } diff --git a/internal/handler/proxy/option.go b/internal/handler/proxy/option.go new file mode 100644 index 00000000..87c55677 --- /dev/null +++ b/internal/handler/proxy/option.go @@ -0,0 +1,17 @@ +package proxy + +import ( + "net/http" + + "github.com/evg4b/uncors/internal/infra" +) + +func (m *Handler) makeOptionsResponse(writer http.ResponseWriter, req *http.Request) error { + infra.WriteCorsHeaders(writer.Header()) + m.logger.PrintResponse(&http.Response{ + StatusCode: http.StatusOK, + Request: req, + }) + + return nil +} diff --git a/internal/handler/proxy/options.go b/internal/handler/proxy/options.go new file mode 100644 index 00000000..ded7adbd --- /dev/null +++ b/internal/handler/proxy/options.go @@ -0,0 +1,25 @@ +package proxy + +import ( + "github.com/evg4b/uncors/internal/contracts" +) + +type HandlerOption = func(*Handler) + +func WithURLReplacerFactory(replacerFactory contracts.URLReplacerFactory) HandlerOption { + return func(m *Handler) { + m.replacers = replacerFactory + } +} + +func WithHTTPClient(http contracts.HTTPClient) HandlerOption { + return func(m *Handler) { + m.http = http + } +} + +func WithLogger(logger contracts.Logger) HandlerOption { + return func(m *Handler) { + m.logger = logger + } +} diff --git a/internal/middlewares/proxy/request.go b/internal/handler/proxy/request.go similarity index 95% rename from internal/middlewares/proxy/request.go rename to internal/handler/proxy/request.go index 256ef03b..d585e838 100644 --- a/internal/middlewares/proxy/request.go +++ b/internal/handler/proxy/request.go @@ -8,7 +8,7 @@ import ( "github.com/go-http-utils/headers" ) -func (m *Middleware) makeOriginalRequest( +func (m *Handler) makeOriginalRequest( req *http.Request, replacer *urlreplacer.Replacer, ) (*http.Request, error) { diff --git a/internal/middlewares/proxy/responce.go b/internal/handler/proxy/responce.go similarity index 85% rename from internal/middlewares/proxy/responce.go rename to internal/handler/proxy/responce.go index 9e0425e9..a87a2b3d 100644 --- a/internal/middlewares/proxy/responce.go +++ b/internal/handler/proxy/responce.go @@ -5,12 +5,12 @@ import ( "io" "net/http" - "github.com/evg4b/uncors/internal/infrastructure" + "github.com/evg4b/uncors/internal/infra" "github.com/evg4b/uncors/internal/urlreplacer" "github.com/go-http-utils/headers" ) -func (m *Middleware) makeUncorsResponse( +func (m *Handler) makeUncorsResponse( original *http.Response, target http.ResponseWriter, replacer *urlreplacer.Replacer, @@ -26,7 +26,7 @@ func (m *Middleware) makeUncorsResponse( return err } - infrastructure.WriteCorsHeaders(target.Header()) + infra.WriteCorsHeaders(target.Header()) return copyResponseData(target, original) } diff --git a/internal/handler/static/file.go b/internal/handler/static/file.go new file mode 100644 index 00000000..863cd763 --- /dev/null +++ b/internal/handler/static/file.go @@ -0,0 +1,63 @@ +package static + +import ( + "errors" + "fmt" + "io/fs" + "os" + + "github.com/spf13/afero" +) + +var errNorHandled = errors.New("request is not handled") + +func (m *Middleware) openFile(filePath string) (afero.File, os.FileInfo, error) { + file, err := m.fs.Open(filePath) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return nil, nil, fmt.Errorf("filed to open file: %w", err) + } + + indexFile, err := m.openIndexFile() + if err != nil { + return nil, nil, err + } + + file = indexFile + } + + stat, err := file.Stat() + if err != nil { + return file, nil, fmt.Errorf("filed to get information about file: %w", err) + } + + if stat.IsDir() { + indexFile, err := m.openIndexFile() + if err != nil { + return file, stat, err + } + + indexFileStat, err := indexFile.Stat() + if err != nil { + return file, stat, fmt.Errorf("filed to get information about index file: %w", err) + } + + file = indexFile + stat = indexFileStat + } + + return file, stat, nil +} + +func (m *Middleware) openIndexFile() (afero.File, error) { + if len(m.index) == 0 { + return nil, errNorHandled + } + + file, err := m.fs.Open(m.index) + if err != nil { + return nil, fmt.Errorf("filed to opend index file: %w", err) + } + + return file, nil +} diff --git a/internal/handler/static/middleware.go b/internal/handler/static/middleware.go new file mode 100644 index 00000000..66f1dfa1 --- /dev/null +++ b/internal/handler/static/middleware.go @@ -0,0 +1,59 @@ +package static + +import ( + "errors" + "net/http" + "path" + "strings" + + "github.com/evg4b/uncors/internal/contracts" + "github.com/evg4b/uncors/internal/helpers" + "github.com/evg4b/uncors/internal/infra" + "github.com/spf13/afero" +) + +type Middleware struct { + fs afero.Fs + next http.Handler + index string + logger contracts.Logger + prefix string +} + +func NewStaticMiddleware(options ...MiddlewareOption) *Middleware { + middleware := &Middleware{} + + for _, option := range options { + option(middleware) + } + + return middleware +} + +func (m *Middleware) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + filePath := m.extractFilePath(request) + + file, stat, err := m.openFile(filePath) + defer helpers.CloseSafe(file) + + if err != nil { + if errors.Is(err, errNorHandled) { + m.next.ServeHTTP(writer, request) + } else { + infra.HTTPError(writer, err) + } + + return + } + + http.ServeContent(writer, request, stat.Name(), stat.ModTime(), file) +} + +func (m *Middleware) extractFilePath(request *http.Request) string { + filePath := strings.TrimPrefix(request.URL.Path, m.prefix) + if !strings.HasPrefix(filePath, "/") { + filePath = "/" + filePath + } + + return path.Clean(filePath) +} diff --git a/internal/middlewares/mock/middleware_options.go b/internal/handler/static/middleware_options.go similarity index 65% rename from internal/middlewares/mock/middleware_options.go rename to internal/handler/static/middleware_options.go index 91c20915..ac9e4083 100644 --- a/internal/middlewares/mock/middleware_options.go +++ b/internal/handler/static/middleware_options.go @@ -1,4 +1,4 @@ -package mock +package static import ( "net/http" @@ -9,26 +9,32 @@ import ( type MiddlewareOption = func(*Middleware) -func WithLogger(logger contracts.Logger) MiddlewareOption { +func WithFileSystem(fs afero.Fs) MiddlewareOption { return func(m *Middleware) { - m.logger = logger + m.fs = fs + } +} + +func WithIndex(index string) MiddlewareOption { + return func(m *Middleware) { + m.index = index } } -func WithNextMiddleware(next http.Handler) MiddlewareOption { +func WithNext(next http.Handler) MiddlewareOption { return func(m *Middleware) { m.next = next } } -func WithMocks(mocks []Mock) MiddlewareOption { +func WithLogger(logger contracts.Logger) MiddlewareOption { return func(m *Middleware) { - m.mocks = mocks + m.logger = logger } } -func WithFileSystem(fs afero.Fs) MiddlewareOption { +func WithPrefix(prefix string) MiddlewareOption { return func(m *Middleware) { - m.fs = fs + m.prefix = prefix } } diff --git a/internal/handler/static/middleware_test.go b/internal/handler/static/middleware_test.go new file mode 100644 index 00000000..fb8d8637 --- /dev/null +++ b/internal/handler/static/middleware_test.go @@ -0,0 +1,212 @@ +package static_test + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/evg4b/uncors/internal/handler/static" + "github.com/evg4b/uncors/internal/sfmt" + "github.com/evg4b/uncors/testing/mocks" + "github.com/evg4b/uncors/testing/testutils" + "github.com/stretchr/testify/assert" +) + +const ( + indexJS = "/assets/index.js" + demoJS = "/assets/demo.js" + indexHTML = "/index.html" +) + +const ( + indexJSContent = "console.log('index.js')" + demoJSContent = "console.log('demo.js')" + indexHTMLContent = "" +) + +func TestMiddleware(t *testing.T) { + fs := testutils.FsFromMap(t, map[string]string{ + indexJS: indexJSContent, + demoJS: demoJSContent, + indexHTML: indexHTMLContent, + }) + + staticFileTests := []struct { + name string + path string + expected string + }{ + { + name: "should send pain file", + path: indexHTML, + expected: indexHTMLContent, + }, + { + name: "should send file from folder", + path: indexJS, + expected: indexJSContent, + }, + { + name: "should send file ignore query params", + path: indexHTML + "?testParam=test", + expected: indexHTMLContent, + }, + { + name: "should send file from folder ignore query params", + path: demoJS + "?testParam=test", + expected: demoJSContent, + }, + { + name: "should send pain file without root slash", + path: strings.TrimPrefix(indexHTML, "/"), + expected: indexHTMLContent, + }, + { + name: "should send demo.js file from folder without root slash", + path: strings.TrimPrefix(demoJS, "/"), + expected: demoJSContent, + }, + } + + notExistingFilesTests := []struct { + name string + path string + }{ + { + name: "where file not exists", + path: "/options.html", + }, + { + name: "where request directory", + path: "/assets/", + }, + { + name: "where request directory without trailing slash", + path: "/assets", + }, + { + name: "where request not exists directory", + path: "/options/", + }, + } + + t.Run("index file is not configured", func(t *testing.T) { + const testHTTPStatusCode = 999 + const testHTTPBody = "this is tests response" + middleware := static.NewStaticMiddleware( + static.WithFileSystem(fs), + static.WithLogger(mocks.NewLoggerMock(t)), + static.WithNext(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + writer.WriteHeader(testHTTPStatusCode) + sfmt.Fprint(writer, testHTTPBody) + })), + ) + + t.Run("return static content", func(t *testing.T) { + for _, testCase := range staticFileTests { + t.Run(testCase.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + requestURI, err := url.Parse(testCase.path) + testutils.CheckNoError(t, err) + + middleware.ServeHTTP(recorder, &http.Request{ + Method: http.MethodGet, + URL: requestURI, + }) + + assert.Equal(t, recorder.Code, http.StatusOK) + assert.Equal(t, testCase.expected, testutils.ReadBody(t, recorder)) + }) + } + }) + + t.Run("forward request to next handler", func(t *testing.T) { + for _, testCase := range notExistingFilesTests { + t.Run(testCase.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + requestURI, err := url.Parse(testCase.path) + testutils.CheckNoError(t, err) + + middleware.ServeHTTP(recorder, &http.Request{ + Method: http.MethodGet, + URL: requestURI, + }) + + assert.Equal(t, testHTTPStatusCode, recorder.Code) + assert.Equal(t, testHTTPBody, testutils.ReadBody(t, recorder)) + }) + } + }) + }) + + t.Run("index file is configured", func(t *testing.T) { + middleware := static.NewStaticMiddleware( + static.WithFileSystem(fs), + static.WithLogger(mocks.NewLoggerMock(t)), + static.WithIndex(indexHTML), + static.WithNext(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + panic("should not be called") + })), + ) + + t.Run("send index file", func(t *testing.T) { + for _, testCase := range staticFileTests { + t.Run(testCase.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + requestURI, err := url.Parse(testCase.path) + testutils.CheckNoError(t, err) + + middleware.ServeHTTP(recorder, &http.Request{ + Method: http.MethodGet, + URL: requestURI, + }) + + assert.Equal(t, recorder.Code, http.StatusOK) + assert.Equal(t, testCase.expected, testutils.ReadBody(t, recorder)) + }) + } + }) + + t.Run("forward request to next handler", func(t *testing.T) { + for _, testCase := range notExistingFilesTests { + t.Run(testCase.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + requestURI, err := url.Parse(testCase.path) + testutils.CheckNoError(t, err) + + middleware.ServeHTTP(recorder, &http.Request{ + Method: http.MethodGet, + URL: requestURI, + }) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, indexHTMLContent, testutils.ReadBody(t, recorder)) + }) + } + }) + + t.Run("index file doesn't exists", func(t *testing.T) { + middleware := static.NewStaticMiddleware( + static.WithFileSystem(fs), + static.WithLogger(mocks.NewLoggerMock(t)), + static.WithIndex("/not-exists.html"), + static.WithNext(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + panic("should not be called") + })), + ) + + recorder := httptest.NewRecorder() + requestURI, err := url.Parse("/options/") + testutils.CheckNoError(t, err) + + middleware.ServeHTTP(recorder, &http.Request{ + Method: http.MethodGet, + URL: requestURI, + }) + + assert.Equal(t, http.StatusInternalServerError, recorder.Code) + }) + }) +} diff --git a/internal/handler/static_routes.go b/internal/handler/static_routes.go new file mode 100644 index 00000000..90837076 --- /dev/null +++ b/internal/handler/static_routes.go @@ -0,0 +1,34 @@ +package handler + +import ( + "net/http" + "strings" + + "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 *RequestHandler) makeStaticRoutes(router *mux.Router, statics config.StaticDirectories, 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 new file mode 100644 index 00000000..ac8b5225 --- /dev/null +++ b/internal/handler/uncors_handler.go @@ -0,0 +1,100 @@ +package handler + +import ( + "errors" + "net/http" + "strings" + "time" + + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/internal/contracts" + "github.com/evg4b/uncors/internal/handler/mock" + "github.com/evg4b/uncors/internal/handler/proxy" + "github.com/evg4b/uncors/internal/infra" + "github.com/evg4b/uncors/internal/sfmt" + "github.com/evg4b/uncors/internal/ui" + "github.com/evg4b/uncors/pkg/urlx" + "github.com/gorilla/mux" + "github.com/spf13/afero" +) + +type RequestHandler struct { + router *mux.Router + fs afero.Fs + logger contracts.Logger + mappings config.Mappings + replacerFactory contracts.URLReplacerFactory + httpClient contracts.HTTPClient +} + +var errHostNotMapped = errors.New("host not mapped") + +func NewUncorsRequestHandler(options ...UncorsRequestHandlerOption) *RequestHandler { + handler := &RequestHandler{ + router: mux.NewRouter(), + mappings: config.Mappings{}, + } + + for _, option := range options { + option(handler) + } + + proxyHandler := proxy.NewProxyHandler( + proxy.WithURLReplacerFactory(handler.replacerFactory), + proxy.WithHTTPClient(handler.httpClient), + proxy.WithLogger(ui.ProxyLogger), + ) + + 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 *RequestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + m.router.ServeHTTP(writer, request) +} + +func (m *RequestHandler) createHandler(response config.Response) *mock.Middleware { + return mock.NewMockMiddleware( + mock.WithLogger(ui.MockLogger), + mock.WithResponse(response), + mock.WithFileSystem(m.fs), + mock.WithAfter(time.After), + ) +} + +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, sfmt.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 new file mode 100644 index 00000000..ac3bdaec --- /dev/null +++ b/internal/handler/uncors_handler_options.go @@ -0,0 +1,39 @@ +package handler + +import ( + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/internal/contracts" + "github.com/spf13/afero" +) + +type UncorsRequestHandlerOption = func(*RequestHandler) + +func WithLogger(logger contracts.Logger) UncorsRequestHandlerOption { + return func(m *RequestHandler) { + m.logger = logger + } +} + +func WithFileSystem(fs afero.Fs) UncorsRequestHandlerOption { + return func(m *RequestHandler) { + m.fs = fs + } +} + +func WithURLReplacerFactory(replacerFactory contracts.URLReplacerFactory) UncorsRequestHandlerOption { + return func(m *RequestHandler) { + m.replacerFactory = replacerFactory + } +} + +func WithHTTPClient(client contracts.HTTPClient) UncorsRequestHandlerOption { + return func(m *RequestHandler) { + m.httpClient = client + } +} + +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 new file mode 100644 index 00000000..96a9da70 --- /dev/null +++ b/internal/handler/uncors_handler_test.go @@ -0,0 +1,737 @@ +package handler_test + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/internal/handler" + "github.com/evg4b/uncors/internal/helpers" + "github.com/evg4b/uncors/internal/log" + "github.com/evg4b/uncors/internal/sfmt" + "github.com/evg4b/uncors/internal/urlreplacer" + "github.com/evg4b/uncors/testing/mocks" + "github.com/evg4b/uncors/testing/testconstants" + "github.com/evg4b/uncors/testing/testutils" + "github.com/go-http-utils/headers" + "github.com/stretchr/testify/assert" +) + +var ( + mock1Body = `{"mock": "mock number 1"}` + mock2Body = `{"mock": "mock number 2"}` + mock3Body = `{"mock": "mock number 3"}` + mock4Body = `{"mock": "mock number 4"}` + + backgroundPng = "background.png" + iconsSvg = "icons.svg" + indexJS = "index.js" + styleCSS = "styles.css" + indexHTML = "index.html" + mockJSON = "mock.json" + + api = "http://localhost/api" + apiUser = "https://localhost/api/user" + + userPath = "/api/user" + + userIDHeader = "User-Id" +) + +func TestUncorsRequestHandler(t *testing.T) { + log.DisableOutput() + fs := testutils.FsFromMap(t, map[string]string{ + "/images/background.png": backgroundPng, + "/images/svg/icons.svg": iconsSvg, + "/assets/js/index.js": indexJS, + "/assets/css/styles.css": styleCSS, + "/assets/index.html": indexHTML, + "/mock.json": mockJSON, + }) + + mappings := config.Mappings{ + { + From: testconstants.HTTPLocalhost, + To: testconstants.HTTPSLocalhost, + Statics: []config.StaticDirectory{ + {Dir: "/assets", Path: "/cc/", Index: indexHTML}, + {Dir: "/assets", Path: "/pnp/", Index: "index.php"}, + {Dir: "/images", Path: "/img/"}, + }, + Mocks: config.Mocks{ + { + Path: "/api/mocks/1", + Response: config.Response{ + Code: http.StatusOK, + Raw: "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, + Raw: "mock-3", + }, + }, + { + Path: "/api/mocks/4", + Response: config.Response{ + Code: http.StatusOK, + File: "/unknown.json", + }, + }, + }, + }, + } + + factory, err := urlreplacer.NewURLReplacerFactory(mappings) + testutils.CheckNoError(t, err) + + httpResponseMapping := map[string]string{ + "/img/original.png": "original.png", + } + + httpMock := mocks.NewHTTPClientMock(t).DoMock.Set(func(request *http.Request) (*http.Response, error) { + if response, ok := httpResponseMapping[request.URL.Path]; ok { + return &http.Response{ + Body: io.NopCloser(strings.NewReader(response)), + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Header: http.Header{}, + Request: request, + }, nil + } + + panic(sfmt.Sprintf("incorrect request: %s", request.URL.Path)) + }) + + hand := handler.NewUncorsRequestHandler( + handler.WithLogger(mocks.NewLoggerMock(t)), + handler.WithFileSystem(fs), + handler.WithURLReplacerFactory(factory), + handler.WithHTTPClient(httpMock), + handler.WithMappings(mappings), + ) + + t.Run("statics directory", func(t *testing.T) { + t.Run("with index file", func(t *testing.T) { + t.Run("should return static file", func(t *testing.T) { + tests := []struct { + name string + url string + expected string + }{ + { + name: indexHTML, + url: "http://localhost/cc/index.html", + expected: indexHTML, + }, + { + name: indexJS, + url: "http://localhost/cc/js/index.js", + expected: indexJS, + }, + { + name: styleCSS, + url: "http://localhost/cc/css/styles.css", + expected: styleCSS, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, testCase.url, nil) + helpers.NormaliseRequest(request) + + hand.ServeHTTP(recorder, request) + + assert.Equal(t, 200, recorder.Code) + assert.Equal(t, testCase.expected, testutils.ReadBody(t, recorder)) + }) + } + }) + + t.Run("should return index file by default", func(t *testing.T) { + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "http://localhost/cc/unknown.html", nil) + helpers.NormaliseRequest(request) + + hand.ServeHTTP(recorder, request) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, indexHTML, testutils.ReadBody(t, recorder)) + }) + + t.Run("should return error code when index file doesn't exists", func(t *testing.T) { + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "http://localhost/pnp/unknown.html", nil) + helpers.NormaliseRequest(request) + + hand.ServeHTTP(recorder, request) + + assert.Equal(t, http.StatusInternalServerError, recorder.Code) + expectedMessage := "filed to opend index file: open /assets/index.php: file does not exist" + assert.Contains(t, testutils.ReadBody(t, recorder), expectedMessage) + }) + }) + + t.Run("without index file", func(t *testing.T) { + t.Run("should return static file", func(t *testing.T) { + tests := []struct { + name string + url string + expected string + }{ + { + name: backgroundPng, + url: "http://localhost/img/background.png", + expected: backgroundPng, + }, + { + name: iconsSvg, + url: "http://localhost/img/svg/icons.svg", + expected: iconsSvg, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, testCase.url, nil) + helpers.NormaliseRequest(request) + + hand.ServeHTTP(recorder, request) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, testCase.expected, testutils.ReadBody(t, recorder)) + }) + } + }) + + t.Run("should return original file", func(t *testing.T) { + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "http://localhost/img/original.png", nil) + helpers.NormaliseRequest(request) + + hand.ServeHTTP(recorder, request) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, "original.png", testutils.ReadBody(t, recorder)) + }) + }) + }) + + t.Run("mocks", func(t *testing.T) { + t.Run("should return mock with", func(t *testing.T) { + tests := []struct { + name string + url string + expected string + expectedCode int + }{ + { + name: "raw content mock", + url: "http://localhost/api/mocks/1", + expected: "mock-1", + expectedCode: http.StatusOK, + }, + { + name: "file content mock", + url: "http://localhost/api/mocks/2", + expected: mockJSON, + expectedCode: http.StatusOK, + }, + { + name: "MultiStatus mock", + url: "http://localhost/api/mocks/3", + expected: "mock-3", + expectedCode: http.StatusMultiStatus, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, testCase.url, nil) + helpers.NormaliseRequest(request) + + hand.ServeHTTP(recorder, request) + + assert.Equal(t, testCase.expectedCode, recorder.Code) + assert.Equal(t, testCase.expected, testutils.ReadBody(t, recorder)) + }) + } + }) + + t.Run("should return error code when mock file doesn't exists", func(t *testing.T) { + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "http://localhost/api/mocks/4", nil) + helpers.NormaliseRequest(request) + + hand.ServeHTTP(recorder, request) + + assert.Equal(t, http.StatusInternalServerError, recorder.Code) + expectedMessage := "filed to opent file /unknown.json: open /unknown.json: file does not exist" + assert.Contains(t, testutils.ReadBody(t, recorder), expectedMessage) + }) + }) +} + +func TestMockMiddleware(t *testing.T) { + log.DisableOutput() + logger := mocks.NewNoopLogger(t) + + t.Run("request method handling", func(t *testing.T) { + t.Run("where mock method is not set allow method", func(t *testing.T) { + middleware := handler.NewUncorsRequestHandler( + handler.WithHTTPClient(mocks.NewHTTPClientMock(t)), + handler.WithURLReplacerFactory(mocks.NewURLReplacerFactoryMock(t)), + handler.WithLogger(logger), + handler.WithMappings(config.Mappings{ + { + From: "*", + To: "*", + Mocks: config.Mocks{ + { + Path: "/api", + Response: config.Response{ + Code: http.StatusOK, + Raw: mock1Body, + }, + }, + }, + }, + }), + ) + + methods := []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodTrace, + } + + for _, method := range methods { + t.Run(method, func(t *testing.T) { + request := httptest.NewRequest(method, api, nil) + recorder := httptest.NewRecorder() + + middleware.ServeHTTP(recorder, request) + + body := testutils.ReadBody(t, recorder) + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, mock1Body, body) + }) + } + }) + + t.Run("where method is set", func(t *testing.T) { + expectedCode := 299 + expectedBody := "forwarded" + mappings := config.Mappings{ + {From: "*", To: "*", Mocks: config.Mocks{{ + Path: "/api", + Method: http.MethodPut, + Response: config.Response{ + Code: http.StatusOK, + Raw: mock1Body, + }, + }}}, + } + 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), + ) + + t.Run("method is not matched", func(t *testing.T) { + methods := []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPatch, + http.MethodDelete, + http.MethodTrace, + } + + for _, method := range methods { + t.Run(method, func(t *testing.T) { + request := httptest.NewRequest(method, api, nil) + recorder := httptest.NewRecorder() + + middleware.ServeHTTP(recorder, request) + + assert.Equal(t, expectedCode, recorder.Code) + assert.Equal(t, expectedBody, testutils.ReadBody(t, recorder)) + }) + } + + t.Run(http.MethodOptions, func(t *testing.T) { + request := httptest.NewRequest(http.MethodOptions, api, nil) + recorder := httptest.NewRecorder() + + middleware.ServeHTTP(recorder, request) + + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, "", testutils.ReadBody(t, recorder)) + }) + }) + + t.Run("method is matched", func(t *testing.T) { + request := httptest.NewRequest(http.MethodPut, api, nil) + recorder := httptest.NewRecorder() + + middleware.ServeHTTP(recorder, request) + + body := testutils.ReadBody(t, recorder) + assert.Equal(t, http.StatusOK, recorder.Code) + assert.Equal(t, mock1Body, body) + }) + }) + }) + + t.Run("path handling", func(t *testing.T) { + expectedCode := 299 + expectedBody := "forwarded" + mappings := config.Mappings{ + {From: "*", To: "*", Mocks: config.Mocks{ + { + Path: userPath, + Response: config.Response{ + Code: http.StatusOK, + Raw: mock1Body, + }, + }, + { + Path: "/api/user/{id:[0-9]+}", + Response: config.Response{ + Code: http.StatusAccepted, + Raw: mock2Body, + }, + }, + { + Path: "/api/{single-path/demo", + Response: config.Response{ + Code: http.StatusBadRequest, + Raw: mock3Body, + }, + }, + { + Path: `/api/v2/{multiple-path:[a-z-\/]+}/demo`, + Response: config.Response{ + Code: http.StatusCreated, + Raw: 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 { + name string + url string + expected string + statusCode int + }{ + { + name: "direct path", + url: apiUser, + expected: mock1Body, + statusCode: http.StatusOK, + }, + { + name: "direct path with ending slash", + url: "https://localhost/api/user/", + expected: expectedBody, + statusCode: expectedCode, + }, + { + name: "direct path with parameter", + url: "https://localhost/api/user/23", + expected: mock2Body, + statusCode: http.StatusAccepted, + }, + { + name: "direct path with incorrect parameter", + url: "https://localhost/api/user/unknow", + expected: expectedBody, + statusCode: expectedCode, + }, + { + name: "path with subpath to single matching param", + url: "https://localhost/api/some-path/with-some-subpath/demo", + expected: expectedBody, + statusCode: expectedCode, + }, + { + name: "path with subpath to multiple matching param", + url: "https://localhost/api/v2/some-path/with-some-subpath/demo", + expected: mock4Body, + statusCode: http.StatusCreated, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + request := httptest.NewRequest(http.MethodGet, testCase.url, nil) + recorder := httptest.NewRecorder() + + middleware.ServeHTTP(recorder, request) + + body := testutils.ReadBody(t, recorder) + assert.Equal(t, testCase.statusCode, recorder.Code) + assert.Equal(t, testCase.expected, body) + }) + } + }) + + t.Run("query handling", func(t *testing.T) { + middleware := handler.NewUncorsRequestHandler( + handler.WithHTTPClient(mocks.NewHTTPClientMock(t)), + handler.WithURLReplacerFactory(mocks.NewURLReplacerFactoryMock(t)), + handler.WithLogger(logger), + handler.WithMappings(config.Mappings{ + {From: "*", To: "*", Mocks: config.Mocks{ + { + Path: userPath, + Response: config.Response{ + Code: http.StatusOK, + Raw: mock1Body, + }, + }, + { + Path: userPath, + Queries: map[string]string{ + "id": "17", + }, + Response: config.Response{ + Code: http.StatusCreated, + Raw: mock2Body, + }, + }, + { + Path: userPath, + Queries: map[string]string{ + "id": "99", + "token": "fe145b54563d9be1b2a476f56b0a412b", + }, + Response: config.Response{ + Code: http.StatusAccepted, + Raw: mock3Body, + }, + }, + }}, + }), + ) + + tests := []struct { + name string + url string + expected string + statusCode int + }{ + { + name: "queries is not set", + url: "http://localhost/api/user", + expected: mock1Body, + statusCode: http.StatusOK, + }, + { + name: "passed unsetted parameter", + url: "http://localhost/api/user?id=16", + expected: mock1Body, + statusCode: http.StatusOK, + }, + { + name: "passed parameter", + url: "http://localhost/api/user?id=17", + expected: mock2Body, + statusCode: http.StatusCreated, + }, + { + name: "passed one of multiple parameters", + url: "http://localhost/api/user?id=99", + expected: mock1Body, + statusCode: http.StatusOK, + }, + { + name: "passed all of multiple parameters", + url: "http://localhost/api/user?id=99&token=fe145b54563d9be1b2a476f56b0a412b", + expected: mock3Body, + statusCode: http.StatusAccepted, + }, + { + name: "passed extra parameters", + url: "http://localhost/api/user?id=99&token=fe145b54563d9be1b2a476f56b0a412b&demo=true", + expected: mock3Body, + statusCode: http.StatusAccepted, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + request := httptest.NewRequest(http.MethodGet, testCase.url, nil) + recorder := httptest.NewRecorder() + + middleware.ServeHTTP(recorder, request) + + body := testutils.ReadBody(t, recorder) + assert.Equal(t, testCase.statusCode, recorder.Code) + assert.Equal(t, testCase.expected, body) + }) + } + }) + + t.Run("header handling", func(t *testing.T) { + middleware := handler.NewUncorsRequestHandler( + handler.WithHTTPClient(mocks.NewHTTPClientMock(t)), + handler.WithURLReplacerFactory(mocks.NewURLReplacerFactoryMock(t)), + handler.WithLogger(logger), + handler.WithMappings(config.Mappings{ + {From: "*", To: "*", Mocks: config.Mocks{ + { + Path: userPath, + Response: config.Response{ + Code: http.StatusOK, + Raw: mock1Body, + }, + }, + { + Path: userPath, + Headers: map[string]string{ + headers.XCSRFToken: "de4e27987d054577b0edc0e828851724", + }, + Response: config.Response{ + Code: http.StatusCreated, + Raw: mock2Body, + }, + }, + { + Path: userPath, + Headers: map[string]string{ + userIDHeader: "99", + headers.XCSRFToken: "fe145b54563d9be1b2a476f56b0a412b", + }, + Response: config.Response{ + Code: http.StatusAccepted, + Raw: mock3Body, + }, + }, + }}, + }), + ) + + tests := []struct { + name string + url string + headers map[string]string + expected string + statusCode int + }{ + { + name: "headers is not set", + url: apiUser, + expected: mock1Body, + statusCode: http.StatusOK, + }, + { + name: "passed unsetted headers", + url: apiUser, + headers: map[string]string{ + headers.XCSRFToken: "55cc413b96026e833835a2c9a3f39c21", + }, + expected: mock1Body, + statusCode: http.StatusOK, + }, + { + name: "passed defined header", + url: apiUser, + headers: map[string]string{ + headers.XCSRFToken: "de4e27987d054577b0edc0e828851724", + }, + expected: mock2Body, + statusCode: http.StatusCreated, + }, + { + name: "passed one of multiple headers", + url: apiUser, + headers: map[string]string{ + userIDHeader: "99", + }, + expected: mock1Body, + statusCode: http.StatusOK, + }, + { + name: "passed all of multiple headers", + url: apiUser, + headers: map[string]string{ + userIDHeader: "99", + headers.XCSRFToken: "fe145b54563d9be1b2a476f56b0a412b", + }, + expected: mock3Body, + statusCode: http.StatusAccepted, + }, + { + name: "passed extra headers", + url: apiUser, + headers: map[string]string{ + userIDHeader: "99", + headers.XCSRFToken: "fe145b54563d9be1b2a476f56b0a412b", + headers.AcceptEncoding: "deflate", + }, + expected: mock3Body, + statusCode: http.StatusAccepted, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + request := httptest.NewRequest(http.MethodPost, testCase.url, nil) + for key, value := range testCase.headers { + request.Header.Add(key, value) + } + recorder := httptest.NewRecorder() + + middleware.ServeHTTP(recorder, request) + + body := testutils.ReadBody(t, recorder) + assert.Equal(t, testCase.statusCode, recorder.Code) + assert.Equal(t, testCase.expected, body) + }) + } + }) +} diff --git a/internal/helpers/clone.go b/internal/helpers/clone.go new file mode 100644 index 00000000..f2272075 --- /dev/null +++ b/internal/helpers/clone.go @@ -0,0 +1,22 @@ +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 { + cloned[key] = cloneable.Clone() + } else if cloneablePtr, ok := any(&value).(lo.Clonable[V]); ok { //nolint:gosec + cloned[key] = cloneablePtr.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..b4e193b7 --- /dev/null +++ b/internal/helpers/clone_test.go @@ -0,0 +1,122 @@ +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: "property 1"}, + "2": {Value: "property 2"}, + "3": {Value: "property 3"}, + } + + expected := map[string]clonableTestStruct{ + "1": {Value: "Cloned:property 1"}, + "2": {Value: "Cloned:property 2"}, + "3": {Value: "Cloned:property 3"}, + } + + 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) + }) + + 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) { + t.Helper() + + actual := helpers.CloneMap(data) + + assert.EqualValues(t, data, actual) + assert.NotSame(t, &data, &actual) +} diff --git a/internal/helpers/closer.go b/internal/helpers/closer.go new file mode 100644 index 00000000..a5e34c5e --- /dev/null +++ b/internal/helpers/closer.go @@ -0,0 +1,13 @@ +package helpers + +import "io" + +func CloseSafe(resource io.Closer) { + if resource == nil { + return + } + + if err := resource.Close(); err != nil { + panic(err) + } +} diff --git a/internal/helpers/closer_test.go b/internal/helpers/closer_test.go new file mode 100644 index 00000000..466a8d9c --- /dev/null +++ b/internal/helpers/closer_test.go @@ -0,0 +1,34 @@ +package helpers_test + +import ( + "testing" + + "github.com/evg4b/uncors/internal/helpers" + "github.com/evg4b/uncors/testing/mocks" + "github.com/evg4b/uncors/testing/testconstants" + "github.com/stretchr/testify/assert" +) + +func TestCloseSafe(t *testing.T) { + t.Run("should correct handle nil pointer", func(t *testing.T) { + assert.NotPanics(t, func() { + helpers.CloseSafe(nil) + }) + }) + + t.Run("correctly close resource without error", func(t *testing.T) { + assert.NotPanics(t, func() { + resource := mocks.NewCloserMock(t).CloseMock.Return(nil) + + helpers.CloseSafe(resource) + }) + }) + + t.Run("panics when resource close return error", func(t *testing.T) { + assert.Panics(t, func() { + resource := mocks.NewCloserMock(t).CloseMock.Return(testconstants.ErrTest1) + + helpers.CloseSafe(resource) + }) + }) +} diff --git a/internal/helpers/http.go b/internal/helpers/http.go new file mode 100644 index 00000000..3d345c44 --- /dev/null +++ b/internal/helpers/http.go @@ -0,0 +1,13 @@ +package helpers + +import "net/http" + +func NormaliseRequest(request *http.Request) { + request.URL.Host = request.Host + + if request.TLS != nil { + request.URL.Scheme = "https" + } else { + request.URL.Scheme = "http" + } +} diff --git a/internal/helpers/http_test.go b/internal/helpers/http_test.go new file mode 100644 index 00000000..a0d0d615 --- /dev/null +++ b/internal/helpers/http_test.go @@ -0,0 +1,55 @@ +package helpers_test + +import ( + "crypto/tls" + "net/http" + "testing" + + "github.com/evg4b/uncors/internal/helpers" + "github.com/evg4b/uncors/pkg/urlx" + "github.com/evg4b/uncors/testing/testconstants" + "github.com/evg4b/uncors/testing/testutils" + "github.com/stretchr/testify/assert" +) + +func TestNormaliseRequest(t *testing.T) { + url, err := urlx.Parse(testconstants.HTTPLocalhost) + testutils.CheckNoError(t, err) + + t.Run("set correct scheme", func(t *testing.T) { + t.Run("http", func(t *testing.T) { + request := &http.Request{ + URL: url, + Host: testconstants.Localhost, + } + + helpers.NormaliseRequest(request) + + assert.Equal(t, request.URL.Scheme, "http") + }) + + t.Run("https", func(t *testing.T) { + request := &http.Request{ + URL: url, + TLS: &tls.ConnectionState{}, + Host: testconstants.Localhost, + } + + helpers.NormaliseRequest(request) + + assert.Equal(t, request.URL.Scheme, "https") + }) + }) + + t.Run("fill url.host", func(t *testing.T) { + request := &http.Request{ + URL: url, + TLS: &tls.ConnectionState{}, + Host: testconstants.Localhost, + } + + helpers.NormaliseRequest(request) + + assert.Equal(t, request.URL.Host, testconstants.Localhost) + }) +} diff --git a/internal/infrastructure/cors.go b/internal/infra/cors.go similarity index 56% rename from internal/infrastructure/cors.go rename to internal/infra/cors.go index 6db23cab..34abebdf 100644 --- a/internal/infrastructure/cors.go +++ b/internal/infra/cors.go @@ -1,4 +1,4 @@ -package infrastructure +package infra import ( "net/http" @@ -6,8 +6,10 @@ import ( "github.com/go-http-utils/headers" ) +const allowMethods = "GET, PUT, POST, HEAD, TRACE, DELETE, PATCH, COPY, HEAD, LINK, OPTIONS" + func WriteCorsHeaders(header http.Header) { header.Set(headers.AccessControlAllowOrigin, "*") header.Set(headers.AccessControlAllowCredentials, "true") - header.Set(headers.AccessControlAllowMethods, "GET, PUT, POST, HEAD, TRACE, DELETE, PATCH, COPY, HEAD, LINK, OPTIONS") + header.Set(headers.AccessControlAllowMethods, allowMethods) } diff --git a/internal/infrastructure/cors_test.go b/internal/infra/cors_test.go similarity index 79% rename from internal/infrastructure/cors_test.go rename to internal/infra/cors_test.go index a3212001..4a3bd402 100644 --- a/internal/infrastructure/cors_test.go +++ b/internal/infra/cors_test.go @@ -1,11 +1,13 @@ -package infrastructure_test +package infra_test import ( "net/http" "testing" - "github.com/evg4b/uncors/internal/infrastructure" + "github.com/evg4b/uncors/internal/infra" + "github.com/evg4b/uncors/testing/testconstants" "github.com/go-http-utils/headers" + "github.com/stretchr/testify/assert" ) func TestWriteCorsHeaders(t *testing.T) { @@ -21,7 +23,7 @@ func TestWriteCorsHeaders(t *testing.T) { headers.AccessControlAllowOrigin: []string{"*"}, headers.AccessControlAllowCredentials: []string{"true"}, headers.AccessControlAllowMethods: []string{ - "GET, PUT, POST, HEAD, TRACE, DELETE, PATCH, COPY, HEAD, LINK, OPTIONS", + testconstants.AllMethods, }, }, }, @@ -36,7 +38,7 @@ func TestWriteCorsHeaders(t *testing.T) { headers.AccessControlAllowOrigin: []string{"*"}, headers.AccessControlAllowCredentials: []string{"true"}, headers.AccessControlAllowMethods: []string{ - "GET, PUT, POST, HEAD, TRACE, DELETE, PATCH, COPY, HEAD, LINK, OPTIONS", + testconstants.AllMethods, }, }, }, @@ -49,7 +51,7 @@ func TestWriteCorsHeaders(t *testing.T) { headers.AccessControlAllowOrigin: []string{"*"}, headers.AccessControlAllowCredentials: []string{"true"}, headers.AccessControlAllowMethods: []string{ - "GET, PUT, POST, HEAD, TRACE, DELETE, PATCH, COPY, HEAD, LINK, OPTIONS", + testconstants.AllMethods, }, "X-DATA": []string{"https://demo.com"}, }, @@ -57,7 +59,9 @@ func TestWriteCorsHeaders(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - infrastructure.WriteCorsHeaders(tt.header) + infra.WriteCorsHeaders(tt.header) + + assert.Equal(t, tt.expected, tt.header) }) } } diff --git a/internal/infra/http_error.go b/internal/infra/http_error.go new file mode 100644 index 00000000..8a356482 --- /dev/null +++ b/internal/infra/http_error.go @@ -0,0 +1,36 @@ +package infra + +import ( + "net/http" + + "github.com/evg4b/uncors/internal/sfmt" + "github.com/go-http-utils/headers" + "github.com/pterm/pterm" + "github.com/pterm/pterm/putils" +) + +var style = pterm.Style{} + +func HTTPError(writer http.ResponseWriter, err error) { + header := writer.Header() + header.Set(headers.ContentType, "text/plain; charset=utf-8") + header.Set(headers.XContentTypeOptions, "nosniff") + + writer.WriteHeader(http.StatusInternalServerError) + message := sfmt.Sprintf("%d Error", http.StatusInternalServerError) + + sfmt.Fprintln(writer) + sfmt.Fprintln(writer, pageHeader(message)) + sfmt.Fprintln(writer) + sfmt.Fprintln(writer, sfmt.Sprintf("Occurred error: %s", err)) +} + +func pageHeader(message string) string { + letters := putils.LettersFromStringWithStyle(message, &style) + text, err := pterm.DefaultBigText.WithLetters(letters).Srender() + if err != nil { + panic(err) + } + + return text +} diff --git a/internal/infra/http_error_test.go b/internal/infra/http_error_test.go new file mode 100644 index 00000000..5f6e14dc --- /dev/null +++ b/internal/infra/http_error_test.go @@ -0,0 +1,39 @@ +package infra_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/evg4b/uncors/internal/infra" + "github.com/evg4b/uncors/testing/testutils" + "github.com/go-http-utils/headers" + "github.com/stretchr/testify/assert" +) + +const expectedPage = ` +███████ ██████ ██████ ███████ ██████ ██████ ██████ ██████ +██ ██ ████ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██ ██ ██ ██ ██ ██ █████ ██████ ██████ ██ ██ ██████ + ██ ████ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██████ ██████ ███████ ██ ██ ██ ██ ██████ ██ ██ + + +Occurred error: net/http: abort Handler +` + +func TestHttpError(t *testing.T) { + recorder := httptest.NewRecorder() + infra.HTTPError(recorder, http.ErrAbortHandler) + + t.Run("write correct page", func(t *testing.T) { + assert.Equal(t, expectedPage, testutils.ReadBody(t, recorder)) + }) + + t.Run("write correct headers", func(t *testing.T) { + header := recorder.Header() + + assert.NotNil(t, header[headers.ContentType]) + assert.NotNil(t, header[headers.XContentTypeOptions]) + }) +} diff --git a/internal/infrastructure/httpclient.go b/internal/infra/httpclient.go similarity index 78% rename from internal/infrastructure/httpclient.go rename to internal/infra/httpclient.go index 765e8ad9..ea8f8227 100644 --- a/internal/infrastructure/httpclient.go +++ b/internal/infra/httpclient.go @@ -1,7 +1,6 @@ -package infrastructure +package infra import ( - "crypto/tls" "fmt" "net/http" "time" @@ -16,9 +15,6 @@ var defaultHTTPClient = http.Client{ return http.ErrUseLastResponse }, Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, //nolint: gosec - }, Proxy: http.ProxyFromEnvironment, }, Jar: nil, @@ -33,11 +29,7 @@ func MakeHTTPClient(proxy string) (*http.Client, error) { } httpClient := defaultHTTPClient - httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, //nolint: gosec - }, Proxy: http.ProxyURL(parsedURL), } diff --git a/internal/infrastructure/httpclient_internal_test.go b/internal/infra/httpclient_internal_test.go similarity index 98% rename from internal/infrastructure/httpclient_internal_test.go rename to internal/infra/httpclient_internal_test.go index ea3f39c6..389cebe0 100644 --- a/internal/infrastructure/httpclient_internal_test.go +++ b/internal/infra/httpclient_internal_test.go @@ -1,5 +1,5 @@ // nolint: lll -package infrastructure +package infra import ( "net/http" diff --git a/internal/infra/noop-logger.go b/internal/infra/noop-logger.go new file mode 100644 index 00000000..c78e1458 --- /dev/null +++ b/internal/infra/noop-logger.go @@ -0,0 +1,11 @@ +package infra + +type NoopLogger struct{} + +func (n NoopLogger) Infof(string, ...any) { + // Interface implementation +} + +func (n NoopLogger) Errorf(string, ...any) { + // Interface implementation +} diff --git a/internal/infrastructure/noop-logger_test.go b/internal/infra/noop-logger_test.go similarity index 80% rename from internal/infrastructure/noop-logger_test.go rename to internal/infra/noop-logger_test.go index 21f129a9..dd5c5eb7 100644 --- a/internal/infrastructure/noop-logger_test.go +++ b/internal/infra/noop-logger_test.go @@ -1,17 +1,17 @@ -package infrastructure_test +package infra_test import ( "bytes" "testing" - "github.com/evg4b/uncors/internal/infrastructure" + "github.com/evg4b/uncors/internal/infra" "github.com/evg4b/uncors/testing/testutils" "github.com/stretchr/testify/assert" ) func TestNoopLogger(t *testing.T) { testMessage := "test message" - noopLogger := infrastructure.NoopLogger{} + noopLogger := infra.NoopLogger{} t.Run("Infof do nothing", testutils.LogTest(func(t *testing.T, output *bytes.Buffer) { noopLogger.Infof(testMessage) diff --git a/internal/infrastructure/panic.go b/internal/infra/panic.go similarity index 85% rename from internal/infrastructure/panic.go rename to internal/infra/panic.go index 66cb38a5..24f50eaa 100644 --- a/internal/infrastructure/panic.go +++ b/internal/infra/panic.go @@ -1,6 +1,6 @@ //go:build release -package infrastructure +package infra func PanicInterceptor(action func(any)) { if recovered := recover(); recovered != nil { diff --git a/internal/infrastructure/panic_debug.go b/internal/infra/panic_debug.go similarity index 77% rename from internal/infrastructure/panic_debug.go rename to internal/infra/panic_debug.go index 22b9a4e0..e2c659a7 100644 --- a/internal/infrastructure/panic_debug.go +++ b/internal/infra/panic_debug.go @@ -1,6 +1,6 @@ //go:build !release -package infrastructure +package infra func PanicInterceptor(_ func(any)) { // stub method diff --git a/internal/infrastructure/panic_test.go b/internal/infra/panic_test.go similarity index 68% rename from internal/infrastructure/panic_test.go rename to internal/infra/panic_test.go index 418fab3e..24e8adaf 100644 --- a/internal/infrastructure/panic_test.go +++ b/internal/infra/panic_test.go @@ -1,11 +1,12 @@ //go:build release -package infrastructure +package infra_test import ( "errors" "testing" + "github.com/evg4b/uncors/internal/infra" "github.com/stretchr/testify/assert" ) @@ -31,20 +32,20 @@ func TestPanicInterceptor(t *testing.T) { }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for _, testCast := range tests { + t.Run(testCast.name, func(t *testing.T) { called := false assert.NotPanics(t, func() { - defer PanicInterceptor(func(data any) { + defer infra.PanicInterceptor(func(data any) { called = true - assert.Equal(t, tt.panicData, data) + assert.Equal(t, testCast.panicData, data) }) - panic(tt.panicData) + panic(testCast.panicData) }) - assert.Equal(t, tt.shouldBeCalled, called) + assert.Equal(t, testCast.shouldBeCalled, called) }) } } diff --git a/internal/infrastructure/noop-logger.go b/internal/infrastructure/noop-logger.go deleted file mode 100644 index 83c8f89a..00000000 --- a/internal/infrastructure/noop-logger.go +++ /dev/null @@ -1,7 +0,0 @@ -package infrastructure - -type NoopLogger struct{} - -func (n NoopLogger) Infof(string, ...any) {} - -func (n NoopLogger) Errorf(string, ...any) {} diff --git a/internal/log/log.go b/internal/log/log.go index ccf40f06..5b975419 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -1,24 +1,18 @@ package log import ( - "fmt" "io" - "os" + "github.com/evg4b/uncors/internal/sfmt" "github.com/pterm/pterm" ) -func Fatal(a ...any) { - Error(a...) - os.Exit(0) -} - func Error(a ...any) { errorPrinter.Println(a...) } func Errorf(template string, a ...any) { - Error(fmt.Sprintf(template, a...)) + Error(sfmt.Sprintf(template, a...)) } func Warning(a ...any) { @@ -26,7 +20,7 @@ func Warning(a ...any) { } func Warningf(template string, a ...any) { - Warning(fmt.Sprintf(template, a...)) + Warning(sfmt.Sprintf(template, a...)) } func Info(a ...any) { @@ -34,7 +28,7 @@ func Info(a ...any) { } func Infof(template string, a ...any) { - Info(fmt.Sprintf(template, a...)) + Info(sfmt.Sprintf(template, a...)) } func Debug(a ...any) { @@ -42,7 +36,7 @@ func Debug(a ...any) { } func Debugf(template string, a ...any) { - Debug(fmt.Sprintf(template, a...)) + Debug(sfmt.Sprintf(template, a...)) } func Print(a ...any) { diff --git a/internal/log/logger.go b/internal/log/logger.go index b012fc63..09370b17 100644 --- a/internal/log/logger.go +++ b/internal/log/logger.go @@ -8,6 +8,7 @@ import ( type PrefixedLogger struct { writer *pterm.PrefixPrinter + debug *pterm.PrefixPrinter } func NewLogger(name string, options ...LoggerOption) *PrefixedLogger { @@ -19,6 +20,14 @@ func NewLogger(name string, options ...LoggerOption) *PrefixedLogger { Text: name, }, }, + debug: &pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.DebugMessageStyle, + Prefix: pterm.Prefix{ + Text: name, + Style: &pterm.ThemeDefault.DebugPrefixStyle, + }, + Debugger: true, + }, } for _, option := range options { @@ -54,13 +63,13 @@ func (logger *PrefixedLogger) Infof(template string, v ...any) { func (logger *PrefixedLogger) Debug(v ...any) { if pterm.PrintDebugMessages { - logger.writer.Println(debugPrinter.Sprint(v...)) + logger.debug.Println(debugPrinter.Sprint(v...)) } } func (logger *PrefixedLogger) Debugf(template string, v ...any) { if pterm.PrintDebugMessages { - logger.writer.Println(debugPrinter.Sprintf(template, v...)) + logger.debug.Println(debugPrinter.Sprintf(template, v...)) } } diff --git a/internal/log/options.go b/internal/log/options.go index cf8071bf..a3475ed2 100644 --- a/internal/log/options.go +++ b/internal/log/options.go @@ -11,6 +11,7 @@ type LoggerOption = func(logger *PrefixedLogger) func WithOutput(writer io.Writer) LoggerOption { return func(logger *PrefixedLogger) { logger.writer.Writer = writer + logger.debug.Writer = writer } } diff --git a/internal/log/printresponse.go b/internal/log/printresponse.go index e5707830..17494644 100644 --- a/internal/log/printresponse.go +++ b/internal/log/printresponse.go @@ -1,14 +1,14 @@ package log import ( - "fmt" "net/http" + "github.com/evg4b/uncors/internal/sfmt" "github.com/pterm/pterm" ) func printResponse(response *http.Response) string { - prefix := fmt.Sprintf("%d %s", response.StatusCode, response.Request.Method) + prefix := sfmt.Sprintf("%d %s", response.StatusCode, response.Request.Method) printer := getPrefixPrinter(response.StatusCode, prefix) return printer.Sprint(response.Request.URL.String()) @@ -16,7 +16,7 @@ func printResponse(response *http.Response) string { func getPrefixPrinter(statusCode int, text string) pterm.PrefixPrinter { if statusCode < 100 || statusCode > 599 { - panic(fmt.Sprintf("status code %d is not supported", statusCode)) + panic(sfmt.Sprintf("status code %d is not supported", statusCode)) } if 100 <= statusCode && statusCode <= 199 { diff --git a/internal/log/standart_log_adaptor.go b/internal/log/standart_log_adaptor.go index 3ad2a4cd..a179537a 100644 --- a/internal/log/standart_log_adaptor.go +++ b/internal/log/standart_log_adaptor.go @@ -1,9 +1,9 @@ package log import ( - "github.com/pterm/pterm" - "log" + + "github.com/pterm/pterm" ) func StandardErrorLogAdapter() *log.Logger { diff --git a/internal/middlewares/mock/handler.go b/internal/middlewares/mock/handler.go deleted file mode 100644 index d4fd9d7a..00000000 --- a/internal/middlewares/mock/handler.go +++ /dev/null @@ -1,65 +0,0 @@ -package mock - -import ( - "net/http" - "time" - - "github.com/evg4b/uncors/internal/contracts" - "github.com/evg4b/uncors/internal/infrastructure" - "github.com/spf13/afero" -) - -type internalHandler struct { - response Response - logger contracts.Logger - fs afero.Fs - after func(duration time.Duration) <-chan time.Time -} - -func (handler *internalHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - response := handler.response - header := writer.Header() - - if response.Delay > 0 { - handler.logger.Debugf("Delay %s for %s", response.Delay, request.URL.RequestURI()) - ctx := request.Context() - select { - case <-ctx.Done(): - writer.WriteHeader(http.StatusServiceUnavailable) - handler.logger.Debugf("Delay for %s canceled", request.URL.RequestURI()) - - return - case <-handler.after(response.Delay): - } - } - - infrastructure.WriteCorsHeaders(header) - for key, value := range response.Headers { - header.Set(key, value) - } - - var err error - if len(handler.response.File) > 0 { - err = handler.serveFileContent(writer, request) - } else { - err = handler.serveRawContent(writer) - } - - if err != nil { - // TODO: add error handling - return - } - - handler.logger.PrintResponse(&http.Response{ - Request: request, - StatusCode: response.Code, - }) -} - -func normaliseCode(code int) int { - if code == 0 { - return http.StatusOK - } - - return code -} diff --git a/internal/middlewares/mock/make_mocked_routes.go b/internal/middlewares/mock/make_mocked_routes.go deleted file mode 100644 index f75669a7..00000000 --- a/internal/middlewares/mock/make_mocked_routes.go +++ /dev/null @@ -1,62 +0,0 @@ -package mock - -import ( - "time" - - "github.com/gorilla/mux" -) - -func (m *Middleware) makeMockedRoutes() { - var defaultMocks []Mock - - for _, mock := range m.mocks { - if len(mock.Queries) > 0 || len(mock.Headers) > 0 || len(mock.Method) > 0 { - route := m.router.NewRoute() - setPath(route, mock.Path) - setMethod(route, mock.Method) - setQueries(route, mock.Queries) - setHeaders(route, mock.Headers) - route.Handler(m.makeHandler(mock.Response)) - } else { - defaultMocks = append(defaultMocks, mock) - } - } - - for _, mock := range defaultMocks { - route := m.router.NewRoute() - setPath(route, mock.Path) - route.Handler(m.makeHandler(mock.Response)) - } -} - -func (m *Middleware) makeHandler(response Response) *internalHandler { - return &internalHandler{response, m.logger, m.fs, time.After} -} - -func setPath(route *mux.Route, path string) { - if len(path) > 0 { - route.Path(path) - } -} - -func setMethod(route *mux.Route, methods string) { - if len(methods) > 0 { - route.Methods(methods) - } -} - -func setQueries(route *mux.Route, queries map[string]string) { - if len(queries) > 0 { - for key, value := range queries { - route.Queries(key, value) - } - } -} - -func setHeaders(route *mux.Route, headers map[string]string) { - if len(headers) > 0 { - for key, value := range headers { - route.Headers(key, value) - } - } -} diff --git a/internal/middlewares/mock/middleware.go b/internal/middlewares/mock/middleware.go deleted file mode 100644 index 7b5b2434..00000000 --- a/internal/middlewares/mock/middleware.go +++ /dev/null @@ -1,36 +0,0 @@ -package mock - -import ( - "net/http" - - "github.com/evg4b/uncors/internal/contracts" - "github.com/gorilla/mux" - "github.com/spf13/afero" -) - -type Middleware struct { - router *mux.Router - next http.Handler - logger contracts.Logger - mocks []Mock - fs afero.Fs -} - -func NewMockMiddleware(options ...MiddlewareOption) *Middleware { - router := mux.NewRouter() - middleware := &Middleware{router: router, mocks: []Mock{}} - - for _, option := range options { - option(middleware) - } - - middleware.makeMockedRoutes() - router.NotFoundHandler = middleware.next - router.MethodNotAllowedHandler = middleware.next - - return middleware -} - -func (m *Middleware) ServeHTTP(response http.ResponseWriter, request *http.Request) { - m.router.ServeHTTP(response, request) -} diff --git a/internal/middlewares/mock/middleware_test.go b/internal/middlewares/mock/middleware_test.go deleted file mode 100644 index 62295325..00000000 --- a/internal/middlewares/mock/middleware_test.go +++ /dev/null @@ -1,410 +0,0 @@ -package mock_test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/evg4b/uncors/internal/middlewares/mock" - "github.com/evg4b/uncors/testing/mocks" - "github.com/evg4b/uncors/testing/testutils" - "github.com/stretchr/testify/assert" -) - -var ( - mock1Body = `{"mock": "mock number 1"}` - mock2Body = `{"mock": "mock number 2"}` - mock3Body = `{"mock": "mock number 3"}` - mock4Body = `{"mock": "mock number 4"}` -) - -var notFoundBody = "404 page not found\n" - -func TestMockMiddleware(t *testing.T) { - logger := mocks.NewNoopLogger(t) - - t.Run("request method handling", func(t *testing.T) { - t.Run("where mock method is not set allow method", func(t *testing.T) { - middleware := mock.NewMockMiddleware( - mock.WithLogger(logger), - mock.WithMocks([]mock.Mock{{ - Path: "/api", - Response: mock.Response{ - Code: http.StatusOK, - RawContent: mock1Body, - }, - }}), - ) - - methods := []string{ - http.MethodGet, - http.MethodHead, - http.MethodPost, - http.MethodPut, - http.MethodPatch, - http.MethodDelete, - http.MethodOptions, - http.MethodTrace, - } - - for _, method := range methods { - t.Run(method, func(t *testing.T) { - request := httptest.NewRequest(method, "http://localhost/api", nil) - recorder := httptest.NewRecorder() - - middleware.ServeHTTP(recorder, request) - - body := testutils.ReadBody(t, recorder) - assert.Equal(t, http.StatusOK, recorder.Code) - assert.Equal(t, mock1Body, body) - }) - } - }) - - t.Run("where method is set", func(t *testing.T) { - middleware := mock.NewMockMiddleware( - mock.WithLogger(logger), - mock.WithMocks([]mock.Mock{{ - Path: "/api", - Method: http.MethodPut, - Response: mock.Response{ - Code: http.StatusOK, - RawContent: mock1Body, - }, - }}), - ) - - t.Run("method is not matched", func(t *testing.T) { - methods := []string{ - http.MethodGet, - http.MethodHead, - http.MethodPost, - http.MethodPatch, - http.MethodDelete, - http.MethodOptions, - http.MethodTrace, - } - - for _, method := range methods { - t.Run(method, func(t *testing.T) { - request := httptest.NewRequest(method, "http://localhost/api", nil) - recorder := httptest.NewRecorder() - - middleware.ServeHTTP(recorder, request) - - assert.Equal(t, http.StatusMethodNotAllowed, recorder.Code) - }) - } - }) - - t.Run("method is matched", func(t *testing.T) { - request := httptest.NewRequest(http.MethodPut, "http://localhost/api", nil) - recorder := httptest.NewRecorder() - - middleware.ServeHTTP(recorder, request) - - body := testutils.ReadBody(t, recorder) - assert.Equal(t, http.StatusOK, recorder.Code) - assert.Equal(t, mock1Body, body) - }) - }) - }) - - t.Run("path handling", func(t *testing.T) { - middleware := mock.NewMockMiddleware( - mock.WithLogger(logger), - mock.WithMocks([]mock.Mock{ - { - Path: "/api/user", - Response: mock.Response{ - Code: http.StatusOK, - RawContent: mock1Body, - }, - }, - { - Path: "/api/user/{id:[0-9]+}", - Response: mock.Response{ - Code: http.StatusAccepted, - RawContent: mock2Body, - }, - }, - { - Path: "/api/{single-path/demo", - Response: mock.Response{ - Code: http.StatusBadRequest, - RawContent: mock3Body, - }, - }, - { - Path: `/api/v2/{multiple-path:[a-z-\/]+}/demo`, - Response: mock.Response{ - Code: http.StatusCreated, - RawContent: mock4Body, - }, - }, - }), - ) - - tests := []struct { - name string - url string - expected string - statusCode int - }{ - { - name: "direct path", - url: "https://localhost/api/user", - expected: mock1Body, - statusCode: http.StatusOK, - }, - { - name: "direct path with ending slash", - url: "https://localhost/api/user/", - expected: notFoundBody, - statusCode: http.StatusNotFound, - }, - { - name: "direct path with parameter", - url: "https://localhost/api/user/23", - expected: mock2Body, - statusCode: http.StatusAccepted, - }, - { - name: "direct path with incorrect parameter", - url: "https://localhost/api/user/unknow", - expected: notFoundBody, - statusCode: http.StatusNotFound, - }, - { - name: "path with subpath to single matching param", - url: "https://localhost/api/some-path/with-some-subpath/demo", - expected: notFoundBody, - statusCode: http.StatusNotFound, - }, - { - name: "path with subpath to multiple matching param", - url: "https://localhost/api/v2/some-path/with-some-subpath/demo", - expected: mock4Body, - statusCode: http.StatusCreated, - }, - } - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - request := httptest.NewRequest(http.MethodGet, testCase.url, nil) - recorder := httptest.NewRecorder() - - middleware.ServeHTTP(recorder, request) - - body := testutils.ReadBody(t, recorder) - assert.Equal(t, testCase.statusCode, recorder.Code) - assert.Equal(t, testCase.expected, body) - }) - } - }) - - t.Run("query handling", func(t *testing.T) { - middleware := mock.NewMockMiddleware( - mock.WithLogger(logger), - mock.WithMocks([]mock.Mock{ - { - Path: "/api/user", - Response: mock.Response{ - Code: http.StatusOK, - RawContent: mock1Body, - }, - }, - { - Path: "/api/user", - Queries: map[string]string{ - "id": "17", - }, - Response: mock.Response{ - Code: http.StatusCreated, - RawContent: mock2Body, - }, - }, - { - Path: "/api/user", - Queries: map[string]string{ - "id": "99", - "token": "fe145b54563d9be1b2a476f56b0a412b", - }, - Response: mock.Response{ - Code: http.StatusAccepted, - RawContent: mock3Body, - }, - }, - }), - ) - - tests := []struct { - name string - url string - expected string - statusCode int - }{ - { - name: "queries is not set", - url: "http://localhost/api/user", - expected: mock1Body, - statusCode: http.StatusOK, - }, - { - name: "passed unsetted parameter", - url: "http://localhost/api/user?id=16", - expected: mock1Body, - statusCode: http.StatusOK, - }, - { - name: "passed parameter", - url: "http://localhost/api/user?id=17", - expected: mock2Body, - statusCode: http.StatusCreated, - }, - { - name: "passed one of multiple parameters", - url: "http://localhost/api/user?id=99", - expected: mock1Body, - statusCode: http.StatusOK, - }, - { - name: "passed all of multiple parameters", - url: "http://localhost/api/user?id=99&token=fe145b54563d9be1b2a476f56b0a412b", - expected: mock3Body, - statusCode: http.StatusAccepted, - }, - { - name: "passed extra parameters", - url: "http://localhost/api/user?id=99&token=fe145b54563d9be1b2a476f56b0a412b&demo=true", - expected: mock3Body, - statusCode: http.StatusAccepted, - }, - } - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - request := httptest.NewRequest(http.MethodGet, testCase.url, nil) - recorder := httptest.NewRecorder() - - middleware.ServeHTTP(recorder, request) - - body := testutils.ReadBody(t, recorder) - assert.Equal(t, testCase.statusCode, recorder.Code) - assert.Equal(t, testCase.expected, body) - }) - } - }) - - t.Run("header handling", func(t *testing.T) { - middleware := mock.NewMockMiddleware( - mock.WithLogger(logger), - mock.WithMocks([]mock.Mock{ - { - Path: "/api/user", - Response: mock.Response{ - Code: http.StatusOK, - RawContent: mock1Body, - }, - }, - { - Path: "/api/user", - Headers: map[string]string{ - "Token": "de4e27987d054577b0edc0e828851724", - }, - Response: mock.Response{ - Code: http.StatusCreated, - RawContent: mock2Body, - }, - }, - { - Path: "/api/user", - Headers: map[string]string{ - "User-Id": "99", - "Token": "fe145b54563d9be1b2a476f56b0a412b", - }, - Response: mock.Response{ - Code: http.StatusAccepted, - RawContent: mock3Body, - }, - }, - }), - ) - - tests := []struct { - name string - url string - headers map[string]string - expected string - statusCode int - }{ - { - name: "headers is not set", - url: "https://localhost/api/user", - expected: mock1Body, - statusCode: http.StatusOK, - }, - { - name: "passed unsetted headers", - url: "https://localhost/api/user", - headers: map[string]string{ - "Token": "55cc413b96026e833835a2c9a3f39c21", - }, - expected: mock1Body, - statusCode: http.StatusOK, - }, - { - name: "passed defined header", - url: "https://localhost/api/user", - headers: map[string]string{ - "Token": "de4e27987d054577b0edc0e828851724", - }, - expected: mock2Body, - statusCode: http.StatusCreated, - }, - { - name: "passed one of multiple headers", - url: "https://localhost/api/user", - headers: map[string]string{ - "User-Id": "99", - }, - expected: mock1Body, - statusCode: http.StatusOK, - }, - { - name: "passed all of multiple headers", - url: "https://localhost/api/user", - headers: map[string]string{ - "User-Id": "99", - "Token": "fe145b54563d9be1b2a476f56b0a412b", - }, - expected: mock3Body, - statusCode: http.StatusAccepted, - }, - { - name: "passed extra headers", - url: "https://localhost/api/user", - headers: map[string]string{ - "User-Id": "99", - "Token": "fe145b54563d9be1b2a476f56b0a412b", - "Accept-Encoding": "deflate", - }, - expected: mock3Body, - statusCode: http.StatusAccepted, - }, - } - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - request := httptest.NewRequest(http.MethodPost, testCase.url, nil) - for key, value := range testCase.headers { - request.Header.Add(key, value) - } - recorder := httptest.NewRecorder() - - middleware.ServeHTTP(recorder, request) - - body := testutils.ReadBody(t, recorder) - assert.Equal(t, testCase.statusCode, recorder.Code) - assert.Equal(t, testCase.expected, body) - }) - } - }) -} diff --git a/internal/middlewares/mock/model.go b/internal/middlewares/mock/model.go deleted file mode 100644 index 62740af5..00000000 --- a/internal/middlewares/mock/model.go +++ /dev/null @@ -1,21 +0,0 @@ -package mock - -import ( - "time" -) - -type Response struct { - Code int `mapstructure:"code"` - Headers map[string]string `mapstructure:"headers"` - RawContent string `mapstructure:"raw-content"` - File string `mapstructure:"file"` - Delay time.Duration `mapstructure:"delay"` -} - -type Mock struct { - Path string `mapstructure:"path"` - Method string `mapstructure:"method"` - Queries map[string]string `mapstructure:"queries"` - Headers map[string]string `mapstructure:"headers"` - Response Response `mapstructure:"response"` -} diff --git a/internal/middlewares/mock/serve_raw_content.go b/internal/middlewares/mock/serve_raw_content.go deleted file mode 100644 index 2c836a1d..00000000 --- a/internal/middlewares/mock/serve_raw_content.go +++ /dev/null @@ -1,24 +0,0 @@ -package mock - -import ( - "fmt" - "net/http" - - "github.com/go-http-utils/headers" -) - -func (handler *internalHandler) serveRawContent(writer http.ResponseWriter) error { - response := handler.response - header := writer.Header() - if len(header.Get(headers.ContentType)) == 0 { - contentType := http.DetectContentType([]byte(response.RawContent)) - header.Set(headers.ContentType, contentType) - } - - writer.WriteHeader(normaliseCode(response.Code)) - if _, err := fmt.Fprint(writer, response.RawContent); err != nil { - return fmt.Errorf("failed to write mock content: %w", err) - } - - return nil -} diff --git a/internal/middlewares/proxy/option.go b/internal/middlewares/proxy/option.go deleted file mode 100644 index d724df49..00000000 --- a/internal/middlewares/proxy/option.go +++ /dev/null @@ -1,17 +0,0 @@ -package proxy - -import ( - "net/http" - - "github.com/evg4b/uncors/internal/infrastructure" -) - -func (m *Middleware) makeOptionsResponse(writer http.ResponseWriter, req *http.Request) error { - infrastructure.WriteCorsHeaders(writer.Header()) - m.logger.PrintResponse(&http.Response{ - StatusCode: http.StatusOK, - Request: req, - }) - - return nil -} diff --git a/internal/middlewares/proxy/options.go b/internal/middlewares/proxy/options.go deleted file mode 100644 index 256cde51..00000000 --- a/internal/middlewares/proxy/options.go +++ /dev/null @@ -1,25 +0,0 @@ -package proxy - -import ( - "github.com/evg4b/uncors/internal/contracts" -) - -type MiddlewareOption = func(*Middleware) - -func WithURLReplacerFactory(replacerFactory URLReplacerFactory) MiddlewareOption { - return func(m *Middleware) { - m.replacers = replacerFactory - } -} - -func WithHTTPClient(http contracts.HTTPClient) MiddlewareOption { - return func(m *Middleware) { - m.http = http - } -} - -func WithLogger(logger contracts.Logger) MiddlewareOption { - return func(m *Middleware) { - m.logger = logger - } -} diff --git a/internal/server/atomic_bool_test.go b/internal/server/atomic_bool_test.go index 8378bc9b..5b9be3a9 100644 --- a/internal/server/atomic_bool_test.go +++ b/internal/server/atomic_bool_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/evg4b/uncors/internal/server" - "github.com/go-playground/assert/v2" ) diff --git a/internal/server/server.go b/internal/server/server.go index 8d9167d3..5000091a 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,8 +7,8 @@ import ( "net/http" "time" + "github.com/evg4b/uncors/internal/helpers" "github.com/evg4b/uncors/internal/log" - "golang.org/x/net/context" ) @@ -17,8 +17,10 @@ type UncorsServer struct { inShutdown AtomicBool } -const readHeaderTimeout = 30 * time.Second -const shutdownTimeout = 15 * time.Second +const ( + readHeaderTimeout = 30 * time.Second + shutdownTimeout = 15 * time.Second +) func NewUncorsServer(ctx context.Context, handler http.Handler) *UncorsServer { globalCtx, globalCtxCancel := context.WithCancel(ctx) @@ -27,8 +29,11 @@ func NewUncorsServer(ctx context.Context, handler http.Handler) *UncorsServer { return globalCtx }, ReadHeaderTimeout: readHeaderTimeout, - Handler: handler, - ErrorLog: log.StandardErrorLogAdapter(), + Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + helpers.NormaliseRequest(request) + handler.ServeHTTP(writer, request) + }), + ErrorLog: log.StandardErrorLogAdapter(), } server.RegisterOnShutdown(globalCtxCancel) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 56274392..befe0808 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -2,14 +2,15 @@ package server_test import ( "context" - "fmt" "io" "net/http" "net/url" "testing" "time" + "github.com/evg4b/uncors/internal/helpers" "github.com/evg4b/uncors/internal/server" + "github.com/evg4b/uncors/internal/sfmt" "github.com/evg4b/uncors/testing/testutils" "github.com/stretchr/testify/assert" ) @@ -20,10 +21,7 @@ func TestNewUncorsServer(t *testing.T) { var handler http.HandlerFunc = func(w http.ResponseWriter, _r *http.Request) { w.WriteHeader(http.StatusOK) - _, err := fmt.Fprint(w, expectedResponse) - if err != nil { - panic(err) - } + sfmt.Fprint(w, expectedResponse) } t.Run("handle request", func(t *testing.T) { @@ -45,9 +43,7 @@ func TestNewUncorsServer(t *testing.T) { res, err := http.DefaultClient.Do(&http.Request{URL: uri, Method: http.MethodGet}) testutils.CheckNoError(t, err) - defer func() { - testutils.CheckNoError(t, res.Body.Close()) - }() + defer helpers.CloseSafe(res.Body) data, err := io.ReadAll(res.Body) testutils.CheckNoError(t, err) @@ -78,9 +74,7 @@ func TestNewUncorsServer(t *testing.T) { response, err := httpClient.Do(&http.Request{URL: uri, Method: http.MethodGet}) testutils.CheckNoError(t, err) - defer func() { - testutils.CheckNoError(t, response.Body.Close()) - }() + defer helpers.CloseSafe(response.Body) actualResponse, err := io.ReadAll(response.Body) testutils.CheckNoError(t, err) @@ -91,8 +85,7 @@ func TestNewUncorsServer(t *testing.T) { t.Run("run already stopped server", func(t *testing.T) { uncorsServer := server.NewUncorsServer(ctx, handler) - err := uncorsServer.Close() - testutils.CheckNoServerError(t, err) + testutils.CheckNoServerError(t, uncorsServer.Close()) t.Run("HTTP", func(t *testing.T) { err := uncorsServer.ListenAndServe("127.0.0.1:0") diff --git a/internal/sfmt/print.go b/internal/sfmt/print.go new file mode 100644 index 00000000..9ae52767 --- /dev/null +++ b/internal/sfmt/print.go @@ -0,0 +1,28 @@ +package sfmt + +import ( + "fmt" + "io" +) + +func Fprint(w io.Writer, payload ...any) { + if _, err := fmt.Fprint(w, payload...); err != nil { + panic(err) + } +} + +func Fprintf(w io.Writer, format string, a ...any) { + if _, err := fmt.Fprintf(w, format, a...); err != nil { + panic(err) + } +} + +func Sprintf(format string, a ...any) string { + return fmt.Sprintf(format, a...) +} + +func Fprintln(w io.Writer, a ...any) { + if _, err := fmt.Fprintln(w, a...); err != nil { + panic(err) + } +} diff --git a/internal/sfmt/print_test.go b/internal/sfmt/print_test.go new file mode 100644 index 00000000..51de9369 --- /dev/null +++ b/internal/sfmt/print_test.go @@ -0,0 +1,82 @@ +package sfmt_test + +import ( + "strings" + "testing" + + "github.com/evg4b/uncors/internal/sfmt" + "github.com/evg4b/uncors/testing/mocks" + "github.com/evg4b/uncors/testing/testconstants" + "github.com/stretchr/testify/assert" +) + +const ( + rawPayload = "test-data" + fPayload = "print formatted %s %d" + fPayloadExpected = "print formatted string 555" +) + +var fPayloadArgs = []any{"string", 555} + +func TestFprint(t *testing.T) { + t.Run("print correctly", func(t *testing.T) { + writer := &strings.Builder{} + + sfmt.Fprint(writer, rawPayload) + + assert.Equal(t, rawPayload, writer.String()) + }) + + t.Run("panic on error", func(t *testing.T) { + assert.Panics(t, func() { + writer := mocks.NewWriterMock(t). + WriteMock.Return(0, testconstants.ErrTest1) + + sfmt.Fprint(writer, rawPayload) + }) + }) +} + +func TestFprintf(t *testing.T) { + t.Run("print correctly", func(t *testing.T) { + writer := &strings.Builder{} + + sfmt.Fprintf(writer, fPayload, fPayloadArgs...) + + assert.Equal(t, fPayloadExpected, writer.String()) + }) + + t.Run("panic on error", func(t *testing.T) { + assert.Panics(t, func() { + writer := mocks.NewWriterMock(t). + WriteMock.Return(0, testconstants.ErrTest1) + + sfmt.Fprintf(writer, fPayload, fPayloadArgs...) + }) + }) +} + +func TestFprintln(t *testing.T) { + t.Run("print correctly", func(t *testing.T) { + writer := &strings.Builder{} + + sfmt.Fprintln(writer, rawPayload) + + assert.Equal(t, rawPayload+"\n", writer.String()) + }) + + t.Run("panic on error", func(t *testing.T) { + assert.Panics(t, func() { + writer := mocks.NewWriterMock(t). + WriteMock.Return(0, testconstants.ErrTest1) + + sfmt.Fprintln(writer, rawPayload) + }) + }) +} + +func TestSprintf(t *testing.T) { + actual := sfmt.Sprintf(fPayload, fPayloadArgs...) + + assert.Equal(t, fPayloadExpected, actual) +} diff --git a/internal/ui/loggers.go b/internal/ui/loggers.go index 9f6ee47a..15876392 100644 --- a/internal/ui/loggers.go +++ b/internal/ui/loggers.go @@ -14,3 +14,8 @@ var MockLogger = log.NewLogger(" MOCK ", log.WithStyle(&pterm.Style{ pterm.FgBlack, pterm.BgLightMagenta, })) + +var StaticLogger = log.NewLogger("STATIC ", log.WithStyle(&pterm.Style{ + pterm.FgBlack, + pterm.BgLightYellow, +})) diff --git a/internal/ui/logo.go b/internal/ui/logo.go index ee75075a..b873e58c 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -1,10 +1,9 @@ package ui import ( - "fmt" "strings" - "github.com/evg4b/uncors/internal/log" + "github.com/evg4b/uncors/internal/sfmt" "github.com/pterm/pterm" "github.com/pterm/pterm/putils" ) @@ -12,7 +11,7 @@ import ( func Logo(version string) string { logoLength := 51 versionLine := strings.Repeat(" ", logoLength) - versionSuffix := fmt.Sprintf("version: %s", version) + versionSuffix := sfmt.Sprintf("version: %s", version) versionPrefix := versionLine[:logoLength-len(versionSuffix)] logo, _ := pterm.DefaultBigText. @@ -24,21 +23,10 @@ func Logo(version string) string { var builder strings.Builder - if _, err := fmt.Fprintln(&builder); err != nil { - log.Fatal(err) - } - - if _, err := fmt.Fprint(&builder, logo); err != nil { - log.Fatal(err) - } - - if _, err := fmt.Fprint(&builder, versionPrefix, versionSuffix); err != nil { - log.Fatal(err) - } - - if _, err := fmt.Fprintln(&builder); err != nil { - log.Fatal(err) - } + sfmt.Fprintln(&builder) + sfmt.Fprint(&builder, logo) + sfmt.Fprint(&builder, versionPrefix, versionSuffix) + sfmt.Fprintln(&builder) return builder.String() } diff --git a/internal/ui/mappings.go b/internal/ui/mappings.go deleted file mode 100644 index 1dffd11c..00000000 --- a/internal/ui/mappings.go +++ /dev/null @@ -1,30 +0,0 @@ -package ui - -import ( - "fmt" - "strings" - - "github.com/evg4b/uncors/internal/middlewares/mock" -) - -func Mappings(mappings map[string]string, mocksDefs []mock.Mock) string { - var builder strings.Builder - - for source, target := range mappings { - if strings.HasPrefix(source, "https:") { - builder.WriteString(fmt.Sprintf("PROXY: %s => %s\n", source, target)) - } - } - for source, target := range mappings { - if strings.HasPrefix(source, "http:") { - builder.WriteString(fmt.Sprintf("PROXY: %s => %s\n", source, target)) - } - } - if len(mocksDefs) > 0 { - builder.WriteString(fmt.Sprintf("MOCKS: %d mock(s) registered", len(mocksDefs))) - } - - builder.WriteString("\n") - - return builder.String() -} diff --git a/internal/ui/mappings_test.go b/internal/ui/mappings_test.go deleted file mode 100644 index b720e932..00000000 --- a/internal/ui/mappings_test.go +++ /dev/null @@ -1,70 +0,0 @@ -//nolint:lll -package ui_test - -import ( - "testing" - - "github.com/evg4b/uncors/internal/middlewares/mock" - "github.com/evg4b/uncors/internal/ui" - "github.com/stretchr/testify/assert" -) - -func TestMappings(t *testing.T) { - tests := []struct { - name string - mappings map[string]string - mocksDefs []mock.Mock - expected string - }{ - { - name: "no mapping and no mocks", - expected: "\n", - }, - { - name: "http mapping only", - mappings: map[string]string{ - "http://localhost": "https://github.com", - }, - expected: "PROXY: http://localhost => https://github.com\n\n", - }, - { - name: "http and https mappings", - mappings: map[string]string{ - "http://localhost": "https://github.com", - "https://localhost": "https://github.com", - }, - expected: "PROXY: https://localhost => https://github.com\nPROXY: http://localhost => https://github.com\n\n", - }, - { - name: "one mock only", - mocksDefs: []mock.Mock{ - {}, - }, - expected: "MOCKS: 1 mock(s) registered\n", - }, - { - name: "2 mocks only", - mocksDefs: []mock.Mock{ - {}, {}, {}, - }, - expected: "MOCKS: 3 mock(s) registered\n", - }, - { - name: "mapping and mocks", - mappings: map[string]string{ - "http://localhost": "https://github.com", - "https://localhost": "https://github.com", - }, - mocksDefs: []mock.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) - assert.Equal(t, tt.expected, actual) - }) - } -} diff --git a/internal/urlreplacer/factory.go b/internal/urlreplacer/factory.go index 9244374a..49a389d2 100644 --- a/internal/urlreplacer/factory.go +++ b/internal/urlreplacer/factory.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "net/url" + + "github.com/evg4b/uncors/internal/config" ) type mapping struct { @@ -22,22 +24,22 @@ var ( ErrMappingNotSpecified = errors.New("you must specify at least one mapping") ) -func NewURLReplacerFactory(urlMappings map[string]string) (*Factory, error) { +func NewURLReplacerFactory(urlMappings config.Mappings) (*Factory, error) { if len(urlMappings) < 1 { return nil, ErrMappingNotSpecified } var mappings []mapping //nolint:prealloc - for sourceURL, targetURL := range urlMappings { - target, source, err := replacers(sourceURL, targetURL) + for _, urlMapping := range urlMappings { + target, source, err := replacers(urlMapping.From, urlMapping.To) if err != nil { return nil, fmt.Errorf("failed to configure url mappings: %w", err) } mappings = append(mappings, mapping{ - rawSource: sourceURL, + rawSource: urlMapping.From, source: source, - rawTarget: targetURL, + rawTarget: urlMapping.To, target: target, }) } diff --git a/internal/urlreplacer/factory_test.go b/internal/urlreplacer/factory_test.go index 29c9d55d..9697a33c 100644 --- a/internal/urlreplacer/factory_test.go +++ b/internal/urlreplacer/factory_test.go @@ -3,8 +3,10 @@ package urlreplacer_test import ( "testing" + "github.com/evg4b/uncors/internal/config" "github.com/evg4b/uncors/internal/urlreplacer" "github.com/evg4b/uncors/pkg/urlx" + "github.com/evg4b/uncors/testing/testconstants" "github.com/evg4b/uncors/testing/testutils" "github.com/stretchr/testify/assert" ) @@ -13,22 +15,22 @@ func TestNewUrlReplacerFactory(t *testing.T) { t.Run("should return error when", func(t *testing.T) { tests := []struct { name string - mapping map[string]string + mapping config.Mappings }{ { name: "mappings is empty", - mapping: make(map[string]string), + mapping: make(config.Mappings, 0), }, { name: "source url is incorrect", - mapping: map[string]string{ - string(rune(0x7f)): "https://github.com", + mapping: config.Mappings{ + {From: string(rune(0x7f)), To: testconstants.HTTPSGithub}, }, }, { name: "target url is incorrect ", - mapping: map[string]string{ - "localhost": string(rune(0x7f)), + mapping: config.Mappings{ + {From: testconstants.Localhost, To: string(rune(0x7f))}, }, }, } @@ -43,8 +45,8 @@ func TestNewUrlReplacerFactory(t *testing.T) { }) t.Run("should return replacers", func(t *testing.T) { - actual, err := urlreplacer.NewURLReplacerFactory(map[string]string{ - "localhost": "https://github.com", + actual, err := urlreplacer.NewURLReplacerFactory(config.Mappings{ + {From: testconstants.Localhost, To: testconstants.HTTPSGithub}, }) assert.NotNil(t, actual) @@ -53,9 +55,9 @@ func TestNewUrlReplacerFactory(t *testing.T) { } func TestFactoryMake(t *testing.T) { - factory, err := urlreplacer.NewURLReplacerFactory(map[string]string{ - "http://server1.com": "https://mappedserver1.com", - "https://server2.com": "https://mappedserver2.com", + factory, err := urlreplacer.NewURLReplacerFactory(config.Mappings{ + {From: "http://server1.com", To: "https://mappedserver1.com"}, + {From: "https://server2.com", To: "https://mappedserver2.com"}, }) testutils.CheckNoError(t, err) diff --git a/internal/urlreplacer/helpers.go b/internal/urlreplacer/helpers.go index caf2fc2e..98af6811 100644 --- a/internal/urlreplacer/helpers.go +++ b/internal/urlreplacer/helpers.go @@ -6,6 +6,7 @@ import ( "regexp" "strings" + "github.com/evg4b/uncors/internal/sfmt" "github.com/evg4b/uncors/pkg/urlx" ) @@ -24,7 +25,7 @@ func wildCardToRegexp(parsedPattern *url.URL) (*regexp.Regexp, int, error) { for index, literal := range parts { if index > 0 { count++ - fmt.Fprintf(&result, "(?P.+)", count) + sfmt.Fprintf(&result, "(?P.+)", count) } if _, err := result.WriteString(regexp.QuoteMeta(literal)); err != nil { @@ -43,31 +44,22 @@ func wildCardToRegexp(parsedPattern *url.URL) (*regexp.Regexp, int, error) { return compiledRegexp, count, nil } -func wildCardToReplacePattern(parsedPattern *url.URL) (string, int, error) { +func wildCardToReplacePattern(parsedPattern *url.URL) (string, int) { result := &strings.Builder{} var count int - if _, err := fmt.Fprint(result, "${scheme}"); err != nil { - return "", count, fmt.Errorf("filed to build url glob: %w", err) - } + sfmt.Fprint(result, "${scheme}") for i, literal := range strings.Split(parsedPattern.Host, "*") { if i > 0 { count++ - if _, err := fmt.Fprintf(result, "${part%d}", count); err != nil { - return "", count, fmt.Errorf("filed to build url glob: %w", err) - } + sfmt.Fprintf(result, "${part%d}", count) } - _, err := fmt.Fprint(result, literal) - if err != nil { - return "", count, fmt.Errorf("filed to build url glob: %w", err) - } + sfmt.Fprint(result, literal) } - if _, err := fmt.Fprint(result, "${path}"); err != nil { - return "", count, fmt.Errorf("filed to build url glob: %w", err) - } + sfmt.Fprint(result, "${path}") - return result.String(), count, nil + return result.String(), count } diff --git a/internal/urlreplacer/helpers_internal_test.go b/internal/urlreplacer/helpers_internal_test.go index 21a734f1..5eb18851 100644 --- a/internal/urlreplacer/helpers_internal_test.go +++ b/internal/urlreplacer/helpers_internal_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/evg4b/uncors/pkg/urlx" + "github.com/evg4b/uncors/testing/testconstants" "github.com/evg4b/uncors/testing/testutils" "github.com/stretchr/testify/assert" ) @@ -16,8 +17,8 @@ var testCases = []struct { expectedPattern string }{ { - name: "localhost", - url: "localhost", + name: testconstants.Localhost, + url: testconstants.Localhost, expectedRegexp: `^(?P(http(s?):)?\/\/)?localhost(:\d+)?(?P[\/?].*)?$`, expectedPattern: "${scheme}localhost${path}", }, @@ -100,7 +101,7 @@ func TestWildCardToRegexp(t *testing.T) { }{ { name: "no wildcards", - url: "localhost", + url: testconstants.Localhost, expected: 0, }, { @@ -161,9 +162,8 @@ func TestWildCardToReplacePattern(t *testing.T) { parsedPattern, err := urlx.Parse(testCase.url) testutils.CheckNoError(t, err) - pattern, _, err := wildCardToReplacePattern(parsedPattern) + pattern, _ := wildCardToReplacePattern(parsedPattern) - assert.NoError(t, err) assert.Equal(t, testCase.expectedPattern, pattern) }) } @@ -177,7 +177,7 @@ func TestWildCardToReplacePattern(t *testing.T) { }{ { name: "no wildcards", - url: "localhost", + url: testconstants.Localhost, expected: 0, }, { @@ -201,7 +201,7 @@ func TestWildCardToReplacePattern(t *testing.T) { parsedPattern, err := urlx.Parse(testCase.url) testutils.CheckNoError(t, err) - _, count, err := wildCardToReplacePattern(parsedPattern) + _, count := wildCardToReplacePattern(parsedPattern) assert.NoError(t, err) assert.Equal(t, testCase.expected, count) diff --git a/internal/urlreplacer/hooks.go b/internal/urlreplacer/hooks.go index e3997668..c37d3083 100644 --- a/internal/urlreplacer/hooks.go +++ b/internal/urlreplacer/hooks.go @@ -1,9 +1,9 @@ package urlreplacer -import "fmt" +import "github.com/evg4b/uncors/internal/sfmt" func schemeHookFactory(targetScheme string) hook { - forceScheme := fmt.Sprintf("%s://", targetScheme) + forceScheme := sfmt.Sprintf("%s://", targetScheme) return func(scheme string) string { if len(scheme) > 0 { diff --git a/internal/urlreplacer/mappings.go b/internal/urlreplacer/mappings.go deleted file mode 100644 index 59c248ba..00000000 --- a/internal/urlreplacer/mappings.go +++ /dev/null @@ -1,65 +0,0 @@ -package urlreplacer - -import ( - "fmt" - "net" - "net/url" - "strconv" - "strings" - - "github.com/evg4b/uncors/pkg/urlx" -) - -const ( - httpScheme = "http" - defaultHTTPPort = 80 -) - -const ( - httpsScheme = "https" - defaultHTTPSPort = 443 -) - -func NormaliseMappings(mappings map[string]string, httpPort, httpsPort int, useHTTPS bool) (map[string]string, error) { - processedMappings := map[string]string{} - for source, target := range mappings { - sourceURL, err := urlx.Parse(source) - if err != nil { - return nil, fmt.Errorf("failed to parse source url: %w", err) - } - - if isApplicableScheme(sourceURL.Scheme, httpScheme) { - normalisedSource := assignPortAndScheme(*sourceURL, httpScheme, httpPort) - processedMappings[normalisedSource] = target - } - - if useHTTPS && isApplicableScheme(sourceURL.Scheme, httpsScheme) { - normalisedSource := assignPortAndScheme(*sourceURL, httpsScheme, httpsPort) - processedMappings[normalisedSource] = target - } - } - - 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/urlreplacer/mappings_test.go b/internal/urlreplacer/mappings_test.go deleted file mode 100644 index 1bf0ae44..00000000 --- a/internal/urlreplacer/mappings_test.go +++ /dev/null @@ -1,201 +0,0 @@ -// nolint: dupl -package urlreplacer_test - -import ( - "testing" - - "github.com/evg4b/uncors/internal/urlreplacer" - "github.com/stretchr/testify/assert" -) - -func TestNormaliseMappings(t *testing.T) { - t.Run("custom port handling", func(t *testing.T) { - httpPort, httpsPort := 3000, 3001 - testsCases := []struct { - name string - mappings map[string]string - expected map[string]string - useHTTPS bool - }{ - { - name: "correctly set http and https ports", - mappings: map[string]string{ - "localhost": "github.com", - }, - expected: map[string]string{ - "http://localhost:3000": "github.com", - "https://localhost:3001": "github.com", - }, - useHTTPS: true, - }, - { - name: "correctly set http port", - mappings: map[string]string{ - "http://localhost": "https://github.com", - }, - expected: map[string]string{ - "http://localhost:3000": "https://github.com", - }, - useHTTPS: true, - }, - { - name: "correctly set https port", - mappings: map[string]string{ - "https://localhost": "https://github.com", - }, - expected: map[string]string{ - "https://localhost:3001": "https://github.com", - }, - useHTTPS: true, - }, - { - name: "correctly set mixed schemes", - mappings: map[string]string{ - "host1": "https://github.com", - "host2": "http://github.com", - "http://host3": "http://api.github.com", - "https://host4": "https://api.github.com", - }, - expected: map[string]string{ - "http://host1:3000": "https://github.com", - "https://host1:3001": "https://github.com", - "http://host2:3000": "http://github.com", - "https://host2:3001": "http://github.com", - "http://host3:3000": "http://api.github.com", - "https://host4:3001": "https://api.github.com", - }, - useHTTPS: true, - }, - } - for _, testCase := range testsCases { - t.Run(testCase.name, func(t *testing.T) { - actual, err := urlreplacer.NormaliseMappings( - testCase.mappings, - httpPort, - httpsPort, - testCase.useHTTPS, - ) - - assert.NoError(t, err) - assert.EqualValues(t, testCase.expected, actual) - }) - } - }) - - t.Run("default port handling", func(t *testing.T) { - httpPort, httpsPort := 80, 443 - testsCases := []struct { - name string - mappings map[string]string - expected map[string]string - useHTTPS bool - }{ - { - name: "correctly set http and https ports", - mappings: map[string]string{ - "localhost": "github.com", - }, - expected: map[string]string{ - "http://localhost": "github.com", - "https://localhost": "github.com", - }, - useHTTPS: true, - }, - { - name: "correctly set http port", - mappings: map[string]string{ - "http://localhost": "https://github.com", - }, - expected: map[string]string{ - "http://localhost": "https://github.com", - }, - useHTTPS: true, - }, - { - name: "correctly set https port", - mappings: map[string]string{ - "https://localhost": "https://github.com", - }, - expected: map[string]string{ - "https://localhost": "https://github.com", - }, - useHTTPS: true, - }, - { - name: "correctly set mixed schemes", - mappings: map[string]string{ - "host1": "https://github.com", - "host2": "http://github.com", - "http://host3": "http://api.github.com", - "https://host4": "https://api.github.com", - }, - expected: map[string]string{ - "http://host1": "https://github.com", - "https://host1": "https://github.com", - "http://host2": "http://github.com", - "https://host2": "http://github.com", - "http://host3": "http://api.github.com", - "https://host4": "https://api.github.com", - }, - useHTTPS: true, - }, - } - for _, testCase := range testsCases { - t.Run(testCase.name, func(t *testing.T) { - actual, err := urlreplacer.NormaliseMappings( - testCase.mappings, - httpPort, - httpsPort, - testCase.useHTTPS, - ) - - assert.NoError(t, err) - assert.EqualValues(t, testCase.expected, actual) - }) - } - }) - - t.Run("incorrect mappings", func(t *testing.T) { - testsCases := []struct { - name string - mappings map[string]string - httpPort int - httpsPort int - useHTTPS bool - expectedErr string - }{ - { - name: "incorrect source url", - mappings: map[string]string{ - "loca^host": "github.com", - }, - httpPort: 3000, - httpsPort: 3001, - useHTTPS: true, - expectedErr: "failed to parse source url: parse \"//loca^host\": invalid character \"^\" in host name", - }, - { - name: "incorrect port in source url", - mappings: map[string]string{ - "localhost:": "github.com", - }, - httpPort: -1, - httpsPort: 3001, - useHTTPS: true, - expectedErr: "failed to parse source url: port \"//localhost:\": empty port", - }, - } - for _, testCase := range testsCases { - t.Run(testCase.name, func(t *testing.T) { - _, err := urlreplacer.NormaliseMappings( - testCase.mappings, - testCase.httpPort, - testCase.httpsPort, - testCase.useHTTPS, - ) - - assert.EqualError(t, err, testCase.expectedErr) - }) - } - }) -} diff --git a/internal/urlreplacer/replacer.go b/internal/urlreplacer/replacer.go index e460e819..eec5a453 100644 --- a/internal/urlreplacer/replacer.go +++ b/internal/urlreplacer/replacer.go @@ -7,6 +7,7 @@ import ( "regexp" "strings" + "github.com/evg4b/uncors/internal/sfmt" "github.com/evg4b/uncors/pkg/urlx" ) @@ -56,9 +57,7 @@ func NewReplacer(source, target string) (*Replacer, error) { return nil, err } - if replacer.pattern, _, _ = wildCardToReplacePattern(replacer.target); err != nil { - return nil, err - } + replacer.pattern, _ = wildCardToReplacePattern(replacer.target) if len(replacer.target.Scheme) > 0 { replacer.hooks["scheme"] = schemeHookFactory(replacer.target.Scheme) @@ -77,7 +76,7 @@ func (r *Replacer) Replace(source string) (string, error) { for _, subexpName := range r.regexp.SubexpNames() { if len(subexpName) > 0 { - partPattern := fmt.Sprintf("${%s}", subexpName) + partPattern := sfmt.Sprintf("${%s}", subexpName) partIndex := r.regexp.SubexpIndex(subexpName) partValue := matches[partIndex] if hook, ok := r.hooks[subexpName]; ok { diff --git a/internal/urlreplacer/replacer_test.go b/internal/urlreplacer/replacer_test.go index 5bf56e52..bc787bb3 100644 --- a/internal/urlreplacer/replacer_test.go +++ b/internal/urlreplacer/replacer_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/evg4b/uncors/internal/urlreplacer" + "github.com/evg4b/uncors/testing/testconstants" "github.com/evg4b/uncors/testing/testutils" "github.com/stretchr/testify/assert" ) @@ -17,7 +18,7 @@ type replacerTestCase struct { func TestReplacerV2Replace(t *testing.T) { t.Run("url is not empty", func(t *testing.T) { t.Run("source", func(t *testing.T) { - _, err := urlreplacer.NewReplacer("", "http://github.com") + _, err := urlreplacer.NewReplacer("", testconstants.HTTPGithub) assert.ErrorIs(t, err, urlreplacer.ErrEmptySourceURL) }) @@ -216,7 +217,7 @@ var isSecureTestCases = []struct { }{ { name: "url with http scheme", - url: "http://localhost", + url: testconstants.HTTPLocalhost, expected: false, }, { @@ -226,12 +227,12 @@ var isSecureTestCases = []struct { }, { name: "url without scheme", - url: "localhost", + url: testconstants.Localhost, expected: false, }, { name: "url with https scheme", - url: "https://localhost", + url: testconstants.HTTPSLocalhost, expected: true, }, } @@ -239,7 +240,7 @@ var isSecureTestCases = []struct { func TestReplacerIsSourceSecure(t *testing.T) { makeReplacer := func(source string) *urlreplacer.Replacer { t.Helper() - replacer, err := urlreplacer.NewReplacer(source, "https://github.com") + replacer, err := urlreplacer.NewReplacer(source, testconstants.HTTPSGithub) if err != nil { t.Error(err) } @@ -259,7 +260,7 @@ func TestReplacerIsSourceSecure(t *testing.T) { func TestReplacerIsTargetSecure(t *testing.T) { makeReplacer := func(target string) *urlreplacer.Replacer { t.Helper() - replacer, err := urlreplacer.NewReplacer("https://github.com", target) + replacer, err := urlreplacer.NewReplacer(testconstants.HTTPSGithub, target) if err != nil { t.Error(err) } diff --git a/internal/version/new_version_check.go b/internal/version/new_version_check.go index 6c758d12..b84608c8 100644 --- a/internal/version/new_version_check.go +++ b/internal/version/new_version_check.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/evg4b/uncors/internal/contracts" + "github.com/evg4b/uncors/internal/helpers" "github.com/evg4b/uncors/internal/log" "github.com/evg4b/uncors/internal/ui" "github.com/hashicorp/go-version" @@ -43,7 +44,7 @@ func CheckNewVersion(ctx context.Context, client contracts.HTTPClient, rawCurren return } - defer response.Body.Close() + defer helpers.CloseSafe(response.Body) decoder := json.NewDecoder(response.Body) lastVersionInfo := versionInfo{} diff --git a/internal/version/new_version_check_test.go b/internal/version/new_version_check_test.go index 2cbe632a..adacf74c 100644 --- a/internal/version/new_version_check_test.go +++ b/internal/version/new_version_check_test.go @@ -6,15 +6,14 @@ import ( "bytes" "context" "errors" - "github.com/evg4b/uncors/internal/version" "io" - "io/ioutil" "net/http" "strings" "testing" "github.com/evg4b/uncors/internal/contracts" "github.com/evg4b/uncors/internal/log" + "github.com/evg4b/uncors/internal/version" "github.com/evg4b/uncors/testing/mocks" "github.com/evg4b/uncors/testing/testutils" "github.com/stretchr/testify/assert" @@ -33,20 +32,20 @@ func TestCheckNewVersion(t *testing.T) { }{ { name: "current version is not correct", - client: mocks.NewHttpClientMock(t), + client: mocks.NewHTTPClientMock(t), version: "#", expected: " DEBUG Checking new version\n DEBUG failed to parse current version: Malformed version: #\n", }, { name: "http error is occupied", - client: mocks.NewHttpClientMock(t). + client: mocks.NewHTTPClientMock(t). DoMock.Return(nil, errors.New("some http error")), version: "0.0.3", expected: " DEBUG Checking new version\n DEBUG http error occupied: some http error\n", }, { name: "invalid json received", - client: mocks.NewHttpClientMock(t). + client: mocks.NewHTTPClientMock(t). DoMock.Return(&http.Response{ Body: io.NopCloser(strings.NewReader(`{ "version"`)), }, nil), @@ -55,7 +54,7 @@ func TestCheckNewVersion(t *testing.T) { }, { name: "incorrect json from api received", - client: mocks.NewHttpClientMock(t). + client: mocks.NewHTTPClientMock(t). DoMock.Return(&http.Response{ Body: io.NopCloser(strings.NewReader(`{ "tag_name": "#" }`)), }, nil), @@ -68,7 +67,7 @@ func TestCheckNewVersion(t *testing.T) { assert.NotPanics(t, func() { version.CheckNewVersion(context.Background(), testCase.client, testCase.version) - outputData, err := ioutil.ReadAll(output) + outputData, err := io.ReadAll(output) testutils.CheckNoError(t, err) assert.Equal(t, testCase.expected, string(outputData)) @@ -79,12 +78,12 @@ func TestCheckNewVersion(t *testing.T) { t.Run("should print ", func(t *testing.T) { t.Run("prop1", testutils.LogTest(func(t *testing.T, output *bytes.Buffer) { - httpClient := mocks.NewHttpClientMock(t). + httpClient := mocks.NewHTTPClientMock(t). DoMock.Return(&http.Response{Body: io.NopCloser(strings.NewReader(`{ "tag_name": "0.0.7" }`))}, nil) version.CheckNewVersion(context.Background(), httpClient, "0.0.4") - outputData, err := ioutil.ReadAll(output) + outputData, err := io.ReadAll(output) testutils.CheckNoError(t, err) expected := ` DEBUG Checking new version INFO NEW VERSION IS Available! @@ -96,12 +95,12 @@ func TestCheckNewVersion(t *testing.T) { })) t.Run("prop2", testutils.LogTest(func(t *testing.T, output *bytes.Buffer) { - httpClient := mocks.NewHttpClientMock(t). + httpClient := mocks.NewHTTPClientMock(t). DoMock.Return(&http.Response{Body: io.NopCloser(strings.NewReader(`{ "tag_name": "0.0.7" }`))}, nil) version.CheckNewVersion(context.Background(), httpClient, "0.0.7") - outputData, err := ioutil.ReadAll(output) + outputData, err := io.ReadAll(output) testutils.CheckNoError(t, err) expected := " DEBUG Checking new version\n DEBUG Version is up to date\n" assert.Equal(t, expected, string(outputData)) diff --git a/main.go b/main.go index b2f0d568..4d3c4413 100644 --- a/main.go +++ b/main.go @@ -1,31 +1,26 @@ -// nolint: cyclop package main import ( "errors" - "fmt" "net" "net/http" "os" "strconv" - "github.com/evg4b/uncors/internal/version" - - "github.com/evg4b/uncors/internal/server" - "golang.org/x/net/context" - - "github.com/evg4b/uncors/internal/configuration" - "github.com/evg4b/uncors/internal/infrastructure" + "github.com/evg4b/uncors/internal/config" + "github.com/evg4b/uncors/internal/handler" + "github.com/evg4b/uncors/internal/infra" "github.com/evg4b/uncors/internal/log" - "github.com/evg4b/uncors/internal/middlewares/mock" - "github.com/evg4b/uncors/internal/middlewares/proxy" + "github.com/evg4b/uncors/internal/server" + "github.com/evg4b/uncors/internal/sfmt" "github.com/evg4b/uncors/internal/ui" "github.com/evg4b/uncors/internal/urlreplacer" + "github.com/evg4b/uncors/internal/version" "github.com/pseidemann/finish" - "github.com/pterm/pterm" "github.com/spf13/afero" "github.com/spf13/pflag" "github.com/spf13/viper" + "golang.org/x/net/context" ) var Version = "X.X.X" @@ -33,36 +28,36 @@ var Version = "X.X.X" const baseAddress = "127.0.0.1" func main() { - defer infrastructure.PanicInterceptor(func(value any) { - pterm.Error.Println(value) + defer infra.PanicInterceptor(func(value any) { + log.Error(value) os.Exit(1) }) pflag.Usage = func() { ui.Logo(Version) - fmt.Fprintf(os.Stdout, "Usage of %s:\n", os.Args[0]) + sfmt.Fprintf(os.Stdout, "Usage of %s:\n", os.Args[0]) pflag.PrintDefaults() } - config, err := configuration.LoadConfiguration(viper.GetViper(), os.Args) + uncorsConfig, err := config.LoadConfiguration(viper.GetViper(), os.Args) if err != nil { panic(err) } - if err = configuration.Validate(config); err != nil { + if err = config.Validate(uncorsConfig); err != nil { panic(err) } - if config.Debug { + if uncorsConfig.Debug { log.EnableDebugMessages() log.Debug("Enabled debug messages") } - mappings, err := urlreplacer.NormaliseMappings( - config.Mappings, - config.HTTPPort, - config.HTTPSPort, - config.IsHTTPSEnabled(), + mappings, err := config.NormaliseMappings( + uncorsConfig.Mappings, + uncorsConfig.HTTPPort, + uncorsConfig.HTTPSPort, + uncorsConfig.IsHTTPSEnabled(), ) if err != nil { panic(err) @@ -73,58 +68,52 @@ func main() { panic(err) } - httpClient, err := infrastructure.MakeHTTPClient(viper.GetString("proxy")) + httpClient, err := infra.MakeHTTPClient(viper.GetString("proxy")) if err != nil { panic(err) } - proxyMiddleware := proxy.NewProxyMiddleware( - proxy.WithURLReplacerFactory(factory), - proxy.WithHTTPClient(httpClient), - proxy.WithLogger(ui.ProxyLogger), - ) + finisher := finish.Finisher{Log: infra.NoopLogger{}} - fileSystem := afero.NewOsFs() + ctx := context.Background() - mockMiddleware := mock.NewMockMiddleware( - mock.WithLogger(ui.MockLogger), - mock.WithNextMiddleware(proxyMiddleware), - mock.WithMocks(config.Mocks), - mock.WithFileSystem(fileSystem), + globalHandler := handler.NewUncorsRequestHandler( + handler.WithMappings(mappings), + handler.WithLogger(ui.MockLogger), + handler.WithFileSystem(afero.NewOsFs()), + handler.WithURLReplacerFactory(factory), + handler.WithHTTPClient(httpClient), ) - finisher := finish.Finisher{Log: infrastructure.NoopLogger{}} + uncorsServer := server.NewUncorsServer(ctx, globalHandler) - ctx := context.Background() + log.Print(ui.Logo(Version)) + log.Print("\n") + log.Warning(ui.DisclaimerMessage) + log.Print("\n") + log.Info(mappings.String()) + log.Print("\n") - uncorsServer := server.NewUncorsServer(ctx, mockMiddleware) finisher.Add(uncorsServer) go func() { - log.Debugf("Starting http server on port %d", config.HTTPPort) - addr := net.JoinHostPort(baseAddress, strconv.Itoa(config.HTTPPort)) + defer finisher.Trigger() + log.Debugf("Starting http server on port %d", uncorsConfig.HTTPPort) + addr := net.JoinHostPort(baseAddress, strconv.Itoa(uncorsConfig.HTTPPort)) err := uncorsServer.ListenAndServe(addr) handleHTTPServerError("HTTP", err) - finisher.Trigger() }() - if config.IsHTTPSEnabled() { + if uncorsConfig.IsHTTPSEnabled() { log.Debug("Found cert file and key file. Https server will be started") - addr := net.JoinHostPort(baseAddress, strconv.Itoa(config.HTTPSPort)) + addr := net.JoinHostPort(baseAddress, strconv.Itoa(uncorsConfig.HTTPSPort)) go func() { - log.Debugf("Starting https server on port %d", config.HTTPSPort) - err := uncorsServer.ListenAndServeTLS(addr, config.CertFile, config.KeyFile) + defer finisher.Trigger() + log.Debugf("Starting https server on port %d", uncorsConfig.HTTPSPort) + err := uncorsServer.ListenAndServeTLS(addr, uncorsConfig.CertFile, uncorsConfig.KeyFile) handleHTTPServerError("HTTPS", err) - finisher.Trigger() }() } - log.Print(ui.Logo(Version)) - log.Print("\n") - log.Warning(ui.DisclaimerMessage) - log.Print("\n") - log.Info(ui.Mappings(mappings, config.Mocks)) - log.Print("\n") - go version.CheckNewVersion(ctx, httpClient, Version) finisher.Wait() diff --git a/pkg/urlx/README.md b/pkg/urlx/README.md index f944e5af..bf165d1f 100644 --- a/pkg/urlx/README.md +++ b/pkg/urlx/README.md @@ -11,4 +11,4 @@ for [uncors](https://github.com/evg4b/uncors) project. ## License -`github.com/evg4b/uncors/pkg/urlx` is licensed under the [MIT License](./LICENSE). +`github.com/evg4b/uncors/pkg/urlx` is licensed under the [MIT License](LICENSE). diff --git a/pkg/urlx/urlx.go b/pkg/urlx/urlx.go index a56a7ca7..5d722c37 100644 --- a/pkg/urlx/urlx.go +++ b/pkg/urlx/urlx.go @@ -81,7 +81,7 @@ func defaultScheme(rawURL, scheme string) string { var ( domainRegexp = regexp.MustCompile(`^([a-zA-Z0-9-_*]{1,63}\.)*([a-zA-Z0-9-*]{1,63})$`) ipv4Regexp = regexp.MustCompile(`^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$`) - ipv6Regexp = regexp.MustCompile(`^\[[a-fA-F0-9:]+\]$`) + ipv6Regexp = regexp.MustCompile(`^\[[a-fA-F0-9:]+]$`) ) func checkHost(host string) error { diff --git a/testing/mocks/closer_mock.go b/testing/mocks/closer_mock.go new file mode 100644 index 00000000..281ecdce --- /dev/null +++ b/testing/mocks/closer_mock.go @@ -0,0 +1,208 @@ +package mocks + +// Code generated by http://github.com/gojuno/minimock (dev). DO NOT EDIT. + +//go:generate minimock -i io.Closer -o ./closer_mock.go -n CloserMock + +import ( + mm_atomic "sync/atomic" + mm_time "time" + + "github.com/gojuno/minimock/v3" +) + +// CloserMock implements io.Closer +type CloserMock struct { + t minimock.Tester + + funcClose func() (err error) + inspectFuncClose func() + afterCloseCounter uint64 + beforeCloseCounter uint64 + CloseMock mCloserMockClose +} + +// NewCloserMock returns a mock for io.Closer +func NewCloserMock(t minimock.Tester) *CloserMock { + m := &CloserMock{t: t} + if controller, ok := t.(minimock.MockController); ok { + controller.RegisterMocker(m) + } + + m.CloseMock = mCloserMockClose{mock: m} + + return m +} + +type mCloserMockClose struct { + mock *CloserMock + defaultExpectation *CloserMockCloseExpectation + expectations []*CloserMockCloseExpectation +} + +// CloserMockCloseExpectation specifies expectation struct of the Closer.Close +type CloserMockCloseExpectation struct { + mock *CloserMock + + results *CloserMockCloseResults + Counter uint64 +} + +// CloserMockCloseResults contains results of the Closer.Close +type CloserMockCloseResults struct { + err error +} + +// Expect sets up expected params for Closer.Close +func (mmClose *mCloserMockClose) Expect() *mCloserMockClose { + if mmClose.mock.funcClose != nil { + mmClose.mock.t.Fatalf("CloserMock.Close mock is already set by Set") + } + + if mmClose.defaultExpectation == nil { + mmClose.defaultExpectation = &CloserMockCloseExpectation{} + } + + return mmClose +} + +// Inspect accepts an inspector function that has same arguments as the Closer.Close +func (mmClose *mCloserMockClose) Inspect(f func()) *mCloserMockClose { + if mmClose.mock.inspectFuncClose != nil { + mmClose.mock.t.Fatalf("Inspect function is already set for CloserMock.Close") + } + + mmClose.mock.inspectFuncClose = f + + return mmClose +} + +// Return sets up results that will be returned by Closer.Close +func (mmClose *mCloserMockClose) Return(err error) *CloserMock { + if mmClose.mock.funcClose != nil { + mmClose.mock.t.Fatalf("CloserMock.Close mock is already set by Set") + } + + if mmClose.defaultExpectation == nil { + mmClose.defaultExpectation = &CloserMockCloseExpectation{mock: mmClose.mock} + } + mmClose.defaultExpectation.results = &CloserMockCloseResults{err} + return mmClose.mock +} + +//Set uses given function f to mock the Closer.Close method +func (mmClose *mCloserMockClose) Set(f func() (err error)) *CloserMock { + if mmClose.defaultExpectation != nil { + mmClose.mock.t.Fatalf("Default expectation is already set for the Closer.Close method") + } + + if len(mmClose.expectations) > 0 { + mmClose.mock.t.Fatalf("Some expectations are already set for the Closer.Close method") + } + + mmClose.mock.funcClose = f + return mmClose.mock +} + +// Close implements io.Closer +func (mmClose *CloserMock) Close() (err error) { + mm_atomic.AddUint64(&mmClose.beforeCloseCounter, 1) + defer mm_atomic.AddUint64(&mmClose.afterCloseCounter, 1) + + if mmClose.inspectFuncClose != nil { + mmClose.inspectFuncClose() + } + + if mmClose.CloseMock.defaultExpectation != nil { + mm_atomic.AddUint64(&mmClose.CloseMock.defaultExpectation.Counter, 1) + + mm_results := mmClose.CloseMock.defaultExpectation.results + if mm_results == nil { + mmClose.t.Fatal("No results are set for the CloserMock.Close") + } + return (*mm_results).err + } + if mmClose.funcClose != nil { + return mmClose.funcClose() + } + mmClose.t.Fatalf("Unexpected call to CloserMock.Close.") + return +} + +// CloseAfterCounter returns a count of finished CloserMock.Close invocations +func (mmClose *CloserMock) CloseAfterCounter() uint64 { + return mm_atomic.LoadUint64(&mmClose.afterCloseCounter) +} + +// CloseBeforeCounter returns a count of CloserMock.Close invocations +func (mmClose *CloserMock) CloseBeforeCounter() uint64 { + return mm_atomic.LoadUint64(&mmClose.beforeCloseCounter) +} + +// MinimockCloseDone returns true if the count of the Close invocations corresponds +// the number of defined expectations +func (m *CloserMock) MinimockCloseDone() bool { + for _, e := range m.CloseMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + return false + } + } + + // if default expectation was set then invocations count should be greater than zero + if m.CloseMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterCloseCounter) < 1 { + return false + } + // if func was set then invocations count should be greater than zero + if m.funcClose != nil && mm_atomic.LoadUint64(&m.afterCloseCounter) < 1 { + return false + } + return true +} + +// MinimockCloseInspect logs each unmet expectation +func (m *CloserMock) MinimockCloseInspect() { + for _, e := range m.CloseMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + m.t.Error("Expected call to CloserMock.Close") + } + } + + // if default expectation was set then invocations count should be greater than zero + if m.CloseMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterCloseCounter) < 1 { + m.t.Error("Expected call to CloserMock.Close") + } + // if func was set then invocations count should be greater than zero + if m.funcClose != nil && mm_atomic.LoadUint64(&m.afterCloseCounter) < 1 { + m.t.Error("Expected call to CloserMock.Close") + } +} + +// MinimockFinish checks that all mocked methods have been called the expected number of times +func (m *CloserMock) MinimockFinish() { + if !m.minimockDone() { + m.MinimockCloseInspect() + m.t.FailNow() + } +} + +// MinimockWait waits for all mocked methods to be called the expected number of times +func (m *CloserMock) MinimockWait(timeout mm_time.Duration) { + timeoutCh := mm_time.After(timeout) + for { + if m.minimockDone() { + return + } + select { + case <-timeoutCh: + m.MinimockFinish() + return + case <-mm_time.After(10 * mm_time.Millisecond): + } + } +} + +func (m *CloserMock) minimockDone() bool { + done := true + return done && + m.MinimockCloseDone() +} diff --git a/testing/mocks/constants.go b/testing/mocks/constants.go deleted file mode 100644 index 548b1bcc..00000000 --- a/testing/mocks/constants.go +++ /dev/null @@ -1,11 +0,0 @@ -package mocks - -var SourceHost1 = "host1" -var SourceHost2 = "host2" -var SourceHost3 = "host3" - -var TargetHost1 = "target-host1" -var TargetHost2 = "target-host2" -var TargetHost3 = "target-host3" - -const AllMethods = "GET, PUT, POST, HEAD, TRACE, DELETE, PATCH, COPY, HEAD, LINK, OPTIONS" diff --git a/testing/mocks/http_client_mock.go b/testing/mocks/http_client_mock.go index 5c61af86..d079caba 100644 --- a/testing/mocks/http_client_mock.go +++ b/testing/mocks/http_client_mock.go @@ -2,80 +2,79 @@ package mocks // Code generated by http://github.com/gojuno/minimock (dev). DO NOT EDIT. -//go:generate minimock -i github.com/evg4b/uncors/internal/contracts.HttpClient -o ./http_client_mock.go -n HttpClientMock +//go:generate minimock -i github.com/evg4b/uncors/internal/contracts.HTTPClient -o ./http_client_mock.go -n HTTPClientMock import ( "net/http" "sync" mm_atomic "sync/atomic" mm_time "time" - "github.com/gojuno/minimock/v3" ) -// HttpClientMock implements contracts.HttpClient -type HttpClientMock struct { +// HTTPClientMock implements contracts.HTTPClient +type HTTPClientMock struct { t minimock.Tester funcDo func(req *http.Request) (rp1 *http.Response, err error) inspectFuncDo func(req *http.Request) afterDoCounter uint64 beforeDoCounter uint64 - DoMock mHttpClientMockDo + DoMock mHTTPClientMockDo } -// NewHttpClientMock returns a mock for contracts.HttpClient -func NewHttpClientMock(t minimock.Tester) *HttpClientMock { - m := &HttpClientMock{t: t} +// NewHTTPClientMock returns a mock for contracts.HTTPClient +func NewHTTPClientMock(t minimock.Tester) *HTTPClientMock { + m := &HTTPClientMock{t: t} if controller, ok := t.(minimock.MockController); ok { controller.RegisterMocker(m) } - m.DoMock = mHttpClientMockDo{mock: m} - m.DoMock.callArgs = []*HttpClientMockDoParams{} + m.DoMock = mHTTPClientMockDo{mock: m} + m.DoMock.callArgs = []*HTTPClientMockDoParams{} return m } -type mHttpClientMockDo struct { - mock *HttpClientMock - defaultExpectation *HttpClientMockDoExpectation - expectations []*HttpClientMockDoExpectation +type mHTTPClientMockDo struct { + mock *HTTPClientMock + defaultExpectation *HTTPClientMockDoExpectation + expectations []*HTTPClientMockDoExpectation - callArgs []*HttpClientMockDoParams + callArgs []*HTTPClientMockDoParams mutex sync.RWMutex } -// HttpClientMockDoExpectation specifies expectation struct of the HttpClient.Do -type HttpClientMockDoExpectation struct { - mock *HttpClientMock - params *HttpClientMockDoParams - results *HttpClientMockDoResults +// HTTPClientMockDoExpectation specifies expectation struct of the HTTPClient.Do +type HTTPClientMockDoExpectation struct { + mock *HTTPClientMock + params *HTTPClientMockDoParams + results *HTTPClientMockDoResults Counter uint64 } -// HttpClientMockDoParams contains parameters of the HttpClient.Do -type HttpClientMockDoParams struct { +// HTTPClientMockDoParams contains parameters of the HTTPClient.Do +type HTTPClientMockDoParams struct { req *http.Request } -// HttpClientMockDoResults contains results of the HttpClient.Do -type HttpClientMockDoResults struct { +// HTTPClientMockDoResults contains results of the HTTPClient.Do +type HTTPClientMockDoResults struct { rp1 *http.Response err error } -// Expect sets up expected params for HttpClient.Do -func (mmDo *mHttpClientMockDo) Expect(req *http.Request) *mHttpClientMockDo { +// Expect sets up expected params for HTTPClient.Do +func (mmDo *mHTTPClientMockDo) Expect(req *http.Request) *mHTTPClientMockDo { if mmDo.mock.funcDo != nil { - mmDo.mock.t.Fatalf("HttpClientMock.Do mock is already set by Set") + mmDo.mock.t.Fatalf("HTTPClientMock.Do mock is already set by Set") } if mmDo.defaultExpectation == nil { - mmDo.defaultExpectation = &HttpClientMockDoExpectation{} + mmDo.defaultExpectation = &HTTPClientMockDoExpectation{} } - mmDo.defaultExpectation.params = &HttpClientMockDoParams{req} + mmDo.defaultExpectation.params = &HTTPClientMockDoParams{req} for _, e := range mmDo.expectations { if minimock.Equal(e.params, mmDo.defaultExpectation.params) { mmDo.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmDo.defaultExpectation.params) @@ -85,10 +84,10 @@ func (mmDo *mHttpClientMockDo) Expect(req *http.Request) *mHttpClientMockDo { return mmDo } -// Inspect accepts an inspector function that has same arguments as the HttpClient.Do -func (mmDo *mHttpClientMockDo) Inspect(f func(req *http.Request)) *mHttpClientMockDo { +// Inspect accepts an inspector function that has same arguments as the HTTPClient.Do +func (mmDo *mHTTPClientMockDo) Inspect(f func(req *http.Request)) *mHTTPClientMockDo { if mmDo.mock.inspectFuncDo != nil { - mmDo.mock.t.Fatalf("Inspect function is already set for HttpClientMock.Do") + mmDo.mock.t.Fatalf("Inspect function is already set for HTTPClientMock.Do") } mmDo.mock.inspectFuncDo = f @@ -96,56 +95,56 @@ func (mmDo *mHttpClientMockDo) Inspect(f func(req *http.Request)) *mHttpClientMo return mmDo } -// Return sets up results that will be returned by HttpClient.Do -func (mmDo *mHttpClientMockDo) Return(rp1 *http.Response, err error) *HttpClientMock { +// Return sets up results that will be returned by HTTPClient.Do +func (mmDo *mHTTPClientMockDo) Return(rp1 *http.Response, err error) *HTTPClientMock { if mmDo.mock.funcDo != nil { - mmDo.mock.t.Fatalf("HttpClientMock.Do mock is already set by Set") + mmDo.mock.t.Fatalf("HTTPClientMock.Do mock is already set by Set") } if mmDo.defaultExpectation == nil { - mmDo.defaultExpectation = &HttpClientMockDoExpectation{mock: mmDo.mock} + mmDo.defaultExpectation = &HTTPClientMockDoExpectation{mock: mmDo.mock} } - mmDo.defaultExpectation.results = &HttpClientMockDoResults{rp1, err} + mmDo.defaultExpectation.results = &HTTPClientMockDoResults{rp1, err} return mmDo.mock } -//Set uses given function f to mock the HttpClient.Do method -func (mmDo *mHttpClientMockDo) Set(f func(req *http.Request) (rp1 *http.Response, err error)) *HttpClientMock { +//Set uses given function f to mock the HTTPClient.Do method +func (mmDo *mHTTPClientMockDo) Set(f func(req *http.Request) (rp1 *http.Response, err error)) *HTTPClientMock { if mmDo.defaultExpectation != nil { - mmDo.mock.t.Fatalf("Default expectation is already set for the HttpClient.Do method") + mmDo.mock.t.Fatalf("Default expectation is already set for the HTTPClient.Do method") } if len(mmDo.expectations) > 0 { - mmDo.mock.t.Fatalf("Some expectations are already set for the HttpClient.Do method") + mmDo.mock.t.Fatalf("Some expectations are already set for the HTTPClient.Do method") } mmDo.mock.funcDo = f return mmDo.mock } -// When sets expectation for the HttpClient.Do which will trigger the result defined by the following +// When sets expectation for the HTTPClient.Do which will trigger the result defined by the following // Then helper -func (mmDo *mHttpClientMockDo) When(req *http.Request) *HttpClientMockDoExpectation { +func (mmDo *mHTTPClientMockDo) When(req *http.Request) *HTTPClientMockDoExpectation { if mmDo.mock.funcDo != nil { - mmDo.mock.t.Fatalf("HttpClientMock.Do mock is already set by Set") + mmDo.mock.t.Fatalf("HTTPClientMock.Do mock is already set by Set") } - expectation := &HttpClientMockDoExpectation{ + expectation := &HTTPClientMockDoExpectation{ mock: mmDo.mock, - params: &HttpClientMockDoParams{req}, + params: &HTTPClientMockDoParams{req}, } mmDo.expectations = append(mmDo.expectations, expectation) return expectation } -// Then sets up HttpClient.Do return parameters for the expectation previously defined by the When method -func (e *HttpClientMockDoExpectation) Then(rp1 *http.Response, err error) *HttpClientMock { - e.results = &HttpClientMockDoResults{rp1, err} +// Then sets up HTTPClient.Do return parameters for the expectation previously defined by the When method +func (e *HTTPClientMockDoExpectation) Then(rp1 *http.Response, err error) *HTTPClientMock { + e.results = &HTTPClientMockDoResults{rp1, err} return e.mock } -// Do implements contracts.HttpClient -func (mmDo *HttpClientMock) Do(req *http.Request) (rp1 *http.Response, err error) { +// Do implements contracts.HTTPClient +func (mmDo *HTTPClientMock) Do(req *http.Request) (rp1 *http.Response, err error) { mm_atomic.AddUint64(&mmDo.beforeDoCounter, 1) defer mm_atomic.AddUint64(&mmDo.afterDoCounter, 1) @@ -153,7 +152,7 @@ func (mmDo *HttpClientMock) Do(req *http.Request) (rp1 *http.Response, err error mmDo.inspectFuncDo(req) } - mm_params := &HttpClientMockDoParams{req} + mm_params := &HTTPClientMockDoParams{req} // Record call args mmDo.DoMock.mutex.Lock() @@ -170,40 +169,40 @@ func (mmDo *HttpClientMock) Do(req *http.Request) (rp1 *http.Response, err error if mmDo.DoMock.defaultExpectation != nil { mm_atomic.AddUint64(&mmDo.DoMock.defaultExpectation.Counter, 1) mm_want := mmDo.DoMock.defaultExpectation.params - mm_got := HttpClientMockDoParams{req} + mm_got := HTTPClientMockDoParams{req} if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { - mmDo.t.Errorf("HttpClientMock.Do got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) + mmDo.t.Errorf("HTTPClientMock.Do got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) } mm_results := mmDo.DoMock.defaultExpectation.results if mm_results == nil { - mmDo.t.Fatal("No results are set for the HttpClientMock.Do") + mmDo.t.Fatal("No results are set for the HTTPClientMock.Do") } return (*mm_results).rp1, (*mm_results).err } if mmDo.funcDo != nil { return mmDo.funcDo(req) } - mmDo.t.Fatalf("Unexpected call to HttpClientMock.Do. %v", req) + mmDo.t.Fatalf("Unexpected call to HTTPClientMock.Do. %v", req) return } -// DoAfterCounter returns a count of finished HttpClientMock.Do invocations -func (mmDo *HttpClientMock) DoAfterCounter() uint64 { +// DoAfterCounter returns a count of finished HTTPClientMock.Do invocations +func (mmDo *HTTPClientMock) DoAfterCounter() uint64 { return mm_atomic.LoadUint64(&mmDo.afterDoCounter) } -// DoBeforeCounter returns a count of HttpClientMock.Do invocations -func (mmDo *HttpClientMock) DoBeforeCounter() uint64 { +// DoBeforeCounter returns a count of HTTPClientMock.Do invocations +func (mmDo *HTTPClientMock) DoBeforeCounter() uint64 { return mm_atomic.LoadUint64(&mmDo.beforeDoCounter) } -// Calls returns a list of arguments used in each call to HttpClientMock.Do. +// Calls returns a list of arguments used in each call to HTTPClientMock.Do. // The list is in the same order as the calls were made (i.e. recent calls have a higher index) -func (mmDo *mHttpClientMockDo) Calls() []*HttpClientMockDoParams { +func (mmDo *mHTTPClientMockDo) Calls() []*HTTPClientMockDoParams { mmDo.mutex.RLock() - argCopy := make([]*HttpClientMockDoParams, len(mmDo.callArgs)) + argCopy := make([]*HTTPClientMockDoParams, len(mmDo.callArgs)) copy(argCopy, mmDo.callArgs) mmDo.mutex.RUnlock() @@ -213,7 +212,7 @@ func (mmDo *mHttpClientMockDo) Calls() []*HttpClientMockDoParams { // MinimockDoDone returns true if the count of the Do invocations corresponds // the number of defined expectations -func (m *HttpClientMock) MinimockDoDone() bool { +func (m *HTTPClientMock) MinimockDoDone() bool { for _, e := range m.DoMock.expectations { if mm_atomic.LoadUint64(&e.Counter) < 1 { return false @@ -232,29 +231,29 @@ func (m *HttpClientMock) MinimockDoDone() bool { } // MinimockDoInspect logs each unmet expectation -func (m *HttpClientMock) MinimockDoInspect() { +func (m *HTTPClientMock) MinimockDoInspect() { for _, e := range m.DoMock.expectations { if mm_atomic.LoadUint64(&e.Counter) < 1 { - m.t.Errorf("Expected call to HttpClientMock.Do with params: %#v", *e.params) + m.t.Errorf("Expected call to HTTPClientMock.Do with params: %#v", *e.params) } } // if default expectation was set then invocations count should be greater than zero if m.DoMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterDoCounter) < 1 { if m.DoMock.defaultExpectation.params == nil { - m.t.Error("Expected call to HttpClientMock.Do") + m.t.Error("Expected call to HTTPClientMock.Do") } else { - m.t.Errorf("Expected call to HttpClientMock.Do with params: %#v", *m.DoMock.defaultExpectation.params) + m.t.Errorf("Expected call to HTTPClientMock.Do with params: %#v", *m.DoMock.defaultExpectation.params) } } // if func was set then invocations count should be greater than zero if m.funcDo != nil && mm_atomic.LoadUint64(&m.afterDoCounter) < 1 { - m.t.Error("Expected call to HttpClientMock.Do") + m.t.Error("Expected call to HTTPClientMock.Do") } } // MinimockFinish checks that all mocked methods have been called the expected number of times -func (m *HttpClientMock) MinimockFinish() { +func (m *HTTPClientMock) MinimockFinish() { if !m.minimockDone() { m.MinimockDoInspect() m.t.FailNow() @@ -262,7 +261,7 @@ func (m *HttpClientMock) MinimockFinish() { } // MinimockWait waits for all mocked methods to be called the expected number of times -func (m *HttpClientMock) MinimockWait(timeout mm_time.Duration) { +func (m *HTTPClientMock) MinimockWait(timeout mm_time.Duration) { timeoutCh := mm_time.After(timeout) for { if m.minimockDone() { @@ -277,7 +276,7 @@ func (m *HttpClientMock) MinimockWait(timeout mm_time.Duration) { } } -func (m *HttpClientMock) minimockDone() bool { +func (m *HTTPClientMock) minimockDone() bool { done := true return done && m.MinimockDoDone() diff --git a/testing/mocks/logger_mock.go b/testing/mocks/logger_mock.go index cbaff793..bd6554dc 100644 --- a/testing/mocks/logger_mock.go +++ b/testing/mocks/logger_mock.go @@ -9,7 +9,6 @@ import ( "sync" mm_atomic "sync/atomic" mm_time "time" - "github.com/gojuno/minimock/v3" ) @@ -175,7 +174,7 @@ func (mmDebug *mLoggerMockDebug) Return() *LoggerMock { return mmDebug.mock } -// Set uses given function f to mock the Logger.Debug method +//Set uses given function f to mock the Logger.Debug method func (mmDebug *mLoggerMockDebug) Set(f func(a ...any)) *LoggerMock { if mmDebug.defaultExpectation != nil { mmDebug.mock.t.Fatalf("Default expectation is already set for the Logger.Debug method") @@ -363,7 +362,7 @@ func (mmDebugf *mLoggerMockDebugf) Return() *LoggerMock { return mmDebugf.mock } -// Set uses given function f to mock the Logger.Debugf method +//Set uses given function f to mock the Logger.Debugf method func (mmDebugf *mLoggerMockDebugf) Set(f func(template string, a ...any)) *LoggerMock { if mmDebugf.defaultExpectation != nil { mmDebugf.mock.t.Fatalf("Default expectation is already set for the Logger.Debugf method") @@ -550,7 +549,7 @@ func (mmError *mLoggerMockError) Return() *LoggerMock { return mmError.mock } -// Set uses given function f to mock the Logger.Error method +//Set uses given function f to mock the Logger.Error method func (mmError *mLoggerMockError) Set(f func(a ...any)) *LoggerMock { if mmError.defaultExpectation != nil { mmError.mock.t.Fatalf("Default expectation is already set for the Logger.Error method") @@ -738,7 +737,7 @@ func (mmErrorf *mLoggerMockErrorf) Return() *LoggerMock { return mmErrorf.mock } -// Set uses given function f to mock the Logger.Errorf method +//Set uses given function f to mock the Logger.Errorf method func (mmErrorf *mLoggerMockErrorf) Set(f func(template string, a ...any)) *LoggerMock { if mmErrorf.defaultExpectation != nil { mmErrorf.mock.t.Fatalf("Default expectation is already set for the Logger.Errorf method") @@ -925,7 +924,7 @@ func (mmInfo *mLoggerMockInfo) Return() *LoggerMock { return mmInfo.mock } -// Set uses given function f to mock the Logger.Info method +//Set uses given function f to mock the Logger.Info method func (mmInfo *mLoggerMockInfo) Set(f func(a ...any)) *LoggerMock { if mmInfo.defaultExpectation != nil { mmInfo.mock.t.Fatalf("Default expectation is already set for the Logger.Info method") @@ -1113,7 +1112,7 @@ func (mmInfof *mLoggerMockInfof) Return() *LoggerMock { return mmInfof.mock } -// Set uses given function f to mock the Logger.Infof method +//Set uses given function f to mock the Logger.Infof method func (mmInfof *mLoggerMockInfof) Set(f func(template string, a ...any)) *LoggerMock { if mmInfof.defaultExpectation != nil { mmInfof.mock.t.Fatalf("Default expectation is already set for the Logger.Infof method") @@ -1300,7 +1299,7 @@ func (mmPrintResponse *mLoggerMockPrintResponse) Return() *LoggerMock { return mmPrintResponse.mock } -// Set uses given function f to mock the Logger.PrintResponse method +//Set uses given function f to mock the Logger.PrintResponse method func (mmPrintResponse *mLoggerMockPrintResponse) Set(f func(response *http.Response)) *LoggerMock { if mmPrintResponse.defaultExpectation != nil { mmPrintResponse.mock.t.Fatalf("Default expectation is already set for the Logger.PrintResponse method") @@ -1487,7 +1486,7 @@ func (mmWarning *mLoggerMockWarning) Return() *LoggerMock { return mmWarning.mock } -// Set uses given function f to mock the Logger.Warning method +//Set uses given function f to mock the Logger.Warning method func (mmWarning *mLoggerMockWarning) Set(f func(a ...any)) *LoggerMock { if mmWarning.defaultExpectation != nil { mmWarning.mock.t.Fatalf("Default expectation is already set for the Logger.Warning method") @@ -1675,7 +1674,7 @@ func (mmWarningf *mLoggerMockWarningf) Return() *LoggerMock { return mmWarningf.mock } -// Set uses given function f to mock the Logger.Warningf method +//Set uses given function f to mock the Logger.Warningf method func (mmWarningf *mLoggerMockWarningf) Set(f func(template string, a ...any)) *LoggerMock { if mmWarningf.defaultExpectation != nil { mmWarningf.mock.t.Fatalf("Default expectation is already set for the Logger.Warningf method") diff --git a/testing/mocks/urlreplacer_factory_mock.go b/testing/mocks/urlreplacer_factory_mock.go index d0f672e6..5f8fdaf0 100644 --- a/testing/mocks/urlreplacer_factory_mock.go +++ b/testing/mocks/urlreplacer_factory_mock.go @@ -9,7 +9,6 @@ import ( "sync" mm_atomic "sync/atomic" mm_time "time" - "github.com/evg4b/uncors/internal/urlreplacer" "github.com/gojuno/minimock/v3" ) @@ -111,7 +110,7 @@ func (mmMake *mURLReplacerFactoryMockMake) Return(rp1 *urlreplacer.Replacer, rp2 return mmMake.mock } -// Set uses given function f to mock the URLReplacerFactory.Make method +//Set uses given function f to mock the URLReplacerFactory.Make method func (mmMake *mURLReplacerFactoryMockMake) Set(f func(requestURL *url.URL) (rp1 *urlreplacer.Replacer, rp2 *urlreplacer.Replacer, err error)) *URLReplacerFactoryMock { if mmMake.defaultExpectation != nil { mmMake.mock.t.Fatalf("Default expectation is already set for the URLReplacerFactory.Make method") diff --git a/testing/mocks/writer_mock.go b/testing/mocks/writer_mock.go new file mode 100644 index 00000000..85ad6148 --- /dev/null +++ b/testing/mocks/writer_mock.go @@ -0,0 +1,283 @@ +package mocks + +// Code generated by http://github.com/gojuno/minimock (dev). DO NOT EDIT. + +//go:generate minimock -i io.Writer -o ./writer_mock.go -n WriterMock + +import ( + "sync" + mm_atomic "sync/atomic" + mm_time "time" + + "github.com/gojuno/minimock/v3" +) + +// WriterMock implements io.Writer +type WriterMock struct { + t minimock.Tester + + funcWrite func(p []byte) (n int, err error) + inspectFuncWrite func(p []byte) + afterWriteCounter uint64 + beforeWriteCounter uint64 + WriteMock mWriterMockWrite +} + +// NewWriterMock returns a mock for io.Writer +func NewWriterMock(t minimock.Tester) *WriterMock { + m := &WriterMock{t: t} + if controller, ok := t.(minimock.MockController); ok { + controller.RegisterMocker(m) + } + + m.WriteMock = mWriterMockWrite{mock: m} + m.WriteMock.callArgs = []*WriterMockWriteParams{} + + return m +} + +type mWriterMockWrite struct { + mock *WriterMock + defaultExpectation *WriterMockWriteExpectation + expectations []*WriterMockWriteExpectation + + callArgs []*WriterMockWriteParams + mutex sync.RWMutex +} + +// WriterMockWriteExpectation specifies expectation struct of the Writer.Write +type WriterMockWriteExpectation struct { + mock *WriterMock + params *WriterMockWriteParams + results *WriterMockWriteResults + Counter uint64 +} + +// WriterMockWriteParams contains parameters of the Writer.Write +type WriterMockWriteParams struct { + p []byte +} + +// WriterMockWriteResults contains results of the Writer.Write +type WriterMockWriteResults struct { + n int + err error +} + +// Expect sets up expected params for Writer.Write +func (mmWrite *mWriterMockWrite) Expect(p []byte) *mWriterMockWrite { + if mmWrite.mock.funcWrite != nil { + mmWrite.mock.t.Fatalf("WriterMock.Write mock is already set by Set") + } + + if mmWrite.defaultExpectation == nil { + mmWrite.defaultExpectation = &WriterMockWriteExpectation{} + } + + mmWrite.defaultExpectation.params = &WriterMockWriteParams{p} + for _, e := range mmWrite.expectations { + if minimock.Equal(e.params, mmWrite.defaultExpectation.params) { + mmWrite.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmWrite.defaultExpectation.params) + } + } + + return mmWrite +} + +// Inspect accepts an inspector function that has same arguments as the Writer.Write +func (mmWrite *mWriterMockWrite) Inspect(f func(p []byte)) *mWriterMockWrite { + if mmWrite.mock.inspectFuncWrite != nil { + mmWrite.mock.t.Fatalf("Inspect function is already set for WriterMock.Write") + } + + mmWrite.mock.inspectFuncWrite = f + + return mmWrite +} + +// Return sets up results that will be returned by Writer.Write +func (mmWrite *mWriterMockWrite) Return(n int, err error) *WriterMock { + if mmWrite.mock.funcWrite != nil { + mmWrite.mock.t.Fatalf("WriterMock.Write mock is already set by Set") + } + + if mmWrite.defaultExpectation == nil { + mmWrite.defaultExpectation = &WriterMockWriteExpectation{mock: mmWrite.mock} + } + mmWrite.defaultExpectation.results = &WriterMockWriteResults{n, err} + return mmWrite.mock +} + +//Set uses given function f to mock the Writer.Write method +func (mmWrite *mWriterMockWrite) Set(f func(p []byte) (n int, err error)) *WriterMock { + if mmWrite.defaultExpectation != nil { + mmWrite.mock.t.Fatalf("Default expectation is already set for the Writer.Write method") + } + + if len(mmWrite.expectations) > 0 { + mmWrite.mock.t.Fatalf("Some expectations are already set for the Writer.Write method") + } + + mmWrite.mock.funcWrite = f + return mmWrite.mock +} + +// When sets expectation for the Writer.Write which will trigger the result defined by the following +// Then helper +func (mmWrite *mWriterMockWrite) When(p []byte) *WriterMockWriteExpectation { + if mmWrite.mock.funcWrite != nil { + mmWrite.mock.t.Fatalf("WriterMock.Write mock is already set by Set") + } + + expectation := &WriterMockWriteExpectation{ + mock: mmWrite.mock, + params: &WriterMockWriteParams{p}, + } + mmWrite.expectations = append(mmWrite.expectations, expectation) + return expectation +} + +// Then sets up Writer.Write return parameters for the expectation previously defined by the When method +func (e *WriterMockWriteExpectation) Then(n int, err error) *WriterMock { + e.results = &WriterMockWriteResults{n, err} + return e.mock +} + +// Write implements io.Writer +func (mmWrite *WriterMock) Write(p []byte) (n int, err error) { + mm_atomic.AddUint64(&mmWrite.beforeWriteCounter, 1) + defer mm_atomic.AddUint64(&mmWrite.afterWriteCounter, 1) + + if mmWrite.inspectFuncWrite != nil { + mmWrite.inspectFuncWrite(p) + } + + mm_params := &WriterMockWriteParams{p} + + // Record call args + mmWrite.WriteMock.mutex.Lock() + mmWrite.WriteMock.callArgs = append(mmWrite.WriteMock.callArgs, mm_params) + mmWrite.WriteMock.mutex.Unlock() + + for _, e := range mmWrite.WriteMock.expectations { + if minimock.Equal(e.params, mm_params) { + mm_atomic.AddUint64(&e.Counter, 1) + return e.results.n, e.results.err + } + } + + if mmWrite.WriteMock.defaultExpectation != nil { + mm_atomic.AddUint64(&mmWrite.WriteMock.defaultExpectation.Counter, 1) + mm_want := mmWrite.WriteMock.defaultExpectation.params + mm_got := WriterMockWriteParams{p} + if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { + mmWrite.t.Errorf("WriterMock.Write got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) + } + + mm_results := mmWrite.WriteMock.defaultExpectation.results + if mm_results == nil { + mmWrite.t.Fatal("No results are set for the WriterMock.Write") + } + return (*mm_results).n, (*mm_results).err + } + if mmWrite.funcWrite != nil { + return mmWrite.funcWrite(p) + } + mmWrite.t.Fatalf("Unexpected call to WriterMock.Write. %v", p) + return +} + +// WriteAfterCounter returns a count of finished WriterMock.Write invocations +func (mmWrite *WriterMock) WriteAfterCounter() uint64 { + return mm_atomic.LoadUint64(&mmWrite.afterWriteCounter) +} + +// WriteBeforeCounter returns a count of WriterMock.Write invocations +func (mmWrite *WriterMock) WriteBeforeCounter() uint64 { + return mm_atomic.LoadUint64(&mmWrite.beforeWriteCounter) +} + +// Calls returns a list of arguments used in each call to WriterMock.Write. +// The list is in the same order as the calls were made (i.e. recent calls have a higher index) +func (mmWrite *mWriterMockWrite) Calls() []*WriterMockWriteParams { + mmWrite.mutex.RLock() + + argCopy := make([]*WriterMockWriteParams, len(mmWrite.callArgs)) + copy(argCopy, mmWrite.callArgs) + + mmWrite.mutex.RUnlock() + + return argCopy +} + +// MinimockWriteDone returns true if the count of the Write invocations corresponds +// the number of defined expectations +func (m *WriterMock) MinimockWriteDone() bool { + for _, e := range m.WriteMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + return false + } + } + + // if default expectation was set then invocations count should be greater than zero + if m.WriteMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterWriteCounter) < 1 { + return false + } + // if func was set then invocations count should be greater than zero + if m.funcWrite != nil && mm_atomic.LoadUint64(&m.afterWriteCounter) < 1 { + return false + } + return true +} + +// MinimockWriteInspect logs each unmet expectation +func (m *WriterMock) MinimockWriteInspect() { + for _, e := range m.WriteMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + m.t.Errorf("Expected call to WriterMock.Write with params: %#v", *e.params) + } + } + + // if default expectation was set then invocations count should be greater than zero + if m.WriteMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterWriteCounter) < 1 { + if m.WriteMock.defaultExpectation.params == nil { + m.t.Error("Expected call to WriterMock.Write") + } else { + m.t.Errorf("Expected call to WriterMock.Write with params: %#v", *m.WriteMock.defaultExpectation.params) + } + } + // if func was set then invocations count should be greater than zero + if m.funcWrite != nil && mm_atomic.LoadUint64(&m.afterWriteCounter) < 1 { + m.t.Error("Expected call to WriterMock.Write") + } +} + +// MinimockFinish checks that all mocked methods have been called the expected number of times +func (m *WriterMock) MinimockFinish() { + if !m.minimockDone() { + m.MinimockWriteInspect() + m.t.FailNow() + } +} + +// MinimockWait waits for all mocked methods to be called the expected number of times +func (m *WriterMock) MinimockWait(timeout mm_time.Duration) { + timeoutCh := mm_time.After(timeout) + for { + if m.minimockDone() { + return + } + select { + case <-timeoutCh: + m.MinimockFinish() + return + case <-mm_time.After(10 * mm_time.Millisecond): + } + } +} + +func (m *WriterMock) minimockDone() bool { + done := true + return done && + m.MinimockWriteDone() +} diff --git a/testing/testconstants/constants.go b/testing/testconstants/constants.go new file mode 100644 index 00000000..ead3800a --- /dev/null +++ b/testing/testconstants/constants.go @@ -0,0 +1,15 @@ +package testconstants + +var ( + SourceHost1 = "host1" + SourceHost2 = "host2" + SourceHost3 = "host3" +) + +var ( + TargetHost1 = "target-host1" + TargetHost2 = "target-host2" + TargetHost3 = "target-host3" +) + +const AllMethods = "GET, PUT, POST, HEAD, TRACE, DELETE, PATCH, COPY, HEAD, LINK, OPTIONS" diff --git a/testing/testconstants/errors.go b/testing/testconstants/errors.go new file mode 100644 index 00000000..32f52cf5 --- /dev/null +++ b/testing/testconstants/errors.go @@ -0,0 +1,5 @@ +package testconstants + +import "errors" + +var ErrTest1 = errors.New("test error #1") diff --git a/testing/testconstants/files.go b/testing/testconstants/files.go new file mode 100644 index 00000000..927b0c6b --- /dev/null +++ b/testing/testconstants/files.go @@ -0,0 +1,6 @@ +package testconstants + +const ( + CertFilePath = "/etc/certificates/cert-file.pem" + KeyFilePath = "/etc/certificates/key-file.key" +) diff --git a/testing/testconstants/hosts.go b/testing/testconstants/hosts.go new file mode 100644 index 00000000..acb1fea7 --- /dev/null +++ b/testing/testconstants/hosts.go @@ -0,0 +1,55 @@ +package testconstants + +import ( + "strconv" +) + +var ( + Localhost = "localhost" + HTTPLocalhost = "http://localhost" + HTTPLocalhostWithPort = portFunction(HTTPLocalhost) + HTTPSLocalhost = "https://localhost" + HTTPSLocalhostWithPort = portFunction(HTTPSLocalhost) + + Github = "github.com" + HTTPGithub = "http://github.com" + HTTPSGithub = "https://github.com" + + Stackoverflow = "stackoverflow.com" + HTTPStackoverflow = "http://stackoverflow.com" + HTTPSStackoverflow = "https://stackoverflow.com" + + APIGithub = "api.github.com" + HTTPAPIGithub = "http://api.github.com" + HTTPSAPIGithub = "https://api.github.com" + + Localhost1 = "localhost1" + HTTPLocalhost1 = "http://localhost1" + HTTPLocalhost1WithPort = portFunction(HTTPLocalhost1) + HTTPSLocalhost1 = "https://localhost1" + HTTPSLocalhost1WithPort = portFunction(HTTPSLocalhost1) + + Localhost2 = "localhost2" + HTTPLocalhost2 = "http://localhost2" + HTTPLocalhost2WithPort = portFunction(HTTPLocalhost2) + HTTPSLocalhost2 = "https://localhost2" + HTTPSLocalhost2WithPort = portFunction(HTTPSLocalhost2) + + Localhost3 = "localhost3" + HTTPLocalhost3 = "http://localhost3" + HTTPLocalhost3WithPort = portFunction(HTTPLocalhost3) + HTTPSLocalhost3 = "https://localhost3" + HTTPSLocalhost3WithPort = portFunction(HTTPSLocalhost3) + + Localhost4 = "localhost4" + HTTPLocalhost4 = "http://localhost4" + HTTPLocalhost4WithPort = portFunction(HTTPLocalhost4) + HTTPSLocalhost4 = "https://localhost4" + HTTPSLocalhost4WithPort = portFunction(HTTPSLocalhost4) +) + +func portFunction(host string) func(port int) string { + return func(port int) string { + return host + ":" + strconv.Itoa(port) + } +} diff --git a/testing/testutils/certs.go b/testing/testutils/certs.go index 5157f709..19361995 100644 --- a/testing/testutils/certs.go +++ b/testing/testutils/certs.go @@ -1,3 +1,4 @@ +// nolint: gomnd package testutils import ( @@ -87,10 +88,10 @@ func certSetup(t *testing.T) *Certs { KeyUsage: x509.KeyUsageDigitalSignature, } - certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + certPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) CheckNoError(t, err) - certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivateKey) + certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivateKey.PublicKey, caPrivateKey) CheckNoError(t, err) certPEM := pem.EncodeToMemory(&pem.Block{ @@ -108,7 +109,7 @@ func certSetup(t *testing.T) *Certs { privateKeyPEM := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + Bytes: x509.MarshalPKCS1PrivateKey(certPrivateKey), }) CheckNoError(t, err) diff --git a/testing/testutils/fs.go b/testing/testutils/fs.go index 9abe2a9f..6dd2fff1 100644 --- a/testing/testutils/fs.go +++ b/testing/testutils/fs.go @@ -2,8 +2,6 @@ package testutils import ( "os" - "path/filepath" - "runtime" "testing" "github.com/spf13/afero" @@ -24,11 +22,3 @@ func FsFromMap(t *testing.T, files map[string]string) afero.Fs { return fs } - -func PrepareFsForTests(t *testing.T, folder string) afero.Fs { - t.Helper() - _, filename, _, _ := runtime.Caller(1) - dirname := filepath.Join(filepath.Dir(filename), folder) - - return afero.NewReadOnlyFs(afero.NewBasePathFs(afero.NewOsFs(), dirname)) -} diff --git a/testing/testutils/http_body..go b/testing/testutils/http_body..go index e6393bba..76191242 100644 --- a/testing/testutils/http_body..go +++ b/testing/testutils/http_body..go @@ -5,13 +5,15 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/evg4b/uncors/internal/helpers" ) func ReadBody(t *testing.T, recorder *httptest.ResponseRecorder) string { t.Helper() response := recorder.Result() - defer CheckNoError(t, response.Body.Close()) + defer helpers.CloseSafe(response.Body) body, err := io.ReadAll(response.Body) CheckNoError(t, err) @@ -23,7 +25,7 @@ func ReadHeader(t *testing.T, recorder *httptest.ResponseRecorder) http.Header { t.Helper() response := recorder.Result() - defer CheckNoError(t, response.Body.Close()) + defer helpers.CloseSafe(response.Body) return response.Header } diff --git a/testing/testutils/log.go b/testing/testutils/log.go index b60e210a..7487cbe5 100644 --- a/testing/testutils/log.go +++ b/testing/testutils/log.go @@ -8,7 +8,7 @@ import ( ) func LogTest(action func(t *testing.T, output *bytes.Buffer)) func(t *testing.T) { - var buffer = &bytes.Buffer{} + buffer := &bytes.Buffer{} log.SetOutput(buffer) return func(t *testing.T) { diff --git a/testing/testutils/params/constants.go b/testing/testutils/params/constants.go index 7651fed8..38f5c459 100644 --- a/testing/testutils/params/constants.go +++ b/testing/testutils/params/constants.go @@ -1,6 +1,8 @@ package params -const Config = "--config" -const From = "--from" -const To = "--to" -const HttpPort = "--http-port" +const ( + Config = "--config" + From = "--from" + To = "--to" + HTTPPort = "--http-port" +)