Repository: ozontech/cute Branch: master Commit: 9f4583b9e8d9 Files: 72 Total size: 255.0 KB Directory structure: gitextract_fkbtdg57/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── main.yml ├── .gitignore ├── .golangci.yaml ├── LICENSE ├── Makefile ├── README.MD ├── allure.go ├── assert.go ├── assert_broken.go ├── assert_broken_test.go ├── assert_optional.go ├── assert_optional_test.go ├── assert_require.go ├── assert_require_test.go ├── assert_trace.go ├── asserts/ │ ├── headers/ │ │ ├── headers.go │ │ └── headers_test.go │ └── json/ │ ├── json.go │ ├── json_test.go │ └── util.go ├── builder.go ├── builder_allure.go ├── builder_asserts.go ├── builder_middleware.go ├── builder_option.go ├── builder_request.go ├── builder_retry.go ├── builder_table.go ├── builder_table_test.go ├── builder_test.go ├── cute.go ├── errors/ │ ├── broken.go │ ├── error.go │ ├── optional.go │ ├── require.go │ └── trace.go ├── examples/ │ ├── custom_asserts.go │ ├── inside_step_test.go │ ├── masked_data_test.go │ ├── parallel_test.go │ ├── single_test.go │ ├── suite/ │ │ ├── common.go │ │ ├── main_test.go │ │ ├── one_step.go │ │ ├── one_step_errors.go │ │ ├── resources/ │ │ │ └── example_valid_request.json │ │ ├── simple.go │ │ └── two_steps.go │ ├── table_test/ │ │ └── table_test.go │ ├── two_step_test.go │ └── upload_file_test.go ├── go.mod ├── go.sum ├── init.go ├── interface.go ├── internal/ │ └── utils/ │ ├── body.go │ └── json.go ├── json_marshaler.go ├── jsonschema.go ├── jsonschema_test.go ├── logger.go ├── provider.go ├── request.go ├── request_test.go ├── result.go ├── result_test.go ├── roundtripper.go ├── step.go ├── test.go └── test_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report \ about: Create a report to help us improve \ title: ' ' \ labels: ' ' \ assignees: ' ' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Used func: '...' 2. What you actually wanted to do: '...' 3. What has been expected: '...' 4. What you got actual: '....' 5. Error Log (if any): '...' 6. Allure-Report screenshot (if any): '...' **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request \ about: Suggest an idea for this project \ title: '' \ labels: '' \ assignees: '' --- **Is your feature request related to a problem? Please describe.** \ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** \ A clear and concise description of what you want to happen. **Describe alternatives you've considered** \ A clear and concise description of any alternative solutions or features you've considered. **Additional context** \ Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build_and_test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.21 - name: Build run: go build -v ./... - name: Test run: go test -v ./... golangci-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run golangci-lint # You may pin to the exact commit or the version. # uses: golangci/golangci-lint-action@5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018 uses: golangci/golangci-lint-action@v2.5.2 # for settings see https://github.com/golangci/golangci-lint-action with: only-new-issues: true # golangci-lint command line arguments args: --timeout=5m0s examples: name: examples runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run examples run: make examples - name: Archive code coverage results uses: actions/upload-artifact@v4 with: name: allure-results path: ./examples/allure-results ================================================ FILE: .gitignore ================================================ .idea/ testdata/ allure-results/ bin/ vendor/ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out ================================================ FILE: .golangci.yaml ================================================ run: timeout: 10m issues-exit-code: 1 tests: true skip-dirs: - bin - vendor - var - tmp skip-files: - \.pb\.go$ - \.pb\.goclay\.go$ output: format: colored-line-number print-issued-lines: true print-linter-name: true linters-settings: govet: check-shadowing: true dupl: threshold: 100 goconst: min-len: 2 min-occurrences: 2 linters: disable-all: true enable: - errcheck - goconst - goimports - gosec - govet - ineffassign - megacheck - revive - typecheck - unused # will be used insted of varcheck + deadcode + structcheck. More info https://github.com/golangci/golangci-lint/issues/1841 - prealloc - wsl issues: exclude-use-default: false exclude: # _ instead of err checks - G104 # for "public interface + private struct implementation" cases only! - exported func .* returns unexported type .*, which can be annoying to use # can be removed in the development phase # - (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form) # not for the active development - can be removed in the stable phase - should have a package comment - don't use an underscore in package name # EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked - should check returned error before deferring ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ export GO111MODULE=on export GOSUMDB=off LOCAL_BIN:=$(CURDIR)/bin ##################### GOLANG-CI RELATED CHECKS ##################### # Check global GOLANGCI-LINT GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint GOLANGCI_TAG:=1.54.2 # Check local bin version ifneq ($(wildcard $(GOLANGCI_BIN)),) GOLANGCI_BIN_VERSION:=$(shell $(GOLANGCI_BIN) --version) ifneq ($(GOLANGCI_BIN_VERSION),) GOLANGCI_BIN_VERSION_SHORT:=$(shell echo "$(GOLANGCI_BIN_VERSION)" | sed -E 's/.* version (.*) built from .* on .*/\1/g') else GOLANGCI_BIN_VERSION_SHORT:=0 endif ifneq "$(GOLANGCI_TAG)" "$(word 1, $(sort $(GOLANGCI_TAG) $(GOLANGCI_BIN_VERSION_SHORT)))" GOLANGCI_BIN:= endif endif # Check global bin version ifneq (, $(shell which golangci-lint)) GOLANGCI_VERSION:=$(shell golangci-lint --version 2> /dev/null ) ifneq ($(GOLANGCI_VERSION),) GOLANGCI_VERSION_SHORT:=$(shell echo "$(GOLANGCI_VERSION)"|sed -E 's/.* version (.*) built from .* on .*/\1/g') else GOLANGCI_VERSION_SHORT:=0 endif ifeq "$(GOLANGCI_TAG)" "$(word 1, $(sort $(GOLANGCI_TAG) $(GOLANGCI_VERSION_SHORT)))" GOLANGCI_BIN:=$(shell which golangci-lint) endif endif ##################### GOLANG-CI RELATED CHECKS ##################### .PHONY: install install: go mod tidy && go mod download # run full lint like in pipeline .PHONY: lint lint: install-lint $(GOLANGCI_BIN) run --config=.golangci.yaml ./... --new-from-rev=origin/master --build-tags=examples,allure_go,provider .PHONY: install-lint install-lint: ifeq ($(wildcard $(GOLANGCI_BIN)),) $(info #Downloading golangci-lint v$(GOLANGCI_TAG)) tmp=$$(mktemp -d) && cd $$tmp && pwd && go mod init temp && go get -d github.com/golangci/golangci-lint/cmd/golangci-lint@v$(GOLANGCI_TAG) && \ go build -ldflags "-X 'main.version=$(GOLANGCI_TAG)' -X 'main.commit=test' -X 'main.date=test'" -o $(LOCAL_BIN)/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint && \ rm -rf $$tmp GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint endif .PHONY: cover cover: go test -v -coverprofile=coverage.out ./... && go tool cover -html=coverage.out .PHONY: example example: go test ./... -tags example .PHONY: test test: go test ./... ================================================ FILE: README.MD ================================================

cute

# CUTE — create your tests easily HTTP and REST API testing for Go using Allure reports. ## Features - Expressive and intuitive syntax. - Built-in JSON support. - Custom asserts. - One step to BDD. - Allure reports. --- ## Head of contents - [Features](#features) - [Workflow](#workflow) - [Installation](#installation) - [Requirements](#requirements) - [Demo](#demo) - [Test examples](#test-examples) - [Single test](#single-step-test) - [Multi-step test](#multi-step-test) - [Suite tests](#suite) - [Table tests](#table-tests) - [Asserts](#asserts) - [Ready-made asserts](#ready-made-asserts) - [JSON asserts](#json-asserts) - [Headers asserts](#headers-asserts) - [JSON schema](#json-schema-validations) - [Custom asserts](#custom-asserts) - [Base](#base) - [T](#t) - [Errors](#assert-errors) - [Global Environment Keys](#global-environment-keys) ## Workflow 1. Create a request and write assets. 2. Run tests. 3. Check Allure reports. ## Installation ```bash go get -u github.com/ozontech/cute ``` ## Requirements - Go 1.17+ ## Demo Run example. ```bash make example ``` To view detailed test reports, install Allure framework. It's optional. [Learn more about Allure reports](https://github.com/allure-framework) ```bash brew install allure ``` Run Allure. ```bash allure serve ./examples/allure-results ``` ## Test examples See [**Examples**](examples) directory for featured examples. ###

Single-step test

Allows implementing single-request tests. See full example in the [**Examples**](examples) directory. To view an Allure report, use `testing.T` or `provider.T` from [allure-go](https://github.com/ozontech/allure-go/tree/master?tab=readme-ov-file). ```go import ( "context" "net/http" "path" "testing" "time" "github.com/ozontech/cute" "github.com/ozontech/cute/asserts/json" ) func TestExample(t *testing.T) { cute.NewTestBuilder(). Title("Title"). Description("some_description"). Create(). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), ). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusOK). AssertBody( json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), ). ExecuteTest(context.Background(), t) } ```
Allure report ![img.png](.images/simple.png)
###

Multi-step test

Allows implementing several requests within one test. ```go import ( "context" "fmt" "net/http" "testing" "github.com/ozontech/cute" ) func Test_TwoSteps(t *testing.T) { responseCode := 0 // First step cute.NewTestBuilder(). Title("Test with two requests and parse body."). Tag("two_steps"). Create(). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), ). ExpectStatus(http.StatusOK). NextTest(). // Execute after first step and parse response code AfterTestExecute(func(response *http.Response, errors []error) error { responseCode = response.StatusCode return nil }). // Second step Create(). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/2/comments"), cute.WithMethod(http.MethodDelete), ). ExecuteTest(context.Background(), t) fmt.Println("Response code from first request", responseCode) } ``` See full in the [**Examples**](examples/two_step_test.go) directory.
Allure report ![multistep_test.png](.images/multistep_test.png)
###

Suite

Suite provides a structure for describing tests by organizing them into test suites. It's helpful if you have a large number of different tests and find it difficult to browse through them without using additional layer nesting levels of test calls. [Learn more about suite with Allure reports](https://github.com/ozontech/allure-go#suite) 1. Declare a structure with `suite.Suite` and `*cute.HTTPTestMaker`. ```go import ( "github.com/ozontech/cute" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/allure-go/pkg/framework/suite" ) type ExampleSuite struct { suite.Suite host *url.URL testMaker *cute.HTTPTestMaker } func (i *ExampleSuite) BeforeAll(t provider.T) { // Prepare http test builder i.testMaker = cute.NewHTTPTestMaker() // Preparing host host, err := url.Parse("https://jsonplaceholder.typicode.com/") if err != nil { t.Fatalf("could not parse url, error %w", err) } i.host = host } ``` 2. Declare a test. ```go import ( "github.com/ozontech/allure-go/pkg/framework/suite" ) func TestExampleTest(t *testing.T) { suite.RunSuite(t, new(ExampleSuite)) } ``` 3. Describe tests. ```go import ( "github.com/ozontech/cute" "github.com/ozontech/cute/asserts/headers" "github.com/ozontech/cute/asserts/json" ) func (i *ExampleSuite) TestExample_OneStep(t provider.T) { var ( testBuilder = i.testMaker.NewTestBuilder() ) u, _ := url.Parse(i.host.String()) u.Path = path.Join(u.Path, "/posts/1/comments") testBuilder. Title("TestExample_OneStep"). Tags("one_step", "some_local_tag", "json"). Create(). StepName("Example GET json request"). RequestBuilder( cute.WithHeaders(map[string][]string{ "some_header": []string{"something"}, "some_array_header": []string{"1", "2", "3", "some_thing"}, }), cute.WithURL(u), cute.WithMethod(http.MethodGet), ). ExpectExecuteTimeout(10*time.Second). ExpectJSONSchemaFile("file://./resources/example_valid_request.json"). ExpectStatus(http.StatusOK). AssertBody( json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), json.NotPresent("$[1].some_not_present"), json.GreaterThan("$", 3), json.Length("$", 5), json.LessThan("$", 100), json.NotEqual("$[3].name", "kekekekeke"), ). OptionalAssertBody( json.GreaterThan("$", 3), json.Length("$", 5), json.LessThan("$", 100), ). AssertHeaders( headers.Present("Content-Type"), ). ExecuteTest(context.Background(), t) } ``` See full example in the [**Examples**](examples/suite) directory.
Allure report ![one_step.png](.images/one_step.png)
##

Table tests

You can create a table test in 2 ways. They'll have the same Allure reports. ### Builder table tests ```go import ( "context" "fmt" "net/http" "testing" "github.com/ozontech/cute" ) func Test_Table_Array(t *testing.T) { tests := []*cute.Test{ { Name: "test_1", Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodPost), }, }, Expect: &cute.Expect{ Code: 200, }, }, { Name: "test_2", Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 200, AssertBody: []cute.AssertBody{ json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), func(body []byte) error { return errors.NewAssertError("example error", "example message", nil, nil) }, }, }, }, } cute.NewTestBuilder(). Title("Example table test"). Tag("table_test"). Description("Execute array tests"). CreateTableTest(). PutTests(tests...). ExecuteTest(context.Background(), t) } ``` ### Array tests ```go func Test_Execute_Array(t *testing.T) { tests := []*cute.Test{ { Name: "test_1", Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodPost), }, }, Expect: &cute.Expect{ Code: 200, }, }, { Name: "test_2", Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 200, AssertBody: []cute.AssertBody{ json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), func(body []byte) error { return errors.NewAssertError("example error", "example message", nil, nil) }, }, }, }, } for _, test := range tests { test.Execute(context.Background(), t) } } ``` See full example in the [**Examples**](examples/table_test/table_test.go) directory.
Allure report Common report for all table tests: ![table_tests_execute_array.png](.images/table_tests_execute_array.png) Main report: ![table_tests_execute_array_test_1.png](.images/table_tests_execute_array_test_1.png)

Asserts

You can create your own asserts or use ready-made from the package asserts. ### Ready-made asserts ####

JSON asserts

- `Equal` is a function to assert that a JSONPath expression matches the given value. - `NotEqual` is a function to check that a JSONPath expression value isn't equal to the given value. - `Length` is a function to assert that value is the expected length. - `GreaterThan` is a function to assert that value is greater than the given length. - `GreaterOrEqualThan` is a function to assert that value is greater or equal to the given length. - `LessThan` is a function to assert that value is less than the given length. - `LessOrEqualThan` is a function to assert that value is less or equal to the given length. - `Present` is a function to assert that value is present. Value can be 0 or null. - `NotEmpty` is a function to assert that value is present and not empty. Value can't be 0 or null. - `NotPresent` is a function to assert that value isn't present. - `Diff` is a function to compare two JSONs. - `Contains` is a function to assert that a JSONPath expression extracts a value in an array. - `EqualJSON` is a function to check that a JSON path expression value is equal to given JSON. - `NotEqualJSON` is a function to check that a JSONPath expression value isn't equal to given JSON. - `GetValueFromJSON` is a function for getting a value from a JSON. [Learn more about expressions](https://goessner.net/articles/JsonPath/) [Learn more about asserts implementation](https://github.com/ozontech/cute/blob/master/asserts/json/json.go) ####

Headers asserts

- `Present` is a function to assert that header is present. - `NotPresent` is a function to assert that header isn't present. [Learn more about asserts implementation](asserts/headers/headers.go) ####

JSON schema validations

You can validate a JSON schema in 3 ways. Choose a way depending on JSON schema location. - `ExpectJSONSchemaString(string)` is a function for validating a JSON schema from a string. - `ExpectJSONSchemaByte([]byte)` is a function for validating a JSON schema from an array of bytes. - `ExpectJSONSchemaFile(string)` is a function for validating a JSON schema from a file or remote resource.
Allure report ![img.png](.images/json_schema.png)
###

Custom asserts

You can implement [3 type of asserts](assert.go): #### Base Types for creating custom assertions. ```go type AssertBody func(body []byte) error type AssertHeaders func(headers http.Header) error type AssertResponse func(response *http.Response) error ``` Example: ```go func customAssertBody() cute.AssertBody { return func(bytes []byte) error { if len(bytes) == 0 { return errors.New("response body is empty") } return nil } } ``` #### T Used for creating custom asserts via [Allure Actions](https://github.com/ozontech/allure-go#suite) and [testing.TB](https://pkg.go.dev/testing#TB). You can: - log information to Allure, - log error on Allure yourself, - return an error. ```go type AssertBodyT func(t cute.T, body []byte) error type AssertHeadersT func(t cute.T, headers http.Header) error type AssertResponseT func(t cute.T, response *http.Response) error ``` Example with T: ```go func customAssertBodyT() cute.AssertBodyT { return func(t cute.T, bytes []byte) error { require.GreaterOrEqual(t, len(bytes), 100) return nil } } ``` Example with creating steps: ```go func customAssertBodySuite() cute.AssertBodyT { return func(t cute.T, bytes []byte) error { step := allure.NewSimpleStep("Custom assert step") defer func() { t.Step(step) }() if len(bytes) == 0 { step.Status = allure.Failed step.Attachment(allure.NewAttachment("Error", allure.Text, []byte("response body is empty"))) return nil } return nil } } ```
Allure report ![custom_assert.png](.images/custom_assert.png)
####

Assert errors

You can use `errors.NewAssertError` method from [errors](errors/error.go) package. Example: ```go import ( "github.com/ozontech/cute" "github.com/ozontech/cute/errors" ) func customAssertBodyWithCustomError() cute.AssertBody { return func(bytes []byte) error { if len(bytes) == 0 { return errors.NewAssertError("customAssertBodyWithCustomError", "body must be not empty", "len is 0", "len more 0") } return nil } } ``` To create a pretty-error in your custom assert, implement it with [interfaces](errors/error.go): - Name. ```go type WithNameError interface { GetName() string SetName(string) } ``` - Parameters for Allure step. ```go type WithFields interface { GetFields() map[string]interface{} PutFields(map[string]interface{}) } ```
Allure report ![assert_error.png](.images/assert_error.png)
#### Optional assert If assert returns an optional error, step fails but the test is successful. You can use `errors.NewOptionalError(error)` method from [errors](errors/error.go) package. ```go import ( "github.com/ozontech/cute" "github.com/ozontech/cute/errors" ) func customAssertBodyWithCustomError() cute.AssertBody { return func(bytes []byte) error { if len(bytes) == 0 { return errors.NewOptionalError("body is empty") } return nil } } ``` To create optional error, implement error with interface: ```go type OptionalError interface { IsOptional() bool SetOptional(bool) } ```
Allure report ![optional_error.png](.images/optional_error.png)
##

Global Environment Keys

| Key | Meaning | Default | |---|---------------------------------------------------------------|-------------------------| |`ALLURE_OUTPUT_PATH`| Path to output allure results. | `.` (Folder with tests) | |`ALLURE_OUTPUT_FOLDER`| Name of result folder. | `/allure-results` | |`ALLURE_ISSUE_PATTERN`| Url pattepn to issue. Must contain `%s`. | | |`ALLURE_TESTCASE_PATTERN`| URL pattern to TestCase. Must contain `%s`. | | |`ALLURE_LAUNCH_TAGS`| Default tags for all tests. Tags must be separated by commas. | | ================================================ FILE: allure.go ================================================ package cute func (qt *cute) setAllureInformation(t allureProvider) { // Log main vars to allureProvider qt.setLabelsAllure(t) qt.setInfoAllure(t) qt.setLinksAllure(t) } func (qt *cute) setLinksAllure(t linksAllureProvider) { if qt.allureLinks.issue != "" { t.SetIssue(qt.allureLinks.issue) } if qt.allureLinks.testCase != "" { t.SetTestCase(qt.allureLinks.testCase) } if qt.allureLinks.link != nil { t.Link(qt.allureLinks.link) } if qt.allureLinks.tmsLink != "" { t.TmsLink(qt.allureLinks.tmsLink) } if len(qt.allureLinks.tmsLinks) > 0 { t.TmsLinks(qt.allureLinks.tmsLinks...) } } func (qt *cute) setLabelsAllure(t labelsAllureProvider) { if qt.allureLabels.id != "" { t.ID(qt.allureLabels.id) } if qt.allureLabels.suiteLabel != "" { t.AddSuiteLabel(qt.allureLabels.suiteLabel) } if qt.allureLabels.subSuite != "" { t.AddSubSuite(qt.allureLabels.subSuite) } if qt.allureLabels.parentSuite != "" { t.AddParentSuite(qt.allureLabels.parentSuite) } if qt.allureLabels.story != "" { t.Story(qt.allureLabels.story) } if qt.allureLabels.tag != "" { t.Tag(qt.allureLabels.tag) } if qt.allureLabels.allureID != "" { t.AllureID(qt.allureLabels.allureID) } if qt.allureLabels.severity != "" { t.Severity(qt.allureLabels.severity) } if qt.allureLabels.owner != "" { t.Owner(qt.allureLabels.owner) } if qt.allureLabels.lead != "" { t.Lead(qt.allureLabels.lead) } if qt.allureLabels.label != nil { t.Label(qt.allureLabels.label) } if len(qt.allureLabels.labels) != 0 { t.Labels(qt.allureLabels.labels...) } if qt.allureLabels.feature != "" { t.Feature(qt.allureLabels.feature) } if qt.allureLabels.epic != "" { t.Epic(qt.allureLabels.epic) } if len(qt.allureLabels.tags) != 0 { t.Tags(qt.allureLabels.tags...) } if qt.allureLabels.layer != "" { t.Layer(qt.allureLabels.layer) } } func (qt *cute) setInfoAllure(t infoAllureProvider) { if qt.allureInfo.title != "" { t.Title(qt.allureInfo.title) } if qt.allureInfo.description != "" { t.Description(qt.allureInfo.description) } if qt.allureInfo.stage != "" { t.Stage(qt.allureInfo.stage) } } ================================================ FILE: assert.go ================================================ package cute import ( "net/http" ) // This is type of asserts, for create some assert with using custom logic. // AssertBody is type for create custom assertions for body // Example asserts: // - json.LengthGreaterThan // - json.LengthGreaterOrEqualThan // - json.LengthLessThan // - json.LengthLessOrEqualThan // - json.Present // - json.NotEmpty // - json.NotPresent type AssertBody func(body []byte) error // AssertHeaders is type for create custom assertions for headers // Example asserts: // - headers.Present // - headers.NotPresent type AssertHeaders func(headers http.Header) error // AssertResponse is type for create custom assertions for response type AssertResponse func(response *http.Response) error // This is type for create custom assertions with using allure and testing.allureProvider // AssertBodyT is type for create custom assertions for body with TB // Check example in AssertBody // TB is testing.T and it can be used for require ore assert from testify or another packages type AssertBodyT func(t T, body []byte) error // AssertHeadersT is type for create custom assertions for headers with TB // Check example in AssertHeaders // TB is testing.T and it can be used for require ore assert from testify or another packages type AssertHeadersT func(t T, headers http.Header) error // AssertResponseT is type for create custom assertions for response with TB // Check example in AssertResponse // TB is testing.T and it can be used for require ore assert from testify or another packages type AssertResponseT func(t T, response *http.Response) error func (it *Test) assertHeaders(t internalT, headers http.Header) []error { var ( asserts = it.Expect.AssertHeaders assertT = it.Expect.AssertHeadersT ) if len(asserts) == 0 && len(assertT) == 0 { return nil } return it.executeWithStep(t, "Assert headers", func(t T) []error { errs := make([]error, 0) // Execute assert only response for _, f := range asserts { err := f(headers) if err != nil { errs = append(errs, err) } } // Execute assert for response with TB for _, f := range assertT { err := f(t, headers) if err != nil { errs = append(errs, err) } } return errs }) } func (it *Test) assertResponse(t internalT, resp *http.Response) []error { var ( asserts = it.Expect.AssertResponse assertT = it.Expect.AssertResponseT ) if len(asserts) == 0 && len(assertT) == 0 { return nil } return it.executeWithStep(t, "Assert response", func(t T) []error { errs := make([]error, 0) // Execute assert only response for _, f := range asserts { err := f(resp) if err != nil { errs = append(errs, err) } } // Execute assert for response with TB for _, f := range assertT { err := f(t, resp) if err != nil { errs = append(errs, err) } } return errs }) } func (it *Test) assertBody(t internalT, body []byte) []error { var ( asserts = it.Expect.AssertBody assertT = it.Expect.AssertBodyT ) if len(asserts) == 0 && len(assertT) == 0 { return nil } return it.executeWithStep(t, "Assert body", func(t T) []error { errs := make([]error, 0) // Execute assert only response for _, f := range asserts { err := f(body) if err != nil { errs = append(errs, err) } } // Execute assert for response with TB for _, f := range assertT { err := f(t, body) if err != nil { errs = append(errs, err) } } return errs }) } ================================================ FILE: assert_broken.go ================================================ package cute import ( "net/http" "github.com/ozontech/cute/errors" ) func brokenAssertHeaders(assert AssertHeaders) AssertHeaders { return func(headers http.Header) error { err := assert(headers) return wrapBrokenError(err) } } func brokenAssertBody(assert AssertBody) AssertBody { return func(body []byte) error { err := assert(body) return wrapBrokenError(err) } } func brokenAssertResponse(assert AssertResponse) AssertResponse { return func(resp *http.Response) error { err := assert(resp) return wrapBrokenError(err) } } func brokenAssertHeadersT(assert AssertHeadersT) AssertHeadersT { return func(t T, headers http.Header) error { err := assert(t, headers) return wrapBrokenError(err) } } func brokenAssertBodyT(assert AssertBodyT) AssertBodyT { return func(t T, body []byte) error { err := assert(t, body) return wrapBrokenError(err) } } func brokenAssertResponseT(assert AssertResponseT) AssertResponseT { return func(t T, resp *http.Response) error { err := assert(t, resp) return wrapBrokenError(err) } } func wrapBrokenError(err error) error { if err == nil { return nil } if tErr, ok := err.(errors.BrokenError); ok { tErr.SetBroken(true) return tErr.(error) } return errors.WrapBrokenError(err) } ================================================ FILE: assert_broken_test.go ================================================ package cute import ( "errors" "net/http" "testing" cuteErrors "github.com/ozontech/cute/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBrokenAssertResponse(t *testing.T) { v := &http.Response{} f := func(_ *http.Response) error { return errors.New("test error") } err := brokenAssertResponse(f)(v) if BrokenError, ok := err.(cuteErrors.BrokenError); assert.True(t, ok) { require.True(t, BrokenError.IsBroken()) } } func TestBrokenAssertResponseT(t *testing.T) { v := &http.Response{} f := func(T, *http.Response) error { return errors.New("test error") } err := brokenAssertResponseT(f)(nil, v) if BrokenError, ok := err.(cuteErrors.BrokenError); assert.True(t, ok) { require.True(t, BrokenError.IsBroken()) } } func TestBrokenAssertHeaders(t *testing.T) { h := http.Header{} f := func(_ http.Header) error { return errors.New("test error") } err := brokenAssertHeaders(f)(h) if BrokenError, ok := err.(cuteErrors.BrokenError); assert.True(t, ok) { require.True(t, BrokenError.IsBroken()) } } func TestBrokenAssertHeadersT(t *testing.T) { h := http.Header{} f := func(T, http.Header) error { return errors.New("test error") } err := brokenAssertHeadersT(f)(nil, h) if BrokenError, ok := err.(cuteErrors.BrokenError); assert.True(t, ok) { require.True(t, BrokenError.IsBroken()) } } func TestBrokenAssertBody(t *testing.T) { v := []byte{} f := func(_ []byte) error { return errors.New("test error") } err := brokenAssertBody(f)(v) if BrokenError, ok := err.(cuteErrors.BrokenError); assert.True(t, ok) { require.True(t, BrokenError.IsBroken()) } } func TestBrokenAssertBodyT(t *testing.T) { v := []byte{} f := func(T, []byte) error { return errors.New("test error") } err := brokenAssertBodyT(f)(nil, v) if BrokenError, ok := err.(cuteErrors.BrokenError); assert.True(t, ok) { require.True(t, BrokenError.IsBroken()) } } func TestWrapBrokenError(t *testing.T) { err := errors.New("test error") optError := wrapBrokenError(err) if BrokenError, ok := optError.(cuteErrors.BrokenError); assert.True(t, ok) { require.True(t, BrokenError.IsBroken()) } } ================================================ FILE: assert_optional.go ================================================ package cute import ( "net/http" "github.com/ozontech/cute/errors" ) func optionalAssertHeaders(assert AssertHeaders) AssertHeaders { return func(headers http.Header) error { err := assert(headers) return wrapOptionalError(err) } } func optionalAssertBody(assert AssertBody) AssertBody { return func(body []byte) error { err := assert(body) return wrapOptionalError(err) } } func optionalAssertResponse(assert AssertResponse) AssertResponse { return func(resp *http.Response) error { err := assert(resp) return wrapOptionalError(err) } } func optionalAssertHeadersT(assert AssertHeadersT) AssertHeadersT { return func(t T, headers http.Header) error { err := assert(t, headers) return wrapOptionalError(err) } } func optionalAssertBodyT(assert AssertBodyT) AssertBodyT { return func(t T, body []byte) error { err := assert(t, body) return wrapOptionalError(err) } } func optionalAssertResponseT(assert AssertResponseT) AssertResponseT { return func(t T, resp *http.Response) error { err := assert(t, resp) return wrapOptionalError(err) } } func wrapOptionalError(err error) error { if err == nil { return nil } if tErr, ok := err.(errors.OptionalError); ok { tErr.SetOptional(true) return tErr.(error) } return errors.WrapOptionalError(err) } ================================================ FILE: assert_optional_test.go ================================================ package cute import ( "errors" "net/http" "testing" cuteErrors "github.com/ozontech/cute/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestOptionalAssertResponse(t *testing.T) { v := &http.Response{} f := func(*http.Response) error { return errors.New("test error") } err := optionalAssertResponse(f)(v) if optionalError, ok := err.(cuteErrors.OptionalError); assert.True(t, ok) { require.True(t, optionalError.IsOptional()) } } func TestOptionalAssertResponseT(t *testing.T) { v := &http.Response{} f := func(T, *http.Response) error { return errors.New("test error") } err := optionalAssertResponseT(f)(nil, v) if optionalError, ok := err.(cuteErrors.OptionalError); assert.True(t, ok) { require.True(t, optionalError.IsOptional()) } } func TestOptionalAssertHeaders(t *testing.T) { h := http.Header{} f := func(http.Header) error { return errors.New("test error") } err := optionalAssertHeaders(f)(h) if optionalError, ok := err.(cuteErrors.OptionalError); assert.True(t, ok) { require.True(t, optionalError.IsOptional()) } } func TestOptionalAssertHeadersT(t *testing.T) { h := http.Header{} f := func(T, http.Header) error { return errors.New("test error") } err := optionalAssertHeadersT(f)(nil, h) if optionalError, ok := err.(cuteErrors.OptionalError); assert.True(t, ok) { require.True(t, optionalError.IsOptional()) } } func TestOptionalAssertBody(t *testing.T) { v := []byte{} f := func([]byte) error { return errors.New("test error") } err := optionalAssertBody(f)(v) if optionalError, ok := err.(cuteErrors.OptionalError); assert.True(t, ok) { require.True(t, optionalError.IsOptional()) } } func TestOptionalAssertBodyT(t *testing.T) { v := []byte{} f := func(T, []byte) error { return errors.New("test error") } err := optionalAssertBodyT(f)(nil, v) if optionalError, ok := err.(cuteErrors.OptionalError); assert.True(t, ok) { require.True(t, optionalError.IsOptional()) } } func TestWrapOptionalError(t *testing.T) { err := errors.New("test error") optError := wrapOptionalError(err) if optionalError, ok := optError.(cuteErrors.OptionalError); assert.True(t, ok) { require.True(t, optionalError.IsOptional()) } } ================================================ FILE: assert_require.go ================================================ package cute import ( "net/http" "github.com/ozontech/cute/errors" ) func requireAssertHeaders(assert AssertHeaders) AssertHeaders { return func(headers http.Header) error { err := assert(headers) return wrapRequireError(err) } } func requireAssertBody(assert AssertBody) AssertBody { return func(body []byte) error { err := assert(body) return wrapRequireError(err) } } func requireAssertResponse(assert AssertResponse) AssertResponse { return func(resp *http.Response) error { err := assert(resp) return wrapRequireError(err) } } func requireAssertHeadersT(assert AssertHeadersT) AssertHeadersT { return func(t T, headers http.Header) error { err := assert(t, headers) return wrapRequireError(err) } } func requireAssertBodyT(assert AssertBodyT) AssertBodyT { return func(t T, body []byte) error { err := assert(t, body) return wrapRequireError(err) } } func requireAssertResponseT(assert AssertResponseT) AssertResponseT { return func(t T, resp *http.Response) error { err := assert(t, resp) return wrapRequireError(err) } } func wrapRequireError(err error) error { if err == nil { return nil } if tErr, ok := err.(errors.RequireError); ok { tErr.SetRequire(true) return tErr.(error) } return errors.WrapRequireError(err) } ================================================ FILE: assert_require_test.go ================================================ package cute import ( "errors" "net/http" "testing" cuteErrors "github.com/ozontech/cute/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRequireAssertResponse(t *testing.T) { v := &http.Response{} f := func(_ *http.Response) error { return errors.New("test error") } err := requireAssertResponse(f)(v) if RequireError, ok := err.(cuteErrors.RequireError); assert.True(t, ok) { require.True(t, RequireError.IsRequire()) } } func TestRequireAssertResponseT(t *testing.T) { v := &http.Response{} f := func(T, *http.Response) error { return errors.New("test error") } err := requireAssertResponseT(f)(nil, v) if RequireError, ok := err.(cuteErrors.RequireError); assert.True(t, ok) { require.True(t, RequireError.IsRequire()) } } func TestRequireAssertHeaders(t *testing.T) { h := http.Header{} f := func(http.Header) error { return errors.New("test error") } err := requireAssertHeaders(f)(h) if RequireError, ok := err.(cuteErrors.RequireError); assert.True(t, ok) { require.True(t, RequireError.IsRequire()) } } func TestRequireAssertHeadersT(t *testing.T) { h := http.Header{} f := func(T, http.Header) error { return errors.New("test error") } err := requireAssertHeadersT(f)(nil, h) if RequireError, ok := err.(cuteErrors.RequireError); assert.True(t, ok) { require.True(t, RequireError.IsRequire()) } } func TestRequireAssertBody(t *testing.T) { v := []byte{} f := func([]byte) error { return errors.New("test error") } err := requireAssertBody(f)(v) if RequireError, ok := err.(cuteErrors.RequireError); assert.True(t, ok) { require.True(t, RequireError.IsRequire()) } } func TestRequireAssertBodyT(t *testing.T) { v := []byte{} f := func(T, []byte) error { return errors.New("test error") } err := requireAssertBodyT(f)(nil, v) if RequireError, ok := err.(cuteErrors.RequireError); assert.True(t, ok) { require.True(t, RequireError.IsRequire()) } } func TestWrapRequireError(t *testing.T) { err := errors.New("test error") optError := wrapRequireError(err) if RequireError, ok := optError.(cuteErrors.RequireError); assert.True(t, ok) { require.True(t, RequireError.IsRequire()) } } ================================================ FILE: assert_trace.go ================================================ package cute import ( "fmt" "net/http" "runtime" "github.com/ozontech/cute/errors" ) // assertHeadersWithTrace is a function to add trace inside assert headers error func assertHeadersWithTrace(assert AssertHeaders, trace string) AssertHeaders { return func(headers http.Header) error { err := assert(headers) return wrapWithTrace(err, trace) } } // assertBodyWithTrace is a function to add trace inside assert body error func assertBodyWithTrace(assert AssertBody, trace string) AssertBody { return func(body []byte) error { err := assert(body) return wrapWithTrace(err, trace) } } // assertResponseWithTrace is a function to add trace inside assert response error func assertResponseWithTrace(assert AssertResponse, trace string) AssertResponse { return func(resp *http.Response) error { err := assert(resp) return wrapWithTrace(err, trace) } } // assertHeadersTWithTrace is a function to add trace inside assert headers error func assertHeadersTWithTrace(assert AssertHeadersT, trace string) AssertHeadersT { return func(t T, headers http.Header) error { err := assert(t, headers) return wrapWithTrace(err, trace) } } // assertBodyTWithTrace is a function to add trace inside assert body error func assertBodyTWithTrace(assert AssertBodyT, trace string) AssertBodyT { return func(t T, body []byte) error { err := assert(t, body) return wrapWithTrace(err, trace) } } // assertResponseTWithTrace is a function to add trace inside assert response error func assertResponseTWithTrace(assert AssertResponseT, trace string) AssertResponseT { return func(t T, resp *http.Response) error { err := assert(t, resp) return wrapWithTrace(err, trace) } } // wrapWithTrace is a function to add trace inside error func wrapWithTrace(err error, trace string) error { if err == nil { return nil } if tErr, ok := err.(errors.WithTrace); ok { tErr.SetTrace(trace) return tErr.(error) } return errors.WrapErrorWithTrace(err, trace) } func getTrace() string { pcs := make([]uintptr, 10) depth := runtime.Callers(3, pcs) if depth == 0 { fmt.Println("Couldn't get the stack information") return "" } callers := runtime.CallersFrames(pcs[:depth]) caller, _ := callers.Next() return fmt.Sprintf("%s:%d", caller.File, caller.Line) } ================================================ FILE: asserts/headers/headers.go ================================================ package headers import ( "fmt" "net/http" "github.com/ozontech/cute" "github.com/ozontech/cute/errors" ) // Present is a function to asserts that header is present func Present(key string) cute.AssertHeaders { return func(headers http.Header) error { if v := headers.Get(key); v == "" { return errors.NewAssertError("Present", fmt.Sprintf("header %s is not present", key), nil, nil) } return nil } } // NotPresent is a function to asserts that header is not present func NotPresent(key string) cute.AssertHeaders { return func(headers http.Header) error { if v := headers.Values(key); len(v) > 0 { return errors.NewAssertError("NotPresent", fmt.Sprintf("header %s is present", key), nil, nil) } return nil } } ================================================ FILE: asserts/headers/headers_test.go ================================================ package headers import ( "net/http" "testing" "github.com/stretchr/testify/require" ) func TestPresent(t *testing.T) { headers := http.Header{ "Content-Type": []string{"application/json"}, } err := Present("Content-Type")(headers) require.NoError(t, err) } func TestPresentError(t *testing.T) { headers := http.Header{ "Content-Type": []string{}, } err := Present("not-present")(headers) require.Error(t, err) } func TestNotPresent(t *testing.T) { headers := http.Header{ "Content-Type": []string{"", "application/json"}, } err := NotPresent("Content-Type")(headers) require.Error(t, err) } func TestNotPresentError(t *testing.T) { headers := http.Header{ "Content-Type": []string{}, } err := NotPresent("not-present")(headers) require.NoError(t, err) } ================================================ FILE: asserts/json/json.go ================================================ package json import ( "fmt" jd "github.com/josephburnett/jd/lib" "github.com/ohler55/ojg/jp" "github.com/ohler55/ojg/oj" "github.com/ozontech/cute" cuteErrors "github.com/ozontech/cute/errors" ) // Diff is a function to compare two jsons func Diff(original string) cute.AssertBody { return func(body []byte) error { originalJSON, err := jd.ReadJsonString(original) if err != nil { return fmt.Errorf("could not parse original json in Diff error: '%s'", err) } bodyJSON, err := jd.ReadJsonString(string(body)) if err != nil { return fmt.Errorf("could not parse body json in Diff error: '%s'", err) } diff := originalJSON.Diff(bodyJSON).Render() if diff != "" { cErr := cuteErrors.NewEmptyAssertError("JSON Diff", "JSON is not the same") cErr.PutAttachment(&cuteErrors.Attachment{ Name: "JSON diff", MimeType: "text/plain", Content: []byte(diff), }) return cErr } return nil } } // Contains is a function to assert that a jsonpath expression extracts a value in an array // About expression - https://goessner.net/articles/JsonPath/ func Contains(expression string, expect interface{}) cute.AssertBody { return func(body []byte) error { return contains(body, expression, expect) } } // Equal is a function to assert that a jsonpath expression matches the given value // About expression - https://goessner.net/articles/JsonPath/ func Equal(expression string, expect interface{}) cute.AssertBody { return func(body []byte) error { return equal(body, expression, expect) } } // NotEqual is a function to check json path expression value is not equal to given value // About expression - https://goessner.net/articles/JsonPath/ func NotEqual(expression string, expect interface{}) cute.AssertBody { return func(body []byte) error { return notEqual(body, expression, expect) } } // EqualJSON is a function to check json path expression value is equal to given json // About expression - https://goessner.net/articles/JsonPath/ func EqualJSON(expression string, expect []byte) cute.AssertBody { return func(body []byte) error { return equalJSON(body, expression, expect) } } // NotEqualJSON is a function to check json path expression value is not equal to given json // About expression - https://goessner.net/articles/JsonPath/ func NotEqualJSON(expression string, expect []byte) cute.AssertBody { return func(body []byte) error { return notEqualJSON(body, expression, expect) } } // Length is a function to asserts that value is the expected length // About expression - https://goessner.net/articles/JsonPath/ func Length(expression string, expectLength int) cute.AssertBody { return func(body []byte) error { return length(body, expression, expectLength) } } // LengthGreaterThan is a function to asserts that value is greater than the given length // About expression - https://goessner.net/articles/JsonPath/ func LengthGreaterThan(expression string, minimumLength int) cute.AssertBody { return func(body []byte) error { return greaterThan(body, expression, minimumLength) } } // LengthGreaterOrEqualThan is a function to asserts that value is greater or equal than the given length // About expression - https://goessner.net/articles/JsonPath/ func LengthGreaterOrEqualThan(expression string, minimumLength int) cute.AssertBody { return func(body []byte) error { return greaterOrEqualThan(body, expression, minimumLength) } } // LengthLessThan is a function to asserts that value is less than the given length // About expression - https://goessner.net/articles/JsonPath/ func LengthLessThan(expression string, maximumLength int) cute.AssertBody { return func(body []byte) error { return lessThan(body, expression, maximumLength) } } // LengthLessOrEqualThan is a function to asserts that value is less or equal than the given length // About expression - https://goessner.net/articles/JsonPath/ func LengthLessOrEqualThan(expression string, maximumLength int) cute.AssertBody { return func(body []byte) error { return lessOrEqualThan(body, expression, maximumLength) } } // Present is a function to asserts that value is present // value can be nil or 0 // About expression - https://goessner.net/articles/JsonPath/ func Present(expression string) cute.AssertBody { return func(body []byte) error { return present(body, expression) } } // NotEmpty is a function to asserts that value is present // value can't be nil or 0 // About expression - https://goessner.net/articles/JsonPath/ func NotEmpty(expression string) cute.AssertBody { return func(body []byte) error { return notEmpty(body, expression) } } // NotPresent is a function to asserts that value is not present // About expression - https://goessner.net/articles/JsonPath/ func NotPresent(expression string) cute.AssertBody { return func(body []byte) error { return notPresent(body, expression) } } // GetValueFromJSON is function for get value from json func GetValueFromJSON(js []byte, expression string) ([]interface{}, error) { obj, err := oj.Parse(js) if err != nil { return nil, fmt.Errorf("could not parse json in GetValueFromJSON error: '%s'", err) } jsonPath, err := jp.ParseString(expression) if err != nil { return nil, fmt.Errorf("could not parse path in GetValueFromJSON error: '%s'", err) } res := jsonPath.Get(obj) if len(res) == 0 { return nil, fmt.Errorf("could not find element by path %v in JSON", expression) } return res, nil } ================================================ FILE: asserts/json/json_test.go ================================================ package json import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type jsonTest struct { caseName string data string expression string expect interface{} IsNilErr bool } func TestDiff(t *testing.T) { testCases := []struct { name string originalJSON string bodyJSON string expectedError string }{ { name: "SameJSON", originalJSON: `{"key1": "value1", "key2": "value2"}`, bodyJSON: `{"key1": "value1", "key2": "value2"}`, expectedError: "", // No error expected, JSONs are the same }, { name: "DifferentValueJSON", originalJSON: `{"key1": "value1", "key2": "value2"}`, bodyJSON: `{"key1": "value1", "key2": "value3"}`, expectedError: "JSON is not the same", }, { name: "MissingKeyJSON", originalJSON: `{"key1": "value1", "key2": "value2"}`, bodyJSON: `{"key1": "value1"}`, expectedError: "JSON is not the same", }, { name: "ExtraKeyJSON", originalJSON: `{"key1": "value1"}`, bodyJSON: `{"key1": "value1", "key2": "value2"}`, expectedError: "JSON is not the same", }, { name: "EmptyJSON", originalJSON: `{}`, bodyJSON: `{}`, expectedError: "", // No error expected, empty JSONs are the same }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { // Call the Diff function with the test input err := Diff(testCase.originalJSON)([]byte(testCase.bodyJSON)) // Check if the error message matches the expected result if testCase.expectedError == "" { assert.NoError(t, err) // No error expected } else { assert.Error(t, err) // Error expected assert.Contains(t, err.Error(), testCase.expectedError) } }) } } func TestNotPresent(t *testing.T) { tests := []jsonTest{ { caseName: "correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", }, { caseName: "not present check", data: `{"o":["a", "b", "c"]}`, expression: "$.b", IsNilErr: true, }, { caseName: "not present check ", data: `{"o":["a", "b", "c"]}`, expression: "$.o[0]", }, { caseName: "correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o[0]['1']", }, } for _, test := range tests { err := NotPresent(test.expression)([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestPresent(t *testing.T) { tests := []jsonTest{ { caseName: "correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", IsNilErr: true, }, { caseName: "not present check", data: `{"o":["a", "b", "c"]}`, expression: "$.b", }, { caseName: "correct present check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o[0]", IsNilErr: true, }, { caseName: "correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o[0]['1']", IsNilErr: true, }, { caseName: "check not correct path", data: `{"o":["a", "b", "c"]}`, expression: "$.not_correct", }, { caseName: "empty integer", data: `{"o":0}`, expression: "$.o", IsNilErr: true, }, { caseName: "empty object", data: `{"o":null}`, expression: "$.o", IsNilErr: true, }, { caseName: "empty string", data: `{"o":null, "b":""}`, expression: "$.b", IsNilErr: true, }, } for _, test := range tests { err := Present(test.expression)([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestNotEmpty(t *testing.T) { tests := []jsonTest{ { caseName: "correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", IsNilErr: true, }, { caseName: "not present check", data: `{"o":["a", "b", "c"]}`, expression: "$.b", }, { caseName: "correct present check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o[0]", IsNilErr: true, }, { caseName: "correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o[0]['1']", IsNilErr: true, }, { caseName: "check not correct path", data: `{"o":["a", "b", "c"]}`, expression: "$.not_correct", }, { caseName: "empty integer", data: `{"o":0}`, expression: "$.o", }, { caseName: "empty object", data: `{"o":null}`, expression: "$.o", }, { caseName: "empty string", data: `{"o":null, "b":""}`, expression: "$.b", }, } for _, test := range tests { err := NotEmpty(test.expression)([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestLength(t *testing.T) { tests := []jsonTest{ { caseName: "correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 3, IsNilErr: true, }, { caseName: "not correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 4, }, { caseName: "correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 6, IsNilErr: true, }, { caseName: "not correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 99, }, { caseName: "check not contain value", data: `{"o":"123456"}`, expression: "$.a", expect: 1, }, { caseName: "correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 3, IsNilErr: true, }, } for _, test := range tests { err := Length(test.expression, test.expect.(int))([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestLengthGreaterThan(t *testing.T) { tests := []jsonTest{ { caseName: "correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 2, IsNilErr: true, }, { caseName: "not correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 4, }, { caseName: "not correct check array when equal", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 3, }, { caseName: "correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 4, IsNilErr: true, }, { caseName: "not correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 99, }, { caseName: "correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 1, IsNilErr: true, }, { caseName: "check not correct path", data: `{"o":["a", "b", "c"]}`, expression: "$.not_correct", expect: 0, }, } for _, test := range tests { err := LengthGreaterThan(test.expression, test.expect.(int))([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestLengthGreaterOrEqualThan(t *testing.T) { tests := []jsonTest{ { caseName: "correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 2, IsNilErr: true, }, { caseName: "correct check array when equal", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 3, IsNilErr: true, }, { caseName: "not correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 4, }, { caseName: "correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 4, IsNilErr: true, }, { caseName: "correct check string when equal", data: `{"o":"123456"}`, expression: "$.o", expect: 6, IsNilErr: true, }, { caseName: "not correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 99, }, { caseName: "correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 1, IsNilErr: true, }, { caseName: "correct check map when equal", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 3, IsNilErr: true, }, { caseName: "not correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 5, }, { caseName: "check not correct path", data: `{"o":["a", "b", "c"]}`, expression: "$.not_correct", expect: 0, }, } for _, test := range tests { err := LengthGreaterOrEqualThan(test.expression, test.expect.(int))([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestLengthLessThan(t *testing.T) { tests := []jsonTest{ { caseName: "correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 4, IsNilErr: true, }, { caseName: "not correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 3, }, { caseName: "correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 7, IsNilErr: true, }, { caseName: "not correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 6, }, { caseName: "correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 4, IsNilErr: true, }, { caseName: "not correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 3, }, { caseName: "check not correct path", data: `{"o":["a", "b", "c"]}`, expression: "$.not_correct", expect: 0, }, } for _, test := range tests { err := LengthLessThan(test.expression, test.expect.(int))([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestLengthLessOrEqualThan(t *testing.T) { tests := []jsonTest{ { caseName: "correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 4, IsNilErr: true, }, { caseName: "correct check array when equal", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 3, IsNilErr: true, }, { caseName: "not correct check array", data: `{"o":["a", "b", "c"]}`, expression: "$.o", expect: 2, }, { caseName: "correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 7, IsNilErr: true, }, { caseName: "correct check string when equal", data: `{"o":"123456"}`, expression: "$.o", expect: 6, IsNilErr: true, }, { caseName: "not correct check string", data: `{"o":"123456"}`, expression: "$.o", expect: 5, }, { caseName: "correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 4, IsNilErr: true, }, { caseName: "correct check map when equal", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 3, IsNilErr: true, }, { caseName: "not correct check map", data: `{"o":[{"1":"a"}, {"2":"b"}, {"3":"c"}]}`, expression: "$.o", expect: 2, }, { caseName: "check not correct path", data: `{"o":["a", "b", "c"]}`, expression: "$.not_correct", expect: 0, }, } for _, test := range tests { err := LengthLessOrEqualThan(test.expression, test.expect.(int))([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestEqual(t *testing.T) { tests := []jsonTest{ { caseName: "valid json", data: `{"first": 777, "second": [{"key_1": "some_key", "value": "some_value"}]}`, expression: "$.second[0].value", expect: "some_value", IsNilErr: true, }, { caseName: "not valid json", data: "{not_valid_json}", }, { caseName: "3rd party key", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.l", expect: nil, }, { caseName: "not array", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.b[bs]", }, { caseName: "valid array", data: `{"arr": ["one","two"]}`, expression: "$.arr", expect: []string{"one", "two"}, IsNilErr: true, }, { caseName: "check equal map", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.b", expect: map[string]interface{}{"bs": "sb"}, IsNilErr: true, }, { caseName: "check equal string", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.a", expect: "as", IsNilErr: true, }, { caseName: "check equal not correct string", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.a", expect: []byte("not_correct"), }, { caseName: "check 186135434", data: `{"a":186135434, "b":{"bs":"sb"}}`, expression: "$.a", expect: 186135434, IsNilErr: true, }, { caseName: "check float", data: `{"a":1.0000001, "b":{"bs":"sb"}}`, expression: "$.a", expect: 1.0000001, IsNilErr: true, }, { caseName: "check float 2", data: `{"a":999.0000001, "b":{"bs":"sb"}}`, expression: "$.a", expect: 999.0000001, IsNilErr: true, }, } for _, test := range tests { err := Equal(test.expression, test.expect)([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestNotEqual(t *testing.T) { tests := []jsonTest{ { caseName: "valid json", data: `{"first": 777, "second": [{"key_1": "some_key", "value": "some_value"}]}`, expression: "$.second[0].value", expect: "some_value", IsNilErr: false, }, { caseName: "not valid json", data: "{not_valid_json}", }, { caseName: "3rd party key", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.l", expect: nil, }, { caseName: "not array", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.b[bs]", expect: "sb", }, { caseName: "check equal map", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.b", expect: map[string]interface{}{"bs": "sb"}, IsNilErr: false, }, { caseName: "check equal string", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.a", expect: "as", IsNilErr: false, }, } for _, test := range tests { err := NotEqual(test.expression, test.expect)([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestEqualJSON(t *testing.T) { tests := []jsonTest{ { caseName: "valid json", data: `{"first": 777, "second": [{"key_1": "some_key", "value": "some_value"}]}`, expression: "$.second[0].value", expect: `"some_value"`, IsNilErr: true, }, { caseName: "not valid json", data: "{not_valid_json}", }, { caseName: "3rd party key", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.l", }, { caseName: "not array", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.b[bs]", }, { caseName: "valid array", data: `{"arr": ["one","two"]}`, expression: "$.arr", expect: `["one", "two"]`, IsNilErr: true, }, { caseName: "check equal map", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.b", expect: `{"bs": "sb"}`, IsNilErr: true, }, { caseName: "check equal string", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.a", expect: `"as"`, IsNilErr: true, }, { caseName: "check equal not correct string", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.a", expect: `"not_correct"`, }, { caseName: "check deep equal", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$", expect: `{ "b": {"bs": "sb"}, "a":"as" }`, IsNilErr: true, }, { caseName: "check deep equal not correct", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$", expect: `{ "b": {"sb": "bs"}, "a":"as" }`, }, { caseName: "check 186135434", data: `{"a":186135434, "b":{"bs":"sb"}}`, expression: "$.a", expect: "186135434", IsNilErr: true, }, { caseName: "check float", data: `{"a":1.0000001, "b":{"bs":"sb"}}`, expression: "$.a", expect: "1.0000001", IsNilErr: true, }, { caseName: "check float 2", data: `{"a":999.0000001, "b":{"bs":"sb"}}`, expression: "$.a", expect: "999.0000001", IsNilErr: true, }, } for _, test := range tests { exp, _ := test.expect.(string) err := EqualJSON(test.expression, []byte(exp))([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestNotEqualJSON(t *testing.T) { tests := []jsonTest{ { caseName: "valid json", data: `{"first": 777, "second": [{"key_1": "some_key", "value": "some_value"}]}`, expression: "$.second[0].value", expect: `"some_value"`, }, { caseName: "not valid json", data: "{not_valid_json}", }, { caseName: "3rd party key", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.l", }, { caseName: "not array", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.b[bs]", }, { caseName: "valid array", data: `{"arr": ["one","two"]}`, expression: "$.arr", expect: `["one", "two"]`, }, { caseName: "check equal map", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.b", expect: `{"bs": "sb"}`, }, { caseName: "check equal string", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.a", expect: `"as"`, }, { caseName: "check equal not correct string", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$.a", expect: `"not_correct"`, IsNilErr: true, }, { caseName: "check deep equal", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$", expect: `{ "b": {"bs": "sb"}, "a":"as" }`, }, { caseName: "check deep equal not correct", data: `{"a":"as", "b":{"bs":"sb"}}`, expression: "$", expect: `{ "b": {"sb": "bs"}, "a":"as" }`, IsNilErr: true, }, { caseName: "check 186135434", data: `{"a":186135434, "b":{"bs":"sb"}}`, expression: "$.a", expect: "186135434", }, { caseName: "check float", data: `{"a":1.0000001, "b":{"bs":"sb"}}`, expression: "$.a", expect: "1.0000001", }, { caseName: "check float 2", data: `{"a":999.0000001, "b":{"bs":"sb"}}`, expression: "$.a", expect: "999.0000001", }, } for _, test := range tests { exp, _ := test.expect.(string) err := NotEqualJSON(test.expression, []byte(exp))([]byte(test.data)) if test.IsNilErr { require.NoError(t, err, "failed test %v", test.caseName) } else { require.Error(t, err, "failed test %v", test.caseName) } } } func TestGetValueFromJSON(t *testing.T) { testCases := []struct { name string inputJSON string expression string expectedValue []interface{} expectedError string }{ { name: "ValidExpressionObject", inputJSON: `{"key1": "value1", "key2": {"key3": "value3"}}`, expression: "key2.key3", expectedValue: []interface{}{"value3"}, expectedError: "", // No error expected }, { name: "ValidExpressionArray", inputJSON: `{"key1": "value1", "key2": [1, 2, 3]}`, expression: "key2[1]", expectedValue: []interface{}{int64(2)}, expectedError: "", // No error expected }, { name: "ValidExpressionMap", inputJSON: `{"key1": "value1", "key2": {"subkey1": "subvalue1"}}`, expression: "key2", expectedValue: []interface{}{map[string]interface{}{"subkey1": "subvalue1"}}, expectedError: "", // No error expected }, { name: "InvalidJSON", inputJSON: `invalid json`, expression: "key1", expectedValue: nil, expectedError: "could not parse json", }, { name: "InvalidExpression", inputJSON: `{"key1": "value1"}`, expression: "key2", expectedValue: nil, expectedError: "could not find element by path key2 in JSON", }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { // Call the GetValueFromJSON function with the test input value, err := GetValueFromJSON([]byte(testCase.inputJSON), testCase.expression) // Check if the error message matches the expected result if testCase.expectedError == "" { assert.NoError(t, err) // No error expected } else { assert.Error(t, err) // Error expected assert.Contains(t, err.Error(), testCase.expectedError) } // Check if the returned value is an array and matches the expected result assert.IsType(t, []interface{}{}, value) assert.Equal(t, testCase.expectedValue, value) }) } } ================================================ FILE: asserts/json/util.go ================================================ package json import ( "bytes" "fmt" "reflect" "strings" "github.com/ohler55/ojg/oj" "github.com/ozontech/cute/errors" ) // Contains is a function to assert that a jsonpath expression extracts a value in an array // Given the response is {"first": 777, "second": [{"key_1": "some_key", "value": "some_value"}]}, we can assert on the result like so `$.second[? @.key_1=="some_key"].value`, "some_value" // About expression - https://goessner.net/articles/JsonPath/ func contains(data []byte, expression string, expect interface{}) error { values, err := GetValueFromJSON(data, expression) if err != nil { return err } for _, value := range values { ok, found := insideArray(value, expect) if !ok { return errors.NewAssertError("Contains", fmt.Sprintf("on path %v. %v could not be applied builtin len()", expression, expect), nil, nil) } if !found { return errors.NewAssertError("Contains", fmt.Sprintf("on path %v. expect %v, but actual %v", expression, expect, value), value, expect) } } return nil } func equalAbstract(data []byte, expression string, expect interface{}, name string) error { values, err := GetValueFromJSON(data, expression) if err != nil { return err } for _, value := range values { if !objectsAreEqual(value, expect) { return errors.NewAssertError(name, fmt.Sprintf("on path %v. expect %v, but actual %v", expression, expect, value), value, expect) } } return nil } func notEqualAbstract(data []byte, expression string, expect interface{}, name string) error { values, err := GetValueFromJSON(data, expression) if err != nil { return err } for _, value := range values { if objectsAreEqual(value, expect) { return errors.NewAssertError(name, fmt.Sprintf("on path %v. expect %v, but actual %v", expression, expect, value), value, expect) } } return nil } // Equal is a function to assert that a jsonpath expression matches the given value // About expression - https://goessner.net/articles/JsonPath/ func equal(data []byte, expression string, expect interface{}) error { return equalAbstract(data, expression, expect, "Equal") } // NotEqual is a function to check json path expression value is not equal to given value // About expression - https://goessner.net/articles/JsonPath/ func notEqual(data []byte, expression string, expect interface{}) error { return notEqualAbstract(data, expression, expect, "NotEqual") } // EqualJSON is a function to check json path expression value is equal to given json // About expression - https://goessner.net/articles/JsonPath/ func equalJSON(data []byte, expression string, expect []byte) error { obj, err := oj.Parse(expect) if err != nil { return fmt.Errorf("could not parse json in EqualJSON error: '%s'", err) } return equalAbstract(data, expression, obj, "EqualJSON") } // NotEqualJSON is a function to check json path expression value is not equal to given json // About expression - https://goessner.net/articles/JsonPath/ func notEqualJSON(data []byte, expression string, expect []byte) error { obj, err := oj.Parse(expect) if err != nil { return fmt.Errorf("could not parse json in NotEqualJSON error: '%s'", err) } return notEqualAbstract(data, expression, obj, "NotEqualJSON") } // Length is a function to asserts that value is the expected length // About expression - https://goessner.net/articles/JsonPath/ func length(data []byte, expression string, expectLength int) error { values, err := GetValueFromJSON(data, expression) if err != nil { return err } for _, value := range values { v := reflect.ValueOf(value) if v.Len() != expectLength { return errors.NewAssertError("Length", fmt.Sprintf("on path %v. expect lenght %v, but actual %v", expression, expectLength, v.Len()), v.Len(), expectLength) } } return nil } // GreaterThan is a function to asserts that value is greater than the given length // About expression - https://goessner.net/articles/JsonPath/ func greaterThan(data []byte, expression string, minimumLength int) error { values, err := GetValueFromJSON(data, expression) if err != nil { return err } for _, value := range values { v := reflect.ValueOf(value) if v.Len() <= minimumLength { return errors.NewAssertError("GreaterThan", fmt.Sprintf("on path %v. %v is greater than %v", expression, v.Len(), minimumLength), v.Len(), minimumLength) } } return nil } // GreaterOrEqualThan is a function to asserts that value is greater or equal than the given length // About expression - https://goessner.net/articles/JsonPath/ func greaterOrEqualThan(data []byte, expression string, minimumLength int) error { values, err := GetValueFromJSON(data, expression) if err != nil { return err } for _, value := range values { v := reflect.ValueOf(value) if v.Len() < minimumLength { return errors.NewAssertError("GreaterOrEqualThan", fmt.Sprintf("on path %v. %v is greater or equal than %v", expression, v.Len(), minimumLength), v.Len(), minimumLength) } } return nil } // LessThan is a function to asserts that value is less than the given length // About expression - https://goessner.net/articles/JsonPath/ func lessThan(data []byte, expression string, maximumLength int) error { values, err := GetValueFromJSON(data, expression) if err != nil { return err } for _, value := range values { v := reflect.ValueOf(value) if v.Len() >= maximumLength { return errors.NewAssertError("LessThan", fmt.Sprintf("on path %v. %v is less than %v", expression, v.Len(), maximumLength), v.Len(), maximumLength) } } return nil } // LessOrEqualThan is a function to asserts that value is less or equal than the given length // About expression - https://goessner.net/articles/JsonPath/ func lessOrEqualThan(data []byte, expression string, maximumLength int) error { values, err := GetValueFromJSON(data, expression) if err != nil { return err } for _, value := range values { v := reflect.ValueOf(value) if v.Len() > maximumLength { return errors.NewAssertError("LessThan", fmt.Sprintf("on path %v. %v is less or equal than %v", expression, v.Len(), maximumLength), v.Len(), maximumLength) } } return nil } // notEmpty is a function to asserts that value is not empty (!= 0, != null) // About expression - https://goessner.net/articles/JsonPath/ func notEmpty(data []byte, expression string) error { values, _ := GetValueFromJSON(data, expression) if len(values) == 0 { return errors.NewAssertError("NotEmpty", fmt.Sprintf("on path %v. value is not present", expression), nil, nil) } for _, value := range values { if isEmpty(value) { return errors.NewAssertError("NotEmpty", fmt.Sprintf("on path %v. value is not present", expression), nil, nil) } } return nil } // Present is a function to asserts that value is present // value can be 0 or null // About expression - https://goessner.net/articles/JsonPath/ func present(data []byte, expression string) error { values, err := GetValueFromJSON(data, expression) if err != nil || len(values) == 0 { return errors.NewAssertError("Present", fmt.Sprintf("on path %v. value not present", expression), nil, nil) } return nil } // NotPresent is a function to asserts that value is not present // About expression - https://goessner.net/articles/JsonPath/ func notPresent(data []byte, expression string) error { values, _ := GetValueFromJSON(data, expression) for _, value := range values { if !isEmpty(value) { return errors.NewAssertError("NotPresent", fmt.Sprintf("on path %v. value present", expression), nil, nil) } } return nil } func objectsAreEqual(expect, actual interface{}) bool { if reflect.DeepEqual(expect, actual) { return true } if expect == nil || actual == nil { return expect == actual } if fmt.Sprintf("%v", expect) == fmt.Sprintf("%v", actual) { return true } exp, ok := expect.([]byte) if !ok { return reflect.DeepEqual(expect, actual) } act, ok := actual.([]byte) if !ok { return false } if exp == nil || act == nil { return exp == nil && act == nil } return bytes.Equal(exp, act) } func isEmpty(object interface{}) bool { if object == nil { return true } objValue := reflect.ValueOf(object) switch objValue.Kind() { case reflect.Ptr: if objValue.IsNil() { return true } deref := objValue.Elem().Interface() return isEmpty(deref) case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: return objValue.Len() == 0 default: zero := reflect.Zero(objValue.Type()) return reflect.DeepEqual(object, zero.Interface()) } } func insideArray(list interface{}, element interface{}) (ok, found bool) { var ( listValue = reflect.ValueOf(list) elementValue = reflect.ValueOf(element) ) defer func() { if err := recover(); err != nil { ok = false found = false } }() if reflect.TypeOf(list).Kind() == reflect.String { return true, strings.Contains(listValue.String(), elementValue.String()) } if reflect.TypeOf(list).Kind() == reflect.Map { mapKeys := listValue.MapKeys() for i := 0; i < len(mapKeys); i++ { if objectsAreEqual(mapKeys[i].Interface(), element) { return true, true } } return true, false } for i := 0; i < listValue.Len(); i++ { if objectsAreEqual(listValue.Index(i).Interface(), element) { return true, true } } return true, false } ================================================ FILE: builder.go ================================================ package cute import ( "net/http" "time" ) const defaultHTTPTimeout = 30 var ( errorAssertIsNil = "assert must be not nil" ) // HTTPTestMaker is a creator tests type HTTPTestMaker struct { httpClient *http.Client middleware *Middleware jsonMarshaler JSONMarshaler } // NewHTTPTestMaker is function for set options for all cute. // For example, you can set timeout for all requests or set custom http client // Options: // - WithCustomHTTPTimeout - set timeout for all requests // - WithHTTPClient - set custom http client // - WithCustomHTTPRoundTripper - set custom http round tripper // - WithJSONMarshaler - set custom json marshaler // - WithMiddlewareAfter - set function which will run AFTER test execution // - WithMiddlewareAfterT - set function which will run AFTER test execution with TB // - WithMiddlewareBefore - set function which will run BEFORE test execution // - WithMiddlewareBeforeT - set function which will run BEFORE test execution with TB func NewHTTPTestMaker(opts ...Option) *HTTPTestMaker { var ( o = &options{ middleware: new(Middleware), } timeout = defaultHTTPTimeout * time.Second roundTripper = http.DefaultTransport jsMarshaler JSONMarshaler = &jsonMarshaler{} ) for _, opt := range opts { opt(o) } if o.httpTimeout != 0 { timeout = o.httpTimeout } if o.httpRoundTripper != nil { //nolint roundTripper = o.httpRoundTripper } httpClient := &http.Client{ Transport: roundTripper, Timeout: timeout, } if o.httpClient != nil { httpClient = o.httpClient } if o.jsonMarshaler != nil { jsMarshaler = o.jsonMarshaler } m := &HTTPTestMaker{ httpClient: httpClient, jsonMarshaler: jsMarshaler, middleware: o.middleware, } return m } // NewTestBuilder is a function for initialization foundation for cute func (m *HTTPTestMaker) NewTestBuilder() AllureBuilder { tests := createDefaultTests(m) return &cute{ baseProps: m, countTests: 0, tests: tests, allureInfo: new(allureInformation), allureLinks: new(allureLinks), allureLabels: new(allureLabels), parallel: false, } } func createDefaultTests(m *HTTPTestMaker) []*Test { tests := make([]*Test, 1) tests[0] = createDefaultTest(m) return tests } func createDefaultTest(m *HTTPTestMaker) *Test { return &Test{ httpClient: m.httpClient, jsonMarshaler: m.jsonMarshaler, Middleware: createMiddlewareFromTemplate(m.middleware), AllureStep: new(AllureStep), Request: &Request{ Retry: new(RequestRetryPolitic), }, Expect: &Expect{JSONSchema: new(ExpectJSONSchema)}, } } func createMiddlewareFromTemplate(m *Middleware) *Middleware { after := make([]AfterExecute, 0, len(m.After)) after = append(after, m.After...) afterT := make([]AfterExecuteT, 0, len(m.AfterT)) afterT = append(afterT, m.AfterT...) before := make([]BeforeExecute, 0, len(m.Before)) before = append(before, m.Before...) beforeT := make([]BeforeExecuteT, 0, len(m.BeforeT)) beforeT = append(beforeT, m.BeforeT...) middleware := &Middleware{ After: after, AfterT: afterT, Before: before, BeforeT: beforeT, } return middleware } func (qt *cute) Create() MiddlewareRequest { return qt } func (qt *cute) CreateStep(name string) MiddlewareRequest { qt.tests[qt.countTests].AllureStep.Name = name return qt } func (qt *cute) CreateRequest() RequestHTTPBuilder { return qt } ================================================ FILE: builder_allure.go ================================================ package cute import ( "fmt" "github.com/ozontech/allure-go/pkg/allure" ) func (qt *cute) Parallel() AllureBuilder { qt.parallel = true return qt } func (qt *cute) Title(title string) AllureBuilder { qt.allureInfo.title = title return qt } func (qt *cute) Epic(epic string) AllureBuilder { qt.allureLabels.epic = epic return qt } func (qt *cute) Titlef(format string, args ...interface{}) AllureBuilder { qt.allureInfo.title = fmt.Sprintf(format, args...) return qt } func (qt *cute) Descriptionf(format string, args ...interface{}) AllureBuilder { qt.allureInfo.description = fmt.Sprintf(format, args...) return qt } func (qt *cute) Stage(stage string) AllureBuilder { qt.allureInfo.stage = stage return qt } func (qt *cute) Stagef(format string, args ...interface{}) AllureBuilder { qt.allureInfo.stage = fmt.Sprintf(format, args...) return qt } func (qt *cute) Layer(value string) AllureBuilder { qt.allureLabels.layer = value return qt } func (qt *cute) TmsLink(tmsLink string) AllureBuilder { qt.allureLinks.tmsLink = tmsLink return qt } func (qt *cute) TmsLinks(tmsLinks ...string) AllureBuilder { qt.allureLinks.tmsLinks = append(qt.allureLinks.tmsLinks, tmsLinks...) return qt } func (qt *cute) SetIssue(issue string) AllureBuilder { qt.allureLinks.issue = issue return qt } func (qt *cute) SetTestCase(testCase string) AllureBuilder { qt.allureLinks.testCase = testCase return qt } func (qt *cute) Link(link *allure.Link) AllureBuilder { qt.allureLinks.link = link return qt } func (qt *cute) ID(value string) AllureBuilder { qt.allureLabels.id = value return qt } func (qt *cute) AllureID(value string) AllureBuilder { qt.allureLabels.allureID = value return qt } func (qt *cute) AddSuiteLabel(value string) AllureBuilder { qt.allureLabels.suiteLabel = value return qt } func (qt *cute) AddSubSuite(value string) AllureBuilder { qt.allureLabels.subSuite = value return qt } func (qt *cute) AddParentSuite(value string) AllureBuilder { qt.allureLabels.parentSuite = value return qt } func (qt *cute) Story(value string) AllureBuilder { qt.allureLabels.story = value return qt } func (qt *cute) Tag(value string) AllureBuilder { qt.allureLabels.tag = value return qt } func (qt *cute) Severity(value allure.SeverityType) AllureBuilder { qt.allureLabels.severity = value return qt } func (qt *cute) Owner(value string) AllureBuilder { qt.allureLabels.owner = value return qt } func (qt *cute) Lead(value string) AllureBuilder { qt.allureLabels.lead = value return qt } func (qt *cute) Label(label *allure.Label) AllureBuilder { qt.allureLabels.label = label return qt } func (qt *cute) Labels(labels ...*allure.Label) AllureBuilder { qt.allureLabels.labels = labels return qt } func (qt *cute) Description(description string) AllureBuilder { qt.allureInfo.description = description return qt } func (qt *cute) Tags(tags ...string) AllureBuilder { qt.allureLabels.tags = tags return qt } func (qt *cute) Feature(feature string) AllureBuilder { qt.allureLabels.feature = feature return qt } ================================================ FILE: builder_asserts.go ================================================ package cute import "time" func (qt *cute) AssertBody(asserts ...AssertBody) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertBody = append(qt.tests[qt.countTests].Expect.AssertBody, assertBodyWithTrace(assert, trace)) } return qt } func (qt *cute) OptionalAssertBody(asserts ...AssertBody) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertBody = append(qt.tests[qt.countTests].Expect.AssertBody, assertBodyWithTrace(optionalAssertBody(assert), trace)) } return qt } func (qt *cute) BrokenAssertBody(asserts ...AssertBody) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertBody = append(qt.tests[qt.countTests].Expect.AssertBody, assertBodyWithTrace(brokenAssertBody(assert), trace)) } return qt } func (qt *cute) RequireBody(asserts ...AssertBody) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertBody = append(qt.tests[qt.countTests].Expect.AssertBody, assertBodyWithTrace(requireAssertBody(assert), trace)) } return qt } func (qt *cute) AssertHeaders(asserts ...AssertHeaders) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertHeaders = append(qt.tests[qt.countTests].Expect.AssertHeaders, assertHeadersWithTrace(assert, trace)) } return qt } func (qt *cute) OptionalAssertHeaders(asserts ...AssertHeaders) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertHeaders = append(qt.tests[qt.countTests].Expect.AssertHeaders, assertHeadersWithTrace(optionalAssertHeaders(assert), trace)) } return qt } func (qt *cute) RequireHeaders(asserts ...AssertHeaders) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertHeaders = append(qt.tests[qt.countTests].Expect.AssertHeaders, assertHeadersWithTrace(requireAssertHeaders(assert), trace)) } return qt } func (qt *cute) BrokenAssertHeaders(asserts ...AssertHeaders) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertHeaders = append(qt.tests[qt.countTests].Expect.AssertHeaders, assertHeadersWithTrace(brokenAssertHeaders(assert), trace)) } return qt } func (qt *cute) AssertResponse(asserts ...AssertResponse) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertResponse = append(qt.tests[qt.countTests].Expect.AssertResponse, assertResponseWithTrace(assert, trace)) } return qt } func (qt *cute) OptionalAssertResponse(asserts ...AssertResponse) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertResponse = append(qt.tests[qt.countTests].Expect.AssertResponse, assertResponseWithTrace(optionalAssertResponse(assert), trace)) } return qt } func (qt *cute) RequireResponse(asserts ...AssertResponse) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertResponse = append(qt.tests[qt.countTests].Expect.AssertResponse, assertResponseWithTrace(requireAssertResponse(assert), trace)) } return qt } func (qt *cute) BrokenAssertResponse(asserts ...AssertResponse) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertResponse = append(qt.tests[qt.countTests].Expect.AssertResponse, assertResponseWithTrace(brokenAssertResponse(assert), trace)) } return qt } func (qt *cute) AssertBodyT(asserts ...AssertBodyT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertBodyT = append(qt.tests[qt.countTests].Expect.AssertBodyT, assertBodyTWithTrace(assert, trace)) } return qt } func (qt *cute) OptionalAssertBodyT(asserts ...AssertBodyT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertBodyT = append(qt.tests[qt.countTests].Expect.AssertBodyT, assertBodyTWithTrace(optionalAssertBodyT(assert), trace)) } return qt } func (qt *cute) BrokenAssertBodyT(asserts ...AssertBodyT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertBodyT = append(qt.tests[qt.countTests].Expect.AssertBodyT, assertBodyTWithTrace(brokenAssertBodyT(assert), trace)) } return qt } func (qt *cute) RequireBodyT(asserts ...AssertBodyT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertBodyT = append(qt.tests[qt.countTests].Expect.AssertBodyT, assertBodyTWithTrace(requireAssertBodyT(assert), trace)) } return qt } func (qt *cute) AssertHeadersT(asserts ...AssertHeadersT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertHeadersT = append(qt.tests[qt.countTests].Expect.AssertHeadersT, assertHeadersTWithTrace(assert, trace)) } return qt } func (qt *cute) OptionalAssertHeadersT(asserts ...AssertHeadersT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertHeadersT = append(qt.tests[qt.countTests].Expect.AssertHeadersT, assertHeadersTWithTrace(optionalAssertHeadersT(assert), trace)) } return qt } func (qt *cute) RequireHeadersT(asserts ...AssertHeadersT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertHeadersT = append(qt.tests[qt.countTests].Expect.AssertHeadersT, assertHeadersTWithTrace(requireAssertHeadersT(assert), trace)) } return qt } func (qt *cute) BrokenAssertHeadersT(asserts ...AssertHeadersT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertHeadersT = append(qt.tests[qt.countTests].Expect.AssertHeadersT, assertHeadersTWithTrace(brokenAssertHeadersT(assert), trace)) } return qt } func (qt *cute) AssertResponseT(asserts ...AssertResponseT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertResponseT = append(qt.tests[qt.countTests].Expect.AssertResponseT, assertResponseTWithTrace(assert, trace)) } return qt } func (qt *cute) OptionalAssertResponseT(asserts ...AssertResponseT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertResponseT = append(qt.tests[qt.countTests].Expect.AssertResponseT, assertResponseTWithTrace(optionalAssertResponseT(assert), trace)) } return qt } func (qt *cute) BrokenAssertResponseT(asserts ...AssertResponseT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertResponseT = append(qt.tests[qt.countTests].Expect.AssertResponseT, assertResponseTWithTrace(brokenAssertResponseT(assert), trace)) } return qt } func (qt *cute) RequireResponseT(asserts ...AssertResponseT) ExpectHTTPBuilder { trace := getTrace() for _, assert := range asserts { if assert == nil { panic(errorAssertIsNil) } qt.tests[qt.countTests].Expect.AssertResponseT = append(qt.tests[qt.countTests].Expect.AssertResponseT, assertResponseTWithTrace(requireAssertResponseT(assert), trace)) } return qt } func (qt *cute) ExpectExecuteTimeout(t time.Duration) ExpectHTTPBuilder { qt.tests[qt.countTests].Expect.ExecuteTime = t return qt } func (qt *cute) ExpectStatus(code int) ExpectHTTPBuilder { qt.tests[qt.countTests].Expect.Code = code return qt } func (qt *cute) ExpectJSONSchemaString(schema string) ExpectHTTPBuilder { qt.tests[qt.countTests].Expect.JSONSchema.String = schema return qt } func (qt *cute) ExpectJSONSchemaByte(schema []byte) ExpectHTTPBuilder { qt.tests[qt.countTests].Expect.JSONSchema.Byte = schema return qt } func (qt *cute) ExpectJSONSchemaFile(filePath string) ExpectHTTPBuilder { qt.tests[qt.countTests].Expect.JSONSchema.File = filePath return qt } ================================================ FILE: builder_middleware.go ================================================ package cute func (qt *cute) StepName(name string) MiddlewareRequest { qt.tests[qt.countTests].AllureStep.Name = name return qt } func (qt *cute) BeforeExecute(fs ...BeforeExecute) MiddlewareRequest { qt.tests[qt.countTests].Middleware.Before = append(qt.tests[qt.countTests].Middleware.Before, fs...) return qt } func (qt *cute) BeforeExecuteT(fs ...BeforeExecuteT) MiddlewareRequest { qt.tests[qt.countTests].Middleware.BeforeT = append(qt.tests[qt.countTests].Middleware.BeforeT, fs...) return qt } func (qt *cute) After(fs ...AfterExecute) ExpectHTTPBuilder { qt.tests[qt.countTests].Middleware.After = append(qt.tests[qt.countTests].Middleware.After, fs...) return qt } func (qt *cute) AfterT(fs ...AfterExecuteT) ExpectHTTPBuilder { qt.tests[qt.countTests].Middleware.AfterT = append(qt.tests[qt.countTests].Middleware.AfterT, fs...) return qt } func (qt *cute) AfterExecute(fs ...AfterExecute) MiddlewareRequest { qt.tests[qt.countTests].Middleware.After = append(qt.tests[qt.countTests].Middleware.After, fs...) return qt } func (qt *cute) AfterExecuteT(fs ...AfterExecuteT) MiddlewareRequest { qt.tests[qt.countTests].Middleware.AfterT = append(qt.tests[qt.countTests].Middleware.AfterT, fs...) return qt } func (qt *cute) AfterTestExecute(fs ...AfterExecute) NextTestBuilder { previousTest := 0 if qt.countTests != 0 { previousTest = qt.countTests - 1 } qt.tests[previousTest].Middleware.After = append(qt.tests[previousTest].Middleware.After, fs...) return qt } func (qt *cute) AfterTestExecuteT(fs ...AfterExecuteT) NextTestBuilder { previousTest := 0 if qt.countTests != 0 { previousTest = qt.countTests - 1 } qt.tests[previousTest].Middleware.AfterT = append(qt.tests[previousTest].Middleware.AfterT, fs...) return qt } ================================================ FILE: builder_option.go ================================================ package cute import ( "net/http" "time" ) type options struct { httpClient *http.Client httpTimeout time.Duration httpRoundTripper http.RoundTripper jsonMarshaler JSONMarshaler middleware *Middleware } // Option ... type Option func(*options) // WithHTTPClient is a function for set custom http client func WithHTTPClient(client *http.Client) Option { return func(o *options) { o.httpClient = client } } // WithJSONMarshaler is a function for set custom json marshaler func WithJSONMarshaler(m JSONMarshaler) Option { return func(o *options) { o.jsonMarshaler = m } } // WithCustomHTTPTimeout is a function for set custom http client timeout func WithCustomHTTPTimeout(t time.Duration) Option { return func(o *options) { o.httpTimeout = t } } // WithCustomHTTPRoundTripper is a function for set custom http round tripper func WithCustomHTTPRoundTripper(r http.RoundTripper) Option { return func(o *options) { o.httpRoundTripper = r } } // WithMiddlewareAfter is function for set function which will run AFTER test execution func WithMiddlewareAfter(after ...AfterExecute) Option { return func(o *options) { o.middleware.After = append(o.middleware.After, after...) } } // WithMiddlewareAfterT is function for set function which will run AFTER test execution func WithMiddlewareAfterT(after ...AfterExecuteT) Option { return func(o *options) { o.middleware.AfterT = append(o.middleware.AfterT, after...) } } // WithMiddlewareBefore is function for set function which will run BEFORE test execution func WithMiddlewareBefore(before ...BeforeExecute) Option { return func(o *options) { o.middleware.Before = append(o.middleware.Before, before...) } } // WithMiddlewareBeforeT is function for set function which will run BEFORE test execution func WithMiddlewareBeforeT(beforeT ...BeforeExecuteT) Option { return func(o *options) { o.middleware.BeforeT = append(o.middleware.BeforeT, beforeT...) } } ================================================ FILE: builder_request.go ================================================ package cute import ( "net/http" "time" ) // RequestRepeat is a function for set options in request // if response.Code != Expect.Code, than request will repeat Count counts with Delay delay. // Default delay is 1 second. // Deprecated: use RequestRetry instead func (qt *cute) RequestRepeat(count int) RequestHTTPBuilder { qt.tests[qt.countTests].Request.Retry.Count = count return qt } // RequestRepeatDelay set delay for request repeat. // if response.Code != Expect.Code, than request will repeat Count counts with Delay delay. // Default delay is 1 second. // Deprecated: use RequestRetryDelay instead func (qt *cute) RequestRepeatDelay(delay time.Duration) RequestHTTPBuilder { qt.tests[qt.countTests].Request.Retry.Delay = delay return qt } // RequestRepeatPolitic set politic for request repeat. // if response.Code != Expect.Code, than request will repeat Count counts with Delay delay. // if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute. // If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will not execute. // Deprecated: use RequestRetryPolitic instead func (qt *cute) RequestRepeatPolitic(politic *RequestRepeatPolitic) RequestHTTPBuilder { if politic == nil { panic("politic is nil in RequestRetryPolitic") } qt.tests[qt.countTests].Request.Retry = &RequestRetryPolitic{ Count: politic.Count, Delay: politic.Delay, Optional: politic.Optional, Broken: politic.Broken, } return qt } // RequestRepeatOptional set option politic for request repeat. // if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute. // Deprecated: use RequestRetryOptional instead func (qt *cute) RequestRepeatOptional(option bool) RequestHTTPBuilder { qt.tests[qt.countTests].Request.Retry.Optional = option return qt } // RequestRepeatBroken set broken politic for request repeat. // If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will not execute. // Deprecated: use RequestRetryBroken instead func (qt *cute) RequestRepeatBroken(broken bool) RequestHTTPBuilder { qt.tests[qt.countTests].Request.Retry.Broken = broken return qt } // RequestRetry is a function for set options in request // if response.Code != Expect.Code, than request will repeat Count counts with Delay delay. // Default delay is 1 second. func (qt *cute) RequestRetry(count int) RequestHTTPBuilder { qt.tests[qt.countTests].Request.Retry.Count = count return qt } // RequestRetryDelay set delay for request repeat. // if response.Code != Expect.Code, than request will repeat Count counts with Delay delay. // Default delay is 1 second. func (qt *cute) RequestRetryDelay(delay time.Duration) RequestHTTPBuilder { qt.tests[qt.countTests].Request.Retry.Delay = delay return qt } // RequestRetryPolitic set politic for request repeat. // if response.Code != Expect.Code, than request will repeat Count counts with Delay delay. // if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute. // If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will not execute. func (qt *cute) RequestRetryPolitic(politic *RequestRetryPolitic) RequestHTTPBuilder { if politic == nil { panic("politic is nil in RequestRetryPolitic") } qt.tests[qt.countTests].Request.Retry = politic return qt } // RequestRetryOptional set option politic for request repeat. // if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute. func (qt *cute) RequestRetryOptional(option bool) RequestHTTPBuilder { qt.tests[qt.countTests].Request.Retry.Optional = option return qt } // RequestRetryBroken set broken politic for request repeat. // If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will not execute. func (qt *cute) RequestRetryBroken(broken bool) RequestHTTPBuilder { qt.tests[qt.countTests].Request.Retry.Broken = broken return qt } // RequestSanitizerHook assigns the provided RequestSanitizerHook to the test, // allowing URL sanitization before logging or reporting. func (qt *cute) RequestSanitizerHook(hook RequestSanitizerHook) RequestHTTPBuilder { qt.tests[qt.countTests].RequestSanitizer = hook return qt } // ResponseSanitizerHook assigns the provided ResponseSanitizerHook to the test, // allowing URL sanitization before logging or reporting. func (qt *cute) ResponseSanitizerHook(hook ResponseSanitizerHook) RequestHTTPBuilder { qt.tests[qt.countTests].ResponseSanitizer = hook return qt } func (qt *cute) Request(r *http.Request) ExpectHTTPBuilder { qt.tests[qt.countTests].Request.Base = r return qt } func (qt *cute) RequestBuilder(r ...RequestBuilder) ExpectHTTPBuilder { qt.tests[qt.countTests].Request.Builders = append(qt.tests[qt.countTests].Request.Builders, r...) return qt } ================================================ FILE: builder_retry.go ================================================ package cute import "time" // Retry is a function for configure test repeat // if response.Code != Expect.Code or any of asserts are failed/broken than test will repeat counts with delay. // Default delay is 1 second. func (qt *cute) Retry(count int) MiddlewareRequest { if count < 1 { panic("count must be greater than 0") } qt.tests[qt.countTests].Retry.MaxAttempts = count return qt } // RetryDelay set delay for test repeat. // if response.Code != Expect.Code or any of asserts are failed/broken than test will repeat counts with delay. // Default delay is 1 second. func (qt *cute) RetryDelay(delay time.Duration) MiddlewareRequest { if delay < 0 { panic("delay must be greater than or equal to 0") } qt.tests[qt.countTests].Retry.Delay = delay return qt } ================================================ FILE: builder_table.go ================================================ package cute import "net/http" func (qt *cute) CreateTableTest() MiddlewareTable { qt.isTableTest = true return qt } func (qt *cute) PutNewTest(name string, r *http.Request, expect *Expect) TableTest { // Validate, that first step is empty if qt.countTests == 0 { if qt.tests[0].Request.Base == nil && len(qt.tests[0].Request.Builders) == 0 { qt.tests[0].Expect = expect qt.tests[0].Name = name qt.tests[0].Request.Base = r return qt } } newTest := createDefaultTest(qt.baseProps) newTest.Expect = expect newTest.Name = name newTest.Request.Base = r qt.tests = append(qt.tests, newTest) qt.countTests++ // async? return qt } func (qt *cute) PutTests(tests ...*Test) TableTest { for _, test := range tests { // Fill common fields qt.fillBaseProps(test) // Validate, that first step is empty if qt.countTests == 0 { if qt.tests[0].Request.Base == nil && len(qt.tests[0].Request.Builders) == 0 { qt.tests[0] = test continue } } qt.tests = append(qt.tests, test) qt.countTests++ } return qt } func (qt *cute) fillBaseProps(t *Test) { if qt.baseProps == nil { return } if qt.baseProps.httpClient != nil { t.httpClient = qt.baseProps.httpClient } if qt.baseProps.jsonMarshaler != nil { t.jsonMarshaler = qt.baseProps.jsonMarshaler } if t.Middleware == nil { t.Middleware = createMiddlewareFromTemplate(qt.baseProps.middleware) } else { t.Middleware.After = append(t.Middleware.After, qt.baseProps.middleware.After...) t.Middleware.AfterT = append(t.Middleware.AfterT, qt.baseProps.middleware.AfterT...) t.Middleware.Before = append(t.Middleware.Before, qt.baseProps.middleware.Before...) t.Middleware.BeforeT = append(t.Middleware.BeforeT, qt.baseProps.middleware.BeforeT...) } } func (qt *cute) NextTest() NextTestBuilder { qt.countTests++ // async? qt.tests = append(qt.tests, createDefaultTest(qt.baseProps)) return qt } ================================================ FILE: builder_table_test.go ================================================ package cute import ( "net/http" "testing" "github.com/stretchr/testify/require" ) func TestFillBaseProps_WhenBasePropsIsNil(t *testing.T) { testObj := &Test{} cuteObj := &cute{} cuteObj.fillBaseProps(testObj) require.Nil(t, testObj.httpClient) require.Nil(t, testObj.jsonMarshaler) require.Nil(t, testObj.Middleware) } func TestFillBaseProps_WhenBasePropsIsNotNil(t *testing.T) { testObj := &Test{} cuteObj := &cute{} qtBaseProps := &HTTPTestMaker{ httpClient: &http.Client{}, jsonMarshaler: &jsonMarshaler{}, middleware: &Middleware{ After: []AfterExecute{ func(*http.Response, []error) error { return nil }, func(*http.Response, []error) error { return nil }, }, AfterT: []AfterExecuteT{func(T, *http.Response, []error) error { return nil }}, Before: []BeforeExecute{ func(*http.Request) error { return nil }, func(*http.Request) error { return nil }, }, BeforeT: []BeforeExecuteT{ func(T, *http.Request) error { return nil }, func(T, *http.Request) error { return nil }, }, }, } cuteObj.baseProps = qtBaseProps cuteObj.fillBaseProps(testObj) require.Equal(t, qtBaseProps.httpClient, testObj.httpClient) require.Equal(t, qtBaseProps.jsonMarshaler, testObj.jsonMarshaler) require.Len(t, testObj.Middleware.After, len(qtBaseProps.middleware.After)) require.Len(t, testObj.Middleware.AfterT, len(qtBaseProps.middleware.AfterT)) require.Len(t, testObj.Middleware.Before, len(qtBaseProps.middleware.Before)) require.Len(t, testObj.Middleware.BeforeT, len(qtBaseProps.middleware.BeforeT)) } func TestFillBaseProps_WhenBasePropsIsNotNil_After(t *testing.T) { testObj := &Test{ Middleware: &Middleware{ After: []AfterExecute{ func(*http.Response, []error) error { return nil }, }, }, } cuteObj := &cute{} qtBaseProps := &HTTPTestMaker{ httpClient: &http.Client{}, jsonMarshaler: &jsonMarshaler{}, middleware: &Middleware{ After: []AfterExecute{ func(*http.Response, []error) error { return nil }, func(*http.Response, []error) error { return nil }, }, BeforeT: []BeforeExecuteT{ func(T, *http.Request) error { return nil }, func(T, *http.Request) error { return nil }, }, }, } cuteObj.baseProps = qtBaseProps cuteObj.fillBaseProps(testObj) require.Equal(t, qtBaseProps.httpClient, testObj.httpClient) require.Equal(t, qtBaseProps.jsonMarshaler, testObj.jsonMarshaler) require.Len(t, testObj.Middleware.After, len(qtBaseProps.middleware.After)+1) require.Len(t, testObj.Middleware.AfterT, len(qtBaseProps.middleware.AfterT)) require.Len(t, testObj.Middleware.Before, len(qtBaseProps.middleware.Before)) require.Len(t, testObj.Middleware.BeforeT, len(qtBaseProps.middleware.BeforeT)) } func TestFillBaseProps_WhenBasePropsIsNotNil_Middleware(t *testing.T) { testObj := &Test{ Middleware: &Middleware{ After: []AfterExecute{ func(*http.Response, []error) error { return nil }, func(*http.Response, []error) error { return nil }, }, AfterT: []AfterExecuteT{ func(T, *http.Response, []error) error { return nil }, func(T, *http.Response, []error) error { return nil }, func(T, *http.Response, []error) error { return nil }, }, Before: []BeforeExecute{ func(*http.Request) error { return nil }, func(*http.Request) error { return nil }, func(*http.Request) error { return nil }, func(*http.Request) error { return nil }, }, BeforeT: []BeforeExecuteT{ func(T, *http.Request) error { return nil }, }, }, } cuteObj := &cute{} qtBaseProps := &HTTPTestMaker{ httpClient: &http.Client{}, jsonMarshaler: &jsonMarshaler{}, middleware: &Middleware{ After: []AfterExecute{ func(*http.Response, []error) error { return nil }, func(*http.Response, []error) error { return nil }, }, AfterT: []AfterExecuteT{func(T, *http.Response, []error) error { return nil }}, Before: []BeforeExecute{ func(*http.Request) error { return nil }, func(*http.Request) error { return nil }, }, BeforeT: []BeforeExecuteT{ func(T, *http.Request) error { return nil }, func(T, *http.Request) error { return nil }, }, }, } cuteObj.baseProps = qtBaseProps cuteObj.fillBaseProps(testObj) require.Equal(t, qtBaseProps.httpClient, testObj.httpClient) require.Equal(t, qtBaseProps.jsonMarshaler, testObj.jsonMarshaler) require.Len(t, testObj.Middleware.After, len(qtBaseProps.middleware.After)+2) require.Len(t, testObj.Middleware.AfterT, len(qtBaseProps.middleware.AfterT)+3) require.Len(t, testObj.Middleware.Before, len(qtBaseProps.middleware.Before)+4) require.Len(t, testObj.Middleware.BeforeT, len(qtBaseProps.middleware.BeforeT)+1) } ================================================ FILE: builder_test.go ================================================ package cute import ( "net/http" "testing" "time" "github.com/ozontech/allure-go/pkg/allure" "github.com/stretchr/testify/require" ) func TestBuilderAfterTest(t *testing.T) { var ( maker = NewHTTPTestMaker() ) ht := maker.NewTestBuilder(). Create(). RequestBuilder(). NextTest(). AfterTestExecute( func(response *http.Response, errors []error) error { return nil }, func(response *http.Response, errors []error) error { return nil }). AfterTestExecuteT( func(t T, response *http.Response, errors []error) error { return nil }, func(t T, response *http.Response, errors []error) error { return nil }, func(t T, response *http.Response, errors []error) error { return nil }, ) res := ht.(*cute) require.Len(t, res.tests[0].Middleware.After, 2) require.Len(t, res.tests[0].Middleware.AfterT, 3) } func TestBuilderAfterTestTwoStep(t *testing.T) { var ( maker = NewHTTPTestMaker( WithMiddlewareBefore( func(request *http.Request) error { return nil }, func(request *http.Request) error { return nil }, ), WithMiddlewareBeforeT( func(t T, request *http.Request) error { return nil }, ), WithMiddlewareAfter( func(response *http.Response, errors []error) error { return nil }, ), WithMiddlewareAfterT( func(t T, response *http.Response, errors []error) error { return nil }, func(t T, response *http.Response, errors []error) error { return nil }, func(t T, response *http.Response, errors []error) error { return nil }, ), ) ) ht := maker.NewTestBuilder(). Create(). RequestBuilder(). NextTest(). AfterTestExecute( func(response *http.Response, errors []error) error { return nil }, func(response *http.Response, errors []error) error { return nil }). AfterTestExecuteT( func(t T, response *http.Response, errors []error) error { return nil }, func(t T, response *http.Response, errors []error) error { return nil }, func(t T, response *http.Response, errors []error) error { return nil }, ). Create(). AfterExecute( func(response *http.Response, errors []error) error { return nil }, ). AfterExecuteT( func(t T, response *http.Response, errors []error) error { return nil }). RequestBuilder(). NextTest(). AfterTestExecute( func(response *http.Response, errors []error) error { return nil }, ) res := ht.(*cute) require.Len(t, res.tests[0].Middleware.After, 2+1) require.Len(t, res.tests[0].Middleware.Before, 2) require.Len(t, res.tests[0].Middleware.BeforeT, 1) require.Len(t, res.tests[0].Middleware.AfterT, 3+3) require.Len(t, res.tests[1].Middleware.After, 2+1) require.Len(t, res.tests[1].Middleware.AfterT, 1+3) require.Len(t, res.tests[1].Middleware.Before, 2) require.Len(t, res.tests[1].Middleware.BeforeT, 1) } func TestNewTestBuilder(t *testing.T) { var ( maker = NewHTTPTestMaker() ht = maker.NewTestBuilder().(*cute) ) require.NotNil(t, ht.tests) require.Len(t, ht.tests, 1) require.NotNil(t, ht.tests[0].Request) require.NotNil(t, ht.tests[0].Middleware) require.NotNil(t, ht.tests[0].AllureStep) require.NotNil(t, ht.allureInfo) require.NotNil(t, ht.baseProps.httpClient) } func TestHTTPTestMaker(t *testing.T) { var ( maker = NewHTTPTestMaker() ht = maker.NewTestBuilder() title = "title" epic = "epic" desc = "desc" feature = "feature" tags = []string{"tag_1", "tag_2"} stepName = "stepname" req, _ = http.NewRequest(http.MethodGet, "https://site.go", nil) executeTime = time.Duration(10) status = 400 schemaStg = "some_json_schema" schemaBt = []byte("some_json_schema") schemaFile = "file_path" id = "ID" addSuiteLabel = "AddSuiteLabel" addSubSuite = "AddSubSuite" addParentSuite = "AddParentSuite" story = "Story" tag = "Tag" allureID = "AllureID" owner = "Owner" lead = "Lead" label = &allure.Label{Name: "kek", Value: "lol"} setIssue = "SetIssue" setTestCase = "SetTestCase" repeatCount = 10 repeatDelay = time.Duration(10) link = &allure.Link{ Name: "link", Type: "type", URL: "http://go.go", } labels = []*allure.Label{ { Name: "label_1", Value: "value_1", }, { Name: "label_2", Value: "value_2", }, } assertHeaders = []AssertHeaders{ func(headers http.Header) error { return nil }, } assertHeadersT = []AssertHeadersT{ func(t T, headers http.Header) error { return nil }, func(t T, headers http.Header) error { return nil }, } assertBody = []AssertBody{ func(body []byte) error { return nil }, } assertBodyT = []AssertBodyT{ func(t T, body []byte) error { return nil }, func(t T, body []byte) error { return nil }, } assertResponse = []AssertResponse{ func(resp *http.Response) error { return nil }, } assertResponseT = []AssertResponseT{ func(t T, resp *http.Response) error { return nil }, func(t T, resp *http.Response) error { return nil }, } after = []AfterExecute{ func(response *http.Response, errors []error) error { return nil }, func(response *http.Response, errors []error) error { return nil }, } afterT = []AfterExecuteT{ func(t T, response *http.Response, errors []error) error { return nil }, func(t T, response *http.Response, errors []error) error { return nil }, } ) ht. Title(title). Tags(tags...). Epic(epic). Feature(feature). ID(id). AddSuiteLabel(addSuiteLabel). AddSubSuite(addSubSuite). AddParentSuite(addParentSuite). Story(story). Tag(tag). Severity(allure.CRITICAL). AllureID(allureID). Owner(owner). Lead(lead). Label(label). Labels(labels...). SetIssue(setIssue). SetTestCase(setTestCase). Link(link). Description(desc). CreateStep(stepName). RequestRetry(repeatCount). RequestRetryDelay(repeatDelay). Request(req). ExpectExecuteTimeout(executeTime). ExpectStatus(status). ExpectJSONSchemaByte(schemaBt). ExpectJSONSchemaString(schemaStg). ExpectJSONSchemaFile(schemaFile). AssertHeaders(assertHeaders...). AssertHeadersT(assertHeadersT...). AssertBody(assertBody...). AssertBodyT(assertBodyT...). AssertResponse(assertResponse...). AssertResponseT(assertResponseT...). After(after...). AfterT(afterT...) resHt := ht.(*cute) resTest := resHt.tests[0] require.Equal(t, title, resHt.allureInfo.title) require.Equal(t, tags, resHt.allureLabels.tags) require.Equal(t, desc, resHt.allureInfo.description) require.Equal(t, feature, resHt.allureLabels.feature) require.Equal(t, epic, resHt.allureLabels.epic) require.Equal(t, stepName, resTest.AllureStep.Name) require.Equal(t, req, resTest.Request.Base) require.Equal(t, executeTime, resTest.Expect.ExecuteTime) require.Equal(t, status, resTest.Expect.Code) require.Equal(t, schemaBt, resTest.Expect.JSONSchema.Byte) require.Equal(t, schemaStg, resTest.Expect.JSONSchema.String) require.Equal(t, schemaFile, resTest.Expect.JSONSchema.File) require.Equal(t, id, resHt.allureLabels.id) require.Equal(t, addSuiteLabel, resHt.allureLabels.suiteLabel) require.Equal(t, addSubSuite, resHt.allureLabels.subSuite) require.Equal(t, addParentSuite, resHt.allureLabels.parentSuite) require.Equal(t, story, resHt.allureLabels.story) require.Equal(t, tag, resHt.allureLabels.tag) require.Equal(t, owner, resHt.allureLabels.owner) require.Equal(t, lead, resHt.allureLabels.lead) require.Equal(t, label, resHt.allureLabels.label) require.Equal(t, allureID, resHt.allureLabels.allureID) require.Equal(t, setIssue, resHt.allureLinks.issue) require.Equal(t, setTestCase, resHt.allureLinks.testCase) require.Equal(t, link, resHt.allureLinks.link) require.Equal(t, repeatCount, resTest.Request.Retry.Count) require.Equal(t, repeatDelay, resTest.Request.Retry.Delay) require.Equal(t, len(assertHeaders), len(resTest.Expect.AssertHeaders)) require.Equal(t, len(assertHeadersT), len(resTest.Expect.AssertHeadersT)) require.Equal(t, len(assertBody), len(resTest.Expect.AssertBody)) require.Equal(t, len(assertBodyT), len(resTest.Expect.AssertBodyT)) require.Equal(t, len(assertResponse), len(resTest.Expect.AssertResponse)) require.Equal(t, len(assertResponseT), len(resTest.Expect.AssertResponseT)) require.Equal(t, len(after), len(resTest.Middleware.After)) require.Equal(t, len(afterT), len(resTest.Middleware.AfterT)) } func TestCreateDefaultTest(t *testing.T) { resTest := createDefaultTest(&HTTPTestMaker{httpClient: http.DefaultClient, middleware: new(Middleware)}) require.Equal(t, &Test{ httpClient: http.DefaultClient, Name: "", AllureStep: new(AllureStep), Middleware: &Middleware{ After: make([]AfterExecute, 0), AfterT: make([]AfterExecuteT, 0), Before: make([]BeforeExecute, 0), BeforeT: make([]BeforeExecuteT, 0), }, Request: &Request{ Retry: new(RequestRetryPolitic), }, Expect: &Expect{ JSONSchema: new(ExpectJSONSchema), }, }, resTest) } func TestCreateTableTest(t *testing.T) { c := &cute{} c.CreateTableTest() require.True(t, c.isTableTest) } func TestPutNewTest(t *testing.T) { tests := make([]*Test, 1) tests[0] = createDefaultTest(&HTTPTestMaker{httpClient: http.DefaultClient, middleware: new(Middleware)}) var ( c = &cute{tests: tests, baseProps: &HTTPTestMaker{ middleware: &Middleware{}, }} reqOne, _ = http.NewRequest("GET", "URL_1", nil) expectOne = &Expect{Code: 200} reqSecond, _ = http.NewRequest("POST", "URL_1", nil) expectSecond = &Expect{Code: 400} ) c.PutNewTest("name_1", reqOne, expectOne) c.PutNewTest("name_2", reqSecond, expectSecond) require.Equal(t, c.tests[0].Name, "name_1") require.Equal(t, c.tests[0].Expect, expectOne) require.Equal(t, c.tests[0].Request.Base, reqOne) require.Equal(t, c.tests[1].Name, "name_2") require.Equal(t, c.tests[1].Expect, expectSecond) require.Equal(t, c.tests[1].Request.Base, reqSecond) } func TestPutTests(t *testing.T) { var ( tests = createDefaultTests(&HTTPTestMaker{httpClient: http.DefaultClient, middleware: new(Middleware)}) c = &cute{tests: tests} reqOne, _ = http.NewRequest("GET", "URL_1", nil) expectOne = &Expect{Code: 200} reqSecond, _ = http.NewRequest("POST", "URL_1", nil) expectSecond = &Expect{Code: 400} ) tests = append(tests, &Test{ Name: "name_1", Request: &Request{ Base: reqOne, }, Expect: expectOne, }, &Test{ Name: "name_2", Request: &Request{ Base: reqSecond, }, Expect: expectSecond, }, ) c.PutTests(tests...) require.Equal(t, c.tests[0].Name, "name_1") require.Equal(t, c.tests[0].Expect, expectOne) require.Equal(t, c.tests[0].Request.Base, reqOne) require.Equal(t, c.tests[1].Name, "name_2") require.Equal(t, c.tests[1].Expect, expectSecond) require.Equal(t, c.tests[1].Request.Base, reqSecond) } func TestCreateHTTPTestMakerWithHttpClient(t *testing.T) { cli := &http.Client{ Transport: nil, CheckRedirect: nil, Jar: nil, Timeout: 100, } maker := NewHTTPTestMaker(WithHTTPClient(cli)) require.Equal(t, cli, maker.httpClient) require.Equal(t, time.Duration(100), maker.httpClient.Timeout) } type rt struct { } func (r *rt) RoundTrip(*http.Request) (*http.Response, error) { return nil, nil } func TestCreateHTTPMakerOps(t *testing.T) { timeout := time.Second * 100 roundTripper := &rt{} maker := NewHTTPTestMaker( WithCustomHTTPTimeout(timeout), WithCustomHTTPRoundTripper(roundTripper), ) require.Equal(t, timeout, maker.httpClient.Timeout) require.Equal(t, roundTripper, maker.httpClient.Transport) } ================================================ FILE: cute.go ================================================ package cute import ( "context" "strings" "testing" "github.com/ozontech/allure-go/pkg/allure" "github.com/ozontech/allure-go/pkg/framework/core/allure_manager/manager" "github.com/ozontech/allure-go/pkg/framework/core/common" "github.com/ozontech/allure-go/pkg/framework/provider" ) type cute struct { baseProps *HTTPTestMaker parallel bool allureInfo *allureInformation allureLinks *allureLinks allureLabels *allureLabels countTests int // Общее количество тестов. isTableTest bool tests []*Test } type allureInformation struct { title string description string stage string } type allureLabels struct { id string feature string epic string tag string tags []string suiteLabel string subSuite string parentSuite string story string severity allure.SeverityType owner string lead string label *allure.Label labels []*allure.Label allureID string layer string } type allureLinks struct { issue string testCase string link *allure.Link tmsLink string tmsLinks []string } func (qt *cute) ExecuteTest(ctx context.Context, t tProvider) []ResultsHTTPBuilder { var internalT allureProvider if t == nil { panic("could not start test without testing.T") } stepCtx, isStepCtx := t.(provider.StepCtx) if isStepCtx { return qt.executeTestsInsideStep(ctx, stepCtx) } tOriginal, ok := t.(*testing.T) if ok { newT := createAllureT(tOriginal) if !qt.isTableTest { defer newT.FinishTest() //nolint } internalT = newT } allureT, ok := t.(provider.T) if ok { internalT = allureT } if qt.parallel { internalT.Parallel() } return qt.executeTests(ctx, internalT) } func createAllureT(t *testing.T) *common.Common { var ( newT = common.NewT(t) callers = strings.Split(t.Name(), "/") providerCfg = manager.NewProviderConfig(). WithFullName(t.Name()). WithPackageName("package"). WithSuiteName(t.Name()). WithRunner(callers[0]) newProvider = manager.NewProvider(providerCfg) ) newProvider.NewTest(t.Name(), "package") newT.SetProvider(newProvider) newT.Provider.TestContext() return newT } // executeTests is method for run tests // It's could be table tests or usual tests func (qt *cute) executeTests(ctx context.Context, allureProvider allureProvider) []ResultsHTTPBuilder { var ( res = make([]ResultsHTTPBuilder, 0) ) // Cycle for change number of Test for i := 0; i <= qt.countTests; i++ { currentTest := qt.tests[i] // Execute by new T for table tests if qt.isTableTest { tableTestName := currentTest.Name allureProvider.Run(tableTestName, func(inT provider.T) { // Set current test name inT.Title(tableTestName) res = append(res, qt.executeInsideAllure(ctx, inT, currentTest)) }) } else { currentTest.Name = allureProvider.Name() // set labels qt.setAllureInformation(allureProvider) res = append(res, qt.executeInsideAllure(ctx, allureProvider, currentTest)) } } return res } // executeInsideAllure is method for run test inside allure // It's could be table tests or usual tests func (qt *cute) executeInsideAllure(ctx context.Context, allureProvider allureProvider, currentTest *Test) ResultsHTTPBuilder { resT := currentTest.executeInsideAllure(ctx, allureProvider) // Remove from base struct all asserts currentTest.clearFields() return resT } // executeTestsInsideStep is method for run group of tests inside provider.StepCtx func (qt *cute) executeTestsInsideStep(ctx context.Context, stepCtx provider.StepCtx) []ResultsHTTPBuilder { var ( res = make([]ResultsHTTPBuilder, 0) ) // Cycle for change number of Test for i := 0; i <= qt.countTests; i++ { currentTest := qt.tests[i] result := currentTest.executeInsideStep(ctx, stepCtx) // Remove from base struct all asserts currentTest.clearFields() res = append(res, result) } return res } ================================================ FILE: errors/broken.go ================================================ package errors // BrokenError is an interface for set errors like Broken errors. // If the function returns an error, which implements this interface, the allure step will has a broken status type BrokenError interface { IsBroken() bool SetBroken(bool) Error() string } // NewBrokenError returns error with a Broken tag for Allure func NewBrokenError(err string) error { return &CuteError{ Broken: true, Message: err, } } // WrapBrokenError returns error with a Broken tag for Allure func WrapBrokenError(err error) error { return &CuteError{ Broken: true, Err: err, } } ================================================ FILE: errors/error.go ================================================ package errors import "fmt" const ( // ActualField is a key for actual value in error fields ActualField = "Actual" // ExpectedField is a key for expected value in error fields ExpectedField = "Expected" ) // AssertError is a common interface for all errors in the package type AssertError interface { error WithNameError WithFields WithAttachments WithTrace } // WithNameError is interface for creates allure step. // If function returns error, which implement this interface, allure step will create automatically type WithNameError interface { GetName() string SetName(string) } // WithFields is interface for put parameters in allure step. // If function returns error, which implement this interface, parameters will add to allure step type WithFields interface { GetFields() map[string]interface{} PutFields(map[string]interface{}) } // WithTrace is interface for put trace in logs type WithTrace interface { GetTrace() string SetTrace(string) } // Attachment represents an attachment to Allure with properties like name, MIME type, and content. type Attachment struct { Name string // Name of the attachment. MimeType string // MIME type of the attachment. Content []byte // Content of the attachment. } // WithAttachments is an interface that defines methods for managing attachments. type WithAttachments interface { GetAttachments() []*Attachment PutAttachment(a *Attachment) } // CuteError is a struct for error with additional fields for allure and logs type CuteError struct { // Optional is a flag to determine if the error is optional // If the error is optional, it will not fail the test Optional bool // Require is a flag to determine if the error is required // If the error is required, it will fail the test Require bool // Broken is a flag to determine if the error is broken // If the error is broken, it will fail the test and mark the test as broken in allure Broken bool // Name is a name of the error Name string // Message is a message of the error Message string // Err is a wrapped error Err error // Trace is a trace of the error // It could be a file path, function name, or any other information Trace string // Fields is a map of additional fields for the error // It could be actual and expected values, parameters, or any other information // ActualField and ExpectedField fields will be logged Fields map[string]interface{} // Attachments is a slice of attachments for the error Attachments []*Attachment } // NewCuteError is the function, which creates cute error with "Name" and "Message" for allure func NewCuteError(name string, err error) *CuteError { return &CuteError{ Name: name, Err: err, } } // NewAssertError is the function, which creates error with "Actual" and "Expected" for allure func NewAssertError(name string, message string, actual interface{}, expected interface{}) error { return &CuteError{ Name: name, Message: message, Fields: map[string]interface{}{ ActualField: actual, ExpectedField: expected, }, } } // NewAssertErrorWithMessage is the function, which creates error with "Name" and "Message" for allure // Deprecated: use NewEmptyAssertError instead func NewAssertErrorWithMessage(name string, message string) error { return NewEmptyAssertError(name, message) } // NewEmptyAssertError is the function, which creates error with "Name" and "Message" for allure // Returns AssertError with empty fields // You can use PutFields and PutAttachment to add additional information // You can use SetOptional, SetRequire, SetBroken to change error behavior func NewEmptyAssertError(name string, message string) AssertError { return &CuteError{ Name: name, Message: message, Fields: map[string]interface{}{}, } } // Unwrap is a method to get wrapped error // It is used for errors.Is and errors.As functions func (a *CuteError) Unwrap() error { return a.Err } // Error is a method to get error message // It is used for fmt.Errorf and fmt.Println functions func (a *CuteError) Error() string { if a.Trace == "" { return a.Message } errText := a.Message if a.Err != nil { errText = a.Err.Error() } return fmt.Sprintf("%s\nCalled from: %s", errText, a.Trace) } // GetName is a method to get error name // It is used for allure step name func (a *CuteError) GetName() string { return a.Name } // SetName is a method to set error name // It is used for allure step name func (a *CuteError) SetName(name string) { a.Name = name } // GetFields ... func (a *CuteError) GetFields() map[string]interface{} { return a.Fields } // PutFields ... func (a *CuteError) PutFields(fields map[string]interface{}) { for k, v := range fields { a.Fields[k] = v } } // GetAttachments ... func (a *CuteError) GetAttachments() []*Attachment { return a.Attachments } // PutAttachment ... func (a *CuteError) PutAttachment(attachment *Attachment) { a.Attachments = append(a.Attachments, attachment) } // IsOptional ... func (a *CuteError) IsOptional() bool { return a.Optional } // SetOptional ... func (a *CuteError) SetOptional(opt bool) { a.Optional = opt } // IsRequire ... func (a *CuteError) IsRequire() bool { return a.Require } // SetRequire ... func (a *CuteError) SetRequire(b bool) { a.Require = b } // IsBroken ... func (a *CuteError) IsBroken() bool { return a.Broken } // SetBroken ... func (a *CuteError) SetBroken(b bool) { a.Broken = b } // GetTrace ... func (a *CuteError) GetTrace() string { return a.Trace } // SetTrace ... func (a *CuteError) SetTrace(trace string) { a.Trace = trace } ================================================ FILE: errors/optional.go ================================================ package errors // OptionalError is an interface for set errors like Optional errors. // If the function returns an error, which implements this interface, the allure step will has to skip status type OptionalError interface { IsOptional() bool SetOptional(bool) } // NewOptionalError returns error with an Optional tag for Allure func NewOptionalError(err string) error { return &CuteError{ Optional: true, Message: err, } } // WrapOptionalError returns error with an Optional tag for Allure func WrapOptionalError(err error) error { return &CuteError{ Optional: true, Err: err, } } ================================================ FILE: errors/require.go ================================================ package errors // RequireError is an interface for set errors like require error. // If the function returns an error, which implements this interface, the allure step will has failed status type RequireError interface { IsRequire() bool SetRequire(bool) } type requireError struct { err error require bool } // NewRequireError returns error with flag for execute t.FailNow() and finish test after this error func NewRequireError(err string) error { return &CuteError{ Require: true, Message: err, } } // WrapRequireError returns error with flag for execute t.FailNow() and finish test after this error func WrapRequireError(err error) error { return &CuteError{ Require: true, Err: err, } } ================================================ FILE: errors/trace.go ================================================ package errors // NewErrorWithTrace is a function for create error with trace func NewErrorWithTrace(err, trace string) error { return &CuteError{ Trace: trace, Message: err, } } // WrapErrorWithTrace is a function for wrap error with trace func WrapErrorWithTrace(err error, trace string) error { return &CuteError{ Trace: trace, Err: err, } } ================================================ FILE: examples/custom_asserts.go ================================================ package examples import ( "errors" "net/http" "github.com/ozontech/allure-go/pkg/allure" "github.com/ozontech/cute" cuteErrors "github.com/ozontech/cute/errors" "github.com/stretchr/testify/require" ) func CustomAssertBodyWithCustomError() cute.AssertBody { return func(bytes []byte) error { if len(bytes) == 0 { return cuteErrors.NewAssertError("customAssertBodyWithCustomError", "body must be not empty", "len is 0", "len more 0") } return nil } } func CustomAssertBody() cute.AssertBody { return func(bytes []byte) error { if len(bytes) == 0 { return errors.New("response body is empty") } return nil } } func CustomAssertBodyT() cute.AssertBodyT { return func(t cute.T, bytes []byte) error { t.WithNewParameters("example_parameter", "example") require.GreaterOrEqual(t, len(bytes), 100) return nil } } func CustomAssertBodyWithAllureStep() cute.AssertBodyT { return func(t cute.T, bytes []byte) error { step := allure.NewSimpleStep("Custom assert step") defer func() { t.Step(step) }() if len(bytes) == 0 { step.Status = allure.Failed step.WithAttachments(allure.NewAttachment("Error", allure.Text, []byte("response body is empty"))) return nil } return nil } } func CustomAssertHeaders() cute.AssertHeaders { return func(headers http.Header) error { if len(headers) == 0 { return errors.New("response without headers") } return nil } } func CustomAssertResponse() cute.AssertResponse { return func(resp *http.Response) error { if resp.ContentLength == 0 { return errors.New("content length is zero") } return nil } } ================================================ FILE: examples/inside_step_test.go ================================================ //go:build example // +build example package examples import ( "context" "net/http" "net/url" "testing" "time" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/allure-go/pkg/framework/runner" "github.com/ozontech/cute" ) func TestInsideStep(t *testing.T) { runner.Run(t, "Single test with allure-go Runner", func(t provider.T) { t.WithNewStep("First step", func(sCtx provider.StepCtx) { sCtx.NewStep("Inside first step") }) t.WithNewStep("Step name", func(sCtx provider.StepCtx) { u, _ := url.Parse("https://jsonplaceholder.typicode.com/posts/1/comments") cute.NewTestBuilder(). Title("Super simple test"). Tags("simple", "suite", "some_local_tag", "json"). Parallel(). Create(). RequestBuilder( cute.WithHeaders(map[string][]string{ "some_header": []string{"something"}, }), cute.WithURL(u), cute.WithMethod(http.MethodPost), ). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusCreated). ExecuteTest(context.Background(), sCtx) }) }) } ================================================ FILE: examples/masked_data_test.go ================================================ //go:build example // +build example package examples import ( "context" "net/http" "net/url" "testing" "time" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/allure-go/pkg/framework/runner" "github.com/ozontech/cute" ) func TestSanitizer(t *testing.T) { runner.Run(t, "Single test with request and response sanitizer", func(t provider.T) { t.WithNewStep("First step", func(sCtx provider.StepCtx) { sCtx.NewStep("Inside first step") }) t.WithNewStep("Step name", func(sCtx provider.StepCtx) { u, _ := url.Parse("https://jsonplaceholder.typicode.com/posts/1/comments?example=11") query := u.Query() query.Set("name", "Vasya") u.RawQuery = query.Encode() cute.NewTestBuilder(). Title("Super simple test"). Tags("simple", "suite", "some_local_tag", "json"). Parallel(). Create(). RequestSanitizerHook(func(req *http.Request) { req.URL.Path = "/path/masked" values := req.URL.Query() values.Set("example", "masked") req.URL.RawQuery = values.Encode() req.Header["some_header"] = []string{"masked"} }). ResponseSanitizerHook(func(resp *http.Response) { resp.Header["some_header"] = []string{"masked"} resp.Header["Content-Type"] = []string{"masked"} }). RequestBuilder( cute.WithHeaders(map[string][]string{ "some_header": []string{"something"}, }), cute.WithURL(u), cute.WithMethod(http.MethodPost), ). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusCreated). ExecuteTest(context.Background(), sCtx) }) }) } ================================================ FILE: examples/parallel_test.go ================================================ //go:build example // +build example package examples import ( "context" "net/http" "testing" "time" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/cute" ) func Test_Async_1(t *testing.T) { cute.NewTestBuilder(). Title("Title async test 1"). Tags("parallel_test"). Parallel(). Create(). BeforeExecuteT( func(t cute.T, r *http.Request) error { t.WithNewStep("insideBefore", func(stepCtx provider.StepCtx) { time.Sleep(time.Second) now := time.Now() stepCtx.Logf("Test 1. Start time %v", now) stepCtx.WithNewParameters("Test 1. Time", now) }) return nil }, ). AfterExecuteT( func(t cute.T, resp *http.Response, errs []error) error { t.WithNewStep("insideAfter", func(stepCtx provider.StepCtx) { now := time.Now() stepCtx.Logf("Test 1. Stop time %v", now) stepCtx.WithNewParameters("Test 1. Stop time", now) }) return nil }). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), ). ExecuteTest(context.Background(), t) } func Test_Async_2(t *testing.T) { cute.NewTestBuilder(). Title("Title async test 2"). Tags("parallel_test"). Parallel(). Create(). BeforeExecuteT( func(t cute.T, r *http.Request) error { t.WithNewStep("insideBefore", func(stepCtx provider.StepCtx) { now := time.Now() stepCtx.Logf("Test 2. Start time %v", now) stepCtx.WithNewParameters("Test 2. Start time", now) time.Sleep(2 * time.Second) }) return nil }, ). AfterExecuteT( func(t cute.T, resp *http.Response, errs []error) error { t.WithNewStep("insideAfter", func(stepCtx provider.StepCtx) { now := time.Now() stepCtx.Logf("test 2. Stop time %v", now) stepCtx.WithNewParameters("Test 2. Stop time", now) }) return nil }). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), ). ExpectStatus(200). ExecuteTest(context.Background(), t) } ================================================ FILE: examples/single_test.go ================================================ //go:build example // +build example package examples import ( "context" "errors" "fmt" "io" "net/http" "net/url" "path" "testing" "time" "github.com/ozontech/allure-go/pkg/allure" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/allure-go/pkg/framework/runner" cuteErrors "github.com/ozontech/cute/errors" "github.com/ozontech/cute" "github.com/ozontech/cute/asserts/json" ) func Test_Single_1(t *testing.T) { cute.NewTestBuilder(). Title("Single test with default T"). Tag("single_test"). Description("some_description"). Parallel(). Create(). RequestRetry(3). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMarshalBody(struct { Name string `json:"name"` }{ Name: "Vasya Pupkin", }), cute.WithQueryKV("socks", "42"), cute.WithMethod(http.MethodGet), ). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusOK). AssertBody(json.Diff("{\"aaa\":\"bb\"}")). AssertBody( json.Present("$[1].name"), json.Present("$[0].passport"), // Example fail json.Equal("$[0].email", "Eliseo@gardner.biz"), CustomAssertBody(), ). AssertBodyT(func(t cute.T, body []byte) error { t.Step(allure.NewSimpleStep("inside Assert body. 1 ", allure.NewParameters("key", "value")...)) return nil }). After( func(response *http.Response, errors []error) error { b, err := io.ReadAll(response.Body) if err != nil { return err } email, err := json.GetValueFromJSON(b, "$[0].email") if err != nil { return err } fmt.Println("Email from test", email) return nil }, ). ExecuteTest(context.Background(), t) } func Test_Single_Broken(t *testing.T) { cute.NewTestBuilder(). Title("Test_Single_Broken"). Create(). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), ). BrokenAssertBodyT(func(t cute.T, body []byte) error { return errors.New("example broken error") }). ExpectStatus(http.StatusOK). NextTest(). Create(). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), ). AssertBody(func(body []byte) error { return errors.New("it's NOT must be run") }, ). ExecuteTest(context.Background(), t) t.Skip() } func Test_Single_RepeatPolitic_Optional_Success_Test(t *testing.T) { cute.NewTestBuilder(). Title("Test_Single_RepeatPolitic_Optional_Success_Test"). Create(). RequestRetry(2). RequestRetryOptional(true). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), ). BrokenAssertBodyT(func(t cute.T, body []byte) error { return errors.New("example broken error") }). ExpectStatus(http.StatusCreated). ExecuteTest(context.Background(), t) t.Logf("You should see it") } func Test_Single_RepeatPolitic_Broken_Failed_Test(t *testing.T) { cute.NewTestBuilder(). Title("Test_Single_RepeatPolitic_Broken_Failed_Test"). Create(). RequestRetry(2). RequestRetryOptional(false). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), ). BrokenAssertBodyT(func(t cute.T, body []byte) error { return errors.New("example broken error") }). ExpectStatus(http.StatusCreated). ExecuteTest(context.Background(), t) t.Logf("You should see it") } func Test_Single_Broken_2(t *testing.T) { cute.NewTestBuilder(). Title("Test_Single_Broken_2"). Create(). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), ). AssertBodyT(func(t cute.T, body []byte) error { err := errors.New("example broken error") return cuteErrors.WrapBrokenError(err) }). ExpectStatus(http.StatusOK). NextTest(). Create(). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), ). AssertBody(func(body []byte) error { return errors.New("it's NOT must be run") }, ). ExecuteTest(context.Background(), t) } func Test_Single_2_AllureRunner(t *testing.T) { runner.Run(t, "Single test with allure-go Runner", func(t provider.T) { var ( testMaker = cute.NewHTTPTestMaker() testBuilder = testMaker.NewTestBuilder() ) u, _ := url.Parse("https://jsonplaceholder.typicode.com/") u.Path = path.Join(u.Path, "/posts/1/comments") testBuilder. Title("Single test with allure.T and repeat errors"). Tag("single_test"). Description("some_description"). Create(). RequestRetryDelay(3*time.Second). // delay before new try RequestRetry(3). // count attempts RequestBuilder( cute.WithURL(u), cute.WithMethod(http.MethodGet), ). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusBadGateway). AssertBody( json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), ). OptionalAssertBody( json.Present("$[0].photo"), // Example optional fail ). ExecuteTest(context.Background(), t) }) } ================================================ FILE: examples/suite/common.go ================================================ package suite import ( "net/url" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/allure-go/pkg/framework/suite" "github.com/stretchr/testify/require" "github.com/ozontech/cute" ) type ExampleSuite struct { suite.Suite host *url.URL testMaker *cute.HTTPTestMaker } func (i *ExampleSuite) BeforeAll(t provider.T) { // Prepare http test builder i.testMaker = cute.NewHTTPTestMaker() // Preparing host host, err := url.Parse("https://jsonplaceholder.typicode.com/") require.NoError(t, err) i.host = host } func (i *ExampleSuite) BeforeEach(t provider.T) { t.Feature("ExampleSuite") t.Tags("some_global_tag") } ================================================ FILE: examples/suite/main_test.go ================================================ //go:build example // +build example package suite import ( "os" "testing" "github.com/ozontech/allure-go/pkg/framework/suite" ) func TestExampleSuite(t *testing.T) { os.Setenv("ALLURE_OUTPUT_PATH", "../") // custom, read Readme.md for more info suite.RunSuite(t, new(ExampleSuite)) } ================================================ FILE: examples/suite/one_step.go ================================================ package suite import ( "context" "net/http" "net/url" "path" "time" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/cute" "github.com/ozontech/cute/asserts/headers" "github.com/ozontech/cute/asserts/json" "github.com/ozontech/cute/examples" ) /* Example testing HTTP GET and validate body. Validate: 1) Execute time 2) Status code 3) Validate body by json schema 4) Validate fields in json Response: [ { "postId": 1, "id": 1, "name": "id labore ex et quam laborum", "email": "Eliseo@gardner.biz", "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium" }, { "postId": 1, "id": 2, "name": "quo vero reiciendis velit similique earum", "email": "Jayne_Kuhic@sydney.com", "body": "est natus enim nihil est dolore omnis voluptatem numquam\net omnis occaecati quod ullam at\nvoluptatem error expedita pariatur\nnihil sint nostrum voluptatem reiciendis et" }, { "postId": 1, "id": 3, "name": "odio adipisci rerum aut animi", "email": "Nikita@garfield.biz", "body": "quia molestiae reprehenderit quasi aspernatur\naut expedita occaecati aliquam eveniet laudantium\nomnis quibusdam delectus saepe quia accusamus maiores nam est\ncum et ducimus et vero voluptates excepturi deleniti ratione" }, { "postId": 1, "id": 4, "name": "alias odio sit", "email": "Lew@alysha.tv", "body": "non et atque\noccaecati deserunt quas accusantium unde odit nobis qui voluptatem\nquia voluptas consequuntur itaque dolor\net qui rerum deleniti ut occaecati" }, { "postId": 1, "id": 5, "name": "vero eaque aliquid doloribus et culpa", "email": "Hayden@althea.biz", "body": "harum non quasi et ratione\ntempore iure ex voluptates in ratione\nharum architecto fugit inventore cupiditate\nvoluptates magni quo et" } ] */ func (i *ExampleSuite) Test_OneStep(t provider.T) { var ( testBuilder = i.testMaker.NewTestBuilder() ) u, _ := url.Parse(i.host.String()) u.Path = path.Join(u.Path, "/posts/1/comments") testBuilder. Title("Test with one step"). Tags("one_stp", "some_local_tag", "suite", "json"). Feature("some_feature"). Epic("some_epic"). Description("some_description"). Parallel(). CreateStep("Example GET json request"). AfterExecuteT(func(t cute.T, resp *http.Response, errs []error) error { if len(errs) != 0 { return nil } /* Implement some logic */ return nil }, // After failed test func(t cute.T, resp *http.Response, errs []error) error { if len(errs) == 0 { return nil } /* Implement some logic */ return nil }, ). RequestBuilder( cute.WithHeaders(map[string][]string{ "some_header": []string{"something"}, "some_array_header": []string{"1", "2", "3", "some_thing"}, }), cute.WithURL(u), cute.WithMethod(http.MethodGet), ). ExpectExecuteTimeout(10*time.Second). ExpectJSONSchemaFile("file://./resources/example_valid_request.json"). ExpectStatus(http.StatusOK). AssertBody( json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), json.NotPresent("$[1].some_not_present"), json.LengthGreaterThan("$", 3), json.Length("$", 5), json.LengthLessThan("$", 100), json.NotEqual("$[3].name", "kekekekeke"), // Custom assert body examples.CustomAssertBody(), ). AssertBodyT( // Custom assert body with testing.tb examples.CustomAssertBodyT(), func(t cute.T, body []byte) error { /* Implement here logic with TB */ time.Sleep(5 * time.Second) return nil }, ). AssertHeaders( headers.Present("Content-Type"), // Custom assert headers examples.CustomAssertHeaders(), ). AssertResponse( examples.CustomAssertResponse(), ). ExecuteTest(context.Background(), t) } ================================================ FILE: examples/suite/one_step_errors.go ================================================ package suite import ( "context" "errors" "net/http" "time" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/cute" "github.com/ozontech/cute/asserts/headers" "github.com/ozontech/cute/asserts/json" cuteErrors "github.com/ozontech/cute/errors" "github.com/ozontech/cute/examples" ) func (i *ExampleSuite) Test_OneStep_Errors(t provider.T) { var ( testBuilder = i.testMaker.NewTestBuilder() ) testBuilder. Title("Test with errors"). Tags("one_step", "some_local_tag", "suite", "json"). Parallel(). CreateStep("Example GET json request"). RequestBuilder( cute.WithHeaders(map[string][]string{ "some_header": []string{"something"}, "some_array_header": []string{"1", "2", "3", "some_thing"}, }), cute.WithURI(i.host.String()+"/posts/1/comments"), cute.WithMethod(http.MethodGet), cute.WithMarshalBody( map[string]interface{}{ "key": "value", "more_key": map[string]interface{}{ "some_value": "sss", }, }, ), ). ExpectExecuteTimeout(10*time.Second). ExpectJSONSchemaFile("file://./resources/example_valid_request.json"). AssertBody( json.Equal("$[0].email", "something"), json.Present("$[1].not_present"), json.LengthGreaterThan("$", 99999), json.Length("$", 0), // Custom assert body examples.CustomAssertBody(), examples.CustomAssertBodyWithCustomError(), ). AssertHeaders( headers.Present("Content-Type"), // Custom assert headers examples.CustomAssertHeaders(), ). AssertResponse( examples.CustomAssertResponse(), ). AssertHeadersT( func(t cute.T, headers http.Header) error { // Example pretty print error return cuteErrors.NewAssertError("custom_assert", "example custom assert", "empty", "not empty") // }, ). // Example optional OptionalAssertBody( // example optional assert func(body []byte) error { return errors.New("some optional error from OptionalAssert") }, ). AssertBody( func(body []byte) error { return cuteErrors.NewOptionalError("some optional error from creator") // example optional error }, ). ExecuteTest(context.Background(), t) } ================================================ FILE: examples/suite/resources/example_valid_request.json ================================================ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "array", "items": [ { "type": "object", "properties": { "postId": { "type": "integer" }, "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" }, "body": { "type": "string" } }, "required": [ "postId", "id", "name", "email", "body" ] }, { "type": "object", "properties": { "postId": { "type": "integer" }, "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" }, "body": { "type": "string" } }, "required": [ "postId", "id", "name", "email", "body" ] }, { "type": "object", "properties": { "postId": { "type": "integer" }, "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" }, "body": { "type": "string" } }, "required": [ "postId", "id", "name", "email", "body" ] }, { "type": "object", "properties": { "postId": { "type": "integer" }, "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" }, "body": { "type": "string" } }, "required": [ "postId", "id", "name", "email", "body" ] }, { "type": "object", "properties": { "postId": { "type": "integer" }, "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" }, "body": { "type": "string" } }, "required": [ "postId", "id", "name", "email", "body" ] } ] } ================================================ FILE: examples/suite/simple.go ================================================ package suite import ( "context" "net/http" "net/url" "time" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/cute" ) /* Example simple request Validate: 1) Execute time 2) Status code */ func (i *ExampleSuite) Test_Simple(t provider.T) { var ( testMaker = cute.NewHTTPTestMaker() testBuilder = testMaker.NewTestBuilder() ) u, _ := url.Parse("https://jsonplaceholder.typicode.com/posts/1/comments") testBuilder. Title("Super simple test"). Tags("simple", "suite", "some_local_tag", "json"). Parallel(). Create(). RequestBuilder( cute.WithHeaders(map[string][]string{ "some_header": []string{"something"}, }), cute.WithURL(u), cute.WithMethod(http.MethodPost), ). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusCreated). ExecuteTest(context.Background(), t) } ================================================ FILE: examples/suite/two_steps.go ================================================ package suite import ( "context" "fmt" "io/ioutil" "net/http" "net/url" "path" "strings" "time" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/cute" "github.com/ozontech/cute/examples" ) /* Example testing HTTP POST, validate body and make second request. Validate: 1) Execute time 2) Status code */ func (i *ExampleSuite) Test_TwoSteps(t provider.T) { var ( testBuilder = i.testMaker.NewTestBuilder() // Request body r = ` { "result": { "author": "Yours Truly", "date": "15.11.1993", "slides": [ { "title": "Beer", "type": "drink" }, { "title": "Apple", "type": "fruit" }, { "title": "Orange", "type": "fruit" } ], "Info": { "shop": "BigShopPlus", "address": "address" }, "title": "Sample Show" } } ` ) u, _ := url.Parse(i.host.String()) u.Path = path.Join(u.Path, "/posts/1/comments") req, _ := http.NewRequest(http.MethodPost, u.String(), ioutil.NopCloser(strings.NewReader(r))) req.Header = map[string][]string{ "some_auth_token": []string{fmt.Sprint(11111)}, } testBuilder. Title("Test in suite with two steps"). Tags("suite", "some_tag"). Parallel(). CreateStep("Creat entry /posts/1"). // CreateWithStep first step Request(req). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusCreated). AssertBody( // Custom assert body examples.CustomAssertBody(), ). NextTest(). CreateStep("Delete entry"). // CreateWithStep second step for delete RequestBuilder( cute.WithURL(u), cute.WithMethod(http.MethodDelete), cute.WithHeaders(map[string][]string{ "some_auth_token": []string{fmt.Sprint(11111)}, }), ). ExecuteTest(context.Background(), t) } ================================================ FILE: examples/table_test/table_test.go ================================================ //go:build example // +build example package table_test import ( "context" "fmt" "net/http" "net/url" "os" "strconv" "testing" "time" "github.com/ozontech/cute" "github.com/ozontech/cute/asserts/json" "github.com/ozontech/cute/errors" ) func init() { os.Setenv("ALLURE_OUTPUT_PATH", "../") // custom, read Readme.md for more info } func Test_Table(t *testing.T) { u, _ := url.Parse("https://jsonplaceholder.typicode.com/posts/1/comments") req, _ := http.NewRequest(http.MethodPost, u.String(), nil) req.Header = map[string][]string{ "some_auth_token": []string{fmt.Sprint(11111)}, } cute.NewTestBuilder(). Title("Example put tests in table test"). Tag("table_test"). CreateTableTest(). PutNewTest( "Execute validation 1", req, &cute.Expect{ Code: 201, }). PutNewTest( "Execute validation 2", req, &cute.Expect{ AssertBody: []cute.AssertBody{ json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), }, }, ). ExecuteTest(context.Background(), t) } func Test_Table_Array(t *testing.T) { tests := []*cute.Test{ { Name: "Create something", Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodPost), }, }, Expect: &cute.Expect{ Code: 200, }, }, { Name: "Delete something", Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 200, AssertBody: []cute.AssertBody{ json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), func(body []byte) error { return errors.NewAssertError("example error", "example message", nil, nil) }, }, }, }, } cute.NewTestBuilder(). Title("Example table test"). Tag("table_test"). Description("Execute array tests"). CreateTableTest(). PutTests(tests...). ExecuteTest(context.Background(), t) } func Test_One_Execute(t *testing.T) { test := &cute.Test{ Name: "test_1", Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), }, }, Expect: nil, } test.Execute(context.Background(), t) } func Test_Array_Retry_OptionalFirstTries(t *testing.T) { tests := []*cute.Test{ { Name: "test_1", Retry: &cute.Retry{ MaxAttempts: 10, Delay: 1 * time.Second, }, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/Random/201,202"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 201, }, }, { Name: "test_2", Retry: &cute.Retry{ MaxAttempts: 10, Delay: 1 * time.Second, }, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/Random/403,404"), cute.WithMethod(http.MethodGet), cute.WithMarshalBody([]byte("{\"test\":\"abc\"}")), }, }, Expect: &cute.Expect{ Code: 404, }, }, } for _, test := range tests { test.Execute(context.Background(), t) } } func Test_Array_Retry_OptionalFirstTries_UltimatelyFailing(t *testing.T) { tests := []*cute.Test{ { Name: "test_1", Retry: &cute.Retry{ MaxAttempts: 4, Delay: 1 * time.Second, }, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/Random/202,200"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 201, }, }, { Name: "test_2", Retry: &cute.Retry{ MaxAttempts: 3, Delay: 1 * time.Second, }, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/Random/403,401"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 404, }, }, } for _, test := range tests { test.Execute(context.Background(), t) } } func Test_Array_TimeoutRetry(t *testing.T) { var executeTimeout = 3000 tests := []*cute.Test{ { Retry: &cute.Retry{ MaxAttempts: 2, }, Name: "test_timeout", Middleware: &cute.Middleware{ Before: []cute.BeforeExecute{ cute.BeforeExecute(func(request *http.Request) error { query := request.URL.Query() query.Set("sleep", strconv.Itoa(executeTimeout)) request.URL.RawQuery = query.Encode() executeTimeout = executeTimeout - 1000 return nil }), }, }, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/202?sleep=3000"), cute.WithBody([]byte("{\"test\":\"abc\"}")), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 202, ExecuteTime: 3 * time.Second, }, }, } for _, test := range tests { test.Execute(context.Background(), t) } } func Test_Array(t *testing.T) { tests := []*cute.Test{ { Name: "test_1", Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodPost), }, }, Expect: &cute.Expect{ Code: 201, }, }, { Name: "test_2", Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 200, AssertBody: []cute.AssertBody{ json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), func(body []byte) error { return errors.NewAssertError("example error", "example message", nil, nil) }, }, }, }, } for _, test := range tests { test.Execute(context.Background(), t) } } func Test_Array_All_Parallel(t *testing.T) { tests := []*cute.Test{ { Name: "test_201", Parallel: true, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/201"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 201, }, }, { Name: "test_200_delay_5s", Parallel: true, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/200?sleep=5000"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 200, }, }, { Name: "test_202_delay_3s", Parallel: true, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/202?sleep=3000"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 202, }, }, { Name: "test_203", Parallel: true, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/203"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 203, }, }, } for _, test := range tests { test.Execute(context.Background(), t) } } func Test_Array_Some_Parallel(t *testing.T) { tests := []*cute.Test{ { Name: "test_parallel_1", Parallel: true, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/201?sleep=1000"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 201, }, }, { Name: "test_parallel_2", Parallel: true, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/202?sleep=1000"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 202, }, }, { Name: "test_1_sequential", Parallel: false, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodPost), }, }, Expect: &cute.Expect{ Code: 201, }, }, { Name: "test_2_sequential", Parallel: false, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 200, AssertBody: []cute.AssertBody{ json.Equal("$[0].email", "Eliseo@gardner.biz"), json.Present("$[1].name"), }, }, }, } for _, test := range tests { test.Execute(context.Background(), t) } } func Test_Array_Retry(t *testing.T) { tests := []*cute.Test{ { Name: "test_1", Parallel: true, Retry: &cute.Retry{ MaxAttempts: 10, Delay: 1, }, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/Random/201,202"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 201, }, }, { Name: "test_2", Parallel: true, Retry: &cute.Retry{ MaxAttempts: 10, Delay: 1, }, Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/Random/403,404"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 404, }, }, } for _, test := range tests { test.Execute(context.Background(), t) } } func Test_Array_Timeout(t *testing.T) { tests := []*cute.Test{ { Name: "test_timeout", Middleware: nil, Request: &cute.Request{ Builders: []cute.RequestBuilder{ cute.WithURI("https://httpstat.us/202?sleep=3000"), cute.WithMethod(http.MethodGet), }, }, Expect: &cute.Expect{ Code: 202, ExecuteTime: 2 * time.Second, }, }, } for _, test := range tests { test.Execute(context.Background(), t) } } ================================================ FILE: examples/two_step_test.go ================================================ //go:build example // +build example package examples import ( "context" "errors" "fmt" "net/http" "testing" "time" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/allure-go/pkg/framework/runner" "github.com/ozontech/cute" ) func Test_TwoSteps_1(t *testing.T) { cute.NewTestBuilder(). Title("Test with two requests."). Tags("two_steps"). Parallel(). CreateStep("Create entry /posts/1"). // CreateWithStep first step RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), ). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusCreated). NextTest(). CreateStep("Delete entry"). // CreateWithStep second step for delete RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodDelete), cute.WithHeaders(map[string][]string{ "some_auth_token": []string{fmt.Sprint(11111)}, }), ). ExecuteTest(context.Background(), t) } func Test_TwoSteps_2_AllureRunner(t *testing.T) { runner.Run(t, "Test with two steps", func(t provider.T) { testBuilder := cute.NewHTTPTestMaker().NewTestBuilder() testBuilder. Title("Test with two requests executed by allure-go"). Tag("two_steps"). Description("some_description"). CreateStep("Request 1"). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), ). ExpectStatus(http.StatusOK). ExecuteTest(context.Background(), t) testBuilder. CreateStep("Request 2"). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/2/comments"), cute.WithMethod(http.MethodGet), ). ExpectExecuteTimeout(10*time.Second). ExpectStatus(http.StatusOK). ExecuteTest(context.Background(), t) }) } func Test_TwoSteps_3(t *testing.T) { responseCode := 0 // First step. cute.NewTestBuilder(). Title("Test with two requests and parse body."). Tag("two_steps"). Create(). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"), cute.WithMethod(http.MethodGet), ). ExpectStatus(http.StatusOK). RequireBody(func(body []byte) error { return errors.New("example") }). NextTest(). AfterTestExecute(func(response *http.Response, errors []error) error { // Execute after first step responseCode = response.StatusCode fmt.Println("Hello from after test execute") fmt.Println("Response code", responseCode) return nil }). // Second step. This test isn't run, because previous test has failed require validation Create(). RequestBuilder( cute.WithURI("https://jsonplaceholder.typicode.com/posts/2/comments"), cute.WithMethod(http.MethodDelete), ). ExecuteTest(context.Background(), t) fmt.Println("Response code from first request", responseCode) } ================================================ FILE: examples/upload_file_test.go ================================================ //go:build example_upload_file // +build example_upload_file package examples import ( "context" "net/http" "testing" "github.com/ozontech/cute" ) func TestUploadFile(t *testing.T) { cute.NewTestBuilder(). Title("Upload file"). Create(). RequestBuilder( cute.WithURI("http://localhost:7000/v1/banner"), cute.WithMethod("POST"), cute.WithFormKV("body", []byte("{\"name\": \"Vasya\"}")), // Fill the form with the body cute.WithFileFormKV("image", &cute.File{ // Fill the form with the file Path: "/vasya/thebestmypicture.png", }), ). ExpectStatus(http.StatusOK). ExecuteTest(context.Background(), t) } ================================================ FILE: go.mod ================================================ module github.com/ozontech/cute go 1.21 require ( github.com/josephburnett/jd v1.7.1 github.com/ohler55/ojg v1.21.1 github.com/ozontech/allure-go/pkg/allure v0.6.13 github.com/ozontech/allure-go/pkg/framework v0.6.31 github.com/stretchr/testify v1.8.4 github.com/xeipuuv/gojsonschema v1.2.0 moul.io/http2curl/v2 v2.3.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.21.1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/josephburnett/jd v1.7.1 h1:oXBPMS+SNnILTMGj1fWLK9pexpeJUXtbVFfRku/PjBU= github.com/josephburnett/jd v1.7.1/go.mod h1:R8ZnZnLt2D4rhW4NvBc/USTo6mzyNT6fYNIIWOJA9GY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/ohler55/ojg v1.21.1 h1:b2RLUaDcy9gvn46dmhTjezu/TDauoR0/kgKTqkwIxto= github.com/ohler55/ojg v1.21.1/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o= github.com/ozontech/allure-go/pkg/allure v0.6.13 h1:vkLSIvOEERHTxe+oq8DXDu/m+kLnVUkrXNN8xTKuKU4= github.com/ozontech/allure-go/pkg/allure v0.6.13/go.mod h1:4oEG2yq+DGOzJS/ZjPc87C/mx3tAnlYpYonk77Ru/vQ= github.com/ozontech/allure-go/pkg/framework v0.6.31 h1:u32AqB9/JkzcL5vSl8PSUmMZbsVTmoriDylI3FIYgX4= github.com/ozontech/allure-go/pkg/framework v0.6.31/go.mod h1:wfqY4e4+w4BoRFDxHp7TNcdWfcCOWJV3BjrUqUughWY= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: init.go ================================================ package cute var commonBuilder *HTTPTestMaker func init() { commonBuilder = NewHTTPTestMaker() } // NewTestBuilder is function for create base test builder, // For create custom test builder use NewHTTPTestMaker() func NewTestBuilder() AllureBuilder { return commonBuilder.NewTestBuilder() } ================================================ FILE: interface.go ================================================ package cute import ( "context" "net/http" "time" "github.com/ozontech/allure-go/pkg/allure" ) // AllureBuilder is a scope of methods for create allure information (Title, Tags, etc.) type AllureBuilder interface { AllureInfoBuilder AllureLabelsBuilder AllureLinksBuilder CreateBuilder // Parallel signals that this Test is to be run in parallel with (and only with) other parallel tests. // This function is not thread save. If you use multiply parallel with one T Test will panic. Parallel() AllureBuilder } // AllureInfoBuilder is a scope of methods for create allure information (Title, Tags, etc.) type AllureInfoBuilder interface { // Title is a function for set title in allure information Title(title string) AllureBuilder Titlef(format string, args ...interface{}) AllureBuilder // Description is a function for set description in allure information Description(description string) AllureBuilder Descriptionf(format string, args ...interface{}) AllureBuilder Stage(stage string) AllureBuilder Stagef(format string, args ...interface{}) AllureBuilder } // AllureLinksBuilder is a scope of methods to set allure links type AllureLinksBuilder interface { SetIssue(issue string) AllureBuilder SetTestCase(testCase string) AllureBuilder Link(link *allure.Link) AllureBuilder TmsLink(tmsLink string) AllureBuilder TmsLinks(tmsLinks ...string) AllureBuilder } // AllureLabelsBuilder is a scope of methods to set allure labels type AllureLabelsBuilder interface { Feature(feature string) AllureBuilder Epic(epic string) AllureBuilder AllureID(value string) AllureBuilder Tags(tags ...string) AllureBuilder ID(value string) AllureBuilder AddSuiteLabel(value string) AllureBuilder AddSubSuite(value string) AllureBuilder AddParentSuite(value string) AllureBuilder Story(value string) AllureBuilder Tag(value string) AllureBuilder Severity(value allure.SeverityType) AllureBuilder Owner(value string) AllureBuilder Lead(value string) AllureBuilder Label(label *allure.Label) AllureBuilder Labels(labels ...*allure.Label) AllureBuilder Layer(value string) AllureBuilder Stagef(format string, args ...interface{}) AllureBuilder Stage(stage string) AllureBuilder } // CreateBuilder is functions for create Test or table tests type CreateBuilder interface { // Create is a function for save main information about allure and start write tests Create() MiddlewareRequest // CreateStep is a function for create step inside suite for Test CreateStep(string) MiddlewareRequest // CreateTableTest is function for create table Test CreateTableTest() MiddlewareTable } // MiddlewareTable is functions for create table Test type MiddlewareTable interface { TableTest BeforeTest AfterTest } // MiddlewareRequest is function for create requests or add After/Before functions type MiddlewareRequest interface { RequestHTTPBuilder RetryPolitic BeforeTest AfterTest } // RetryPolitic is a scope of methods to configure test repeat type RetryPolitic interface { // Retry is a function for configure test repeat // if response.Code != Expect.Code or any of asserts are failed/broken than test will repeat counts with delay. // Default delay is 1 second. Retry(count int) MiddlewareRequest // RetryDelay set delay for test repeat. // if response.Code != Expect.Code or any of asserts are failed/broken than test will repeat counts with delay. // Default delay is 1 second. RetryDelay(timeout time.Duration) MiddlewareRequest } // BeforeTest are functions for processing request before test execution // Same functions: // Before type BeforeTest interface { // BeforeExecute is function for processing request before test execution BeforeExecute(...BeforeExecute) MiddlewareRequest // BeforeExecuteT is function for processing request before test execution BeforeExecuteT(...BeforeExecuteT) MiddlewareRequest } // After are functions for processing response after test execution // Same functions: // AfterText // AfterTestExecute type After interface { // After is function for processing response after test execution After(...AfterExecute) ExpectHTTPBuilder // AfterT is function for processing response after test execution AfterT(...AfterExecuteT) ExpectHTTPBuilder } // AfterTest are functions for processing response after test execution // Same functions: // After // AfterTestExecute type AfterTest interface { // AfterExecute is function for processing response after test execution AfterExecute(...AfterExecute) MiddlewareRequest // AfterExecuteT is function for processing response after test execution AfterExecuteT(...AfterExecuteT) MiddlewareRequest } // AfterTestExecute are functions for processing response after test execution // Same functions: // After // AfterText type AfterTestExecute interface { // AfterTestExecute is function for processing response after test execution AfterTestExecute(...AfterExecute) NextTestBuilder // AfterTestExecuteT is function for processing response after test execution AfterTestExecuteT(...AfterExecuteT) NextTestBuilder } // TableTest is function for put request and assert for table tests type TableTest interface { // PutNewTest is function for put request and assert for table Test PutNewTest(name string, r *http.Request, expect *Expect) TableTest // PutTests is function for put requests and asserts for table Test PutTests(params ...*Test) TableTest ControlTest } // RequestHTTPBuilder is a scope of methods to create HTTP requests type RequestHTTPBuilder interface { // Request is function for set http.Request Request(r *http.Request) ExpectHTTPBuilder // RequestBuilder is function for set http.Request with builders // Available builders: // WithMethod // WithURL // WithHeaders // WithHeadersKV // WithBody // WithMarshalBody // WithBody // WithURI // WithQuery // WithQueryKV // WithFileForm // WithFileFormKV // WithForm // WithFormKV RequestBuilder(r ...RequestBuilder) ExpectHTTPBuilder RequestParams } // RequestParams is a scope of methods to configure request type RequestParams interface { // RequestRepeat is a function for set options in request // if response.Code != Expect.Code, than request will repeat counts with delay. // Default delay is 1 second. // Deprecated: use RequestRetry instead RequestRepeat(count int) RequestHTTPBuilder RequestRetry(count int) RequestHTTPBuilder // RequestRepeatDelay set delay for request repeat. // if response.Code != Expect.Code, than request will repeat counts with delay. // Default delay is 1 second. // Deprecated: use RequestRetryDelay instead RequestRepeatDelay(delay time.Duration) RequestHTTPBuilder RequestRetryDelay(delay time.Duration) RequestHTTPBuilder // RequestRepeatPolitic is a politic for repeat request. // if response.Code != Expect.Code, than request will repeat counts with delay. // if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute. // If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will execute. // Deprecated: use RequestRetryPolitic instead RequestRepeatPolitic(politic *RequestRepeatPolitic) RequestHTTPBuilder RequestRetryPolitic(politic *RequestRetryPolitic) RequestHTTPBuilder // RequestRepeatOptional is a option politic for repeat request. // if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute. // Deprecated: use RequestRetryOptional instead RequestRepeatOptional(optional bool) RequestHTTPBuilder RequestRetryOptional(optional bool) RequestHTTPBuilder // RequestRepeatBroken is a broken politic for repeat request. // If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will execute. // Deprecated: use RequestRetryBroken instead RequestRepeatBroken(broken bool) RequestHTTPBuilder RequestRetryBroken(broken bool) RequestHTTPBuilder // RequestSanitizerHook sets a RequestSanitizerHook function for the request. // This hook allows you to modify or mask parts of the request URL (e.g., hide sensitive data) // before it is logged or added to the test report (Allure). // Example usage: RequestWithSanitizeHook(func(req *http.Request) { ... }). // Example: RequestWithSanitizeHook(func(req *http.Request) { req.URL.Path = "/masked" }). // Example: RequestWithSanitizeHook(func(req *http.Request) { req.Header["some_header"] = []string{"masked"} }). RequestSanitizerHook(hook RequestSanitizerHook) RequestHTTPBuilder // ResponseSanitizerHook sets a ResponseSanitizerHook function for the request. // This hook allows you to modify or mask parts of the response body (e.g., hide sensitive data) // before it is logged or added to the test report (Allure). // Example usage: ResponseWithSanitizeHook(func(resp *http.Response) { ... }). ResponseSanitizerHook(hook ResponseSanitizerHook) RequestHTTPBuilder } // ExpectHTTPBuilder is a scope of methods for validate http response type ExpectHTTPBuilder interface { // ExpectExecuteTimeout is function for validate time of execution // Default value - 10 seconds ExpectExecuteTimeout(t time.Duration) ExpectHTTPBuilder // ExpectStatus is function for validate response status code ExpectStatus(code int) ExpectHTTPBuilder // ExpectJSONSchemaString is function for validate response by json schema from string ExpectJSONSchemaString(schema string) ExpectHTTPBuilder // ExpectJSONSchemaByte is function for validate response by json schema from byte ExpectJSONSchemaByte(schema []byte) ExpectHTTPBuilder // ExpectJSONSchemaFile is function for validate response by json schema from file // For get file from network use: // "http://www.some_host.com/schema.json" // For get local file use: // "file://./project/me/schema.json" ExpectJSONSchemaFile(path string) ExpectHTTPBuilder // AssertBody is function for validate response body. // Available asserts from asserts/json/json.go: // Contains is a function to assert that a jsonpath expression extracts a value in an array // Equal is a function to assert that a jsonpath expression matches the given value // NotEqual is a function to check jsonpath expression value is not equal to given value // Length is a function to asserts that jsonpath expression value is the expected length // GreaterThan is a function to asserts that jsonpath expression value is greater than the given length // LessThan is a function to asserts that jsonpath expression value is less than the given length // Present is a function to asserts that jsonpath expression value is present // NotPresent is a function to asserts that jsonpath expression value is not present // Also you can write you assert. AssertBody(asserts ...AssertBody) ExpectHTTPBuilder // RequireBody implements the same assertions as the `AssertBody`, but stops test execution when a test fails. RequireBody(asserts ...AssertBody) ExpectHTTPBuilder // OptionalAssertBody is not a mandatory assert. // Mark in allure as Skipped OptionalAssertBody(asserts ...AssertBody) ExpectHTTPBuilder // BrokenAssertBody is function for validate response, if it's failed, then test will be Broken. // Mark in allure as Broken BrokenAssertBody(asserts ...AssertBody) ExpectHTTPBuilder // AssertBodyT is function for validate response body with help testing.TB and allure allureProvider. // You may create allure step inside assert, add attachment, log information, etc. AssertBodyT(asserts ...AssertBodyT) ExpectHTTPBuilder // RequireBodyT implements the same assertions as the `AssertBodyT`, but stops test execution when a test fails. RequireBodyT(asserts ...AssertBodyT) ExpectHTTPBuilder // OptionalAssertBodyT is not a mandatory assert. // Mark in allure as Skipped OptionalAssertBodyT(asserts ...AssertBodyT) ExpectHTTPBuilder // BrokenAssertBodyT is function for validate response, if it's failed, then test will be Broken. // Mark in allure as Broken BrokenAssertBodyT(asserts ...AssertBodyT) ExpectHTTPBuilder // AssertHeaders is function for validate response headers // Available asserts from asserts/headers/headers.go: // Present is a function to asserts header is present // NotPresent is a function to asserts header is present // Also you can write you assert. AssertHeaders(asserts ...AssertHeaders) ExpectHTTPBuilder // RequireHeaders implements the same assertions as the `AssertHeaders`, but stops test execution when a test fails. RequireHeaders(asserts ...AssertHeaders) ExpectHTTPBuilder // OptionalAssertHeaders is not a mandatory assert. // Mark in allure as Skipped OptionalAssertHeaders(asserts ...AssertHeaders) ExpectHTTPBuilder // BrokenAssertHeaders is function for validate response, if it's failed, then test will be Broken. // Mark in allure as Broken BrokenAssertHeaders(asserts ...AssertHeaders) ExpectHTTPBuilder // AssertHeadersT is function for validate headers body with help testing.TB and allure allureProvider. // You may create allure step inside assert, add attachment, log information, etc. AssertHeadersT(asserts ...AssertHeadersT) ExpectHTTPBuilder // RequireHeadersT implements the same assertions as the `AssertHeadersT`, but stops test execution when a test fails. RequireHeadersT(asserts ...AssertHeadersT) ExpectHTTPBuilder // OptionalAssertHeadersT is not a mandatory assert. // Mark in allure as Skipped OptionalAssertHeadersT(asserts ...AssertHeadersT) ExpectHTTPBuilder // BrokenAssertHeadersT is function for validate response, if it's failed, then test will be Broken. // Mark in allure as Broken BrokenAssertHeadersT(asserts ...AssertHeadersT) ExpectHTTPBuilder // AssertResponse is function for validate response. AssertResponse(asserts ...AssertResponse) ExpectHTTPBuilder // RequireResponse implements the same assertions as the `AssertResponse`, but stops test execution when a test fails. RequireResponse(asserts ...AssertResponse) ExpectHTTPBuilder // OptionalAssertResponse is not a mandatory assert. // Mark in allure as Skipped OptionalAssertResponse(asserts ...AssertResponse) ExpectHTTPBuilder // BrokenAssertResponse is function for validate response, if it's failed, then test will be Broken. // Mark in allure as Broken BrokenAssertResponse(asserts ...AssertResponse) ExpectHTTPBuilder // AssertResponseT is function for validate response with help testing.TB. // You may create allure step inside assert, add attachment, log information, etc. AssertResponseT(asserts ...AssertResponseT) ExpectHTTPBuilder // RequireResponseT implements the same assertions as the `AssertResponseT`, but stops test execution when a test fails. RequireResponseT(asserts ...AssertResponseT) ExpectHTTPBuilder // OptionalAssertResponseT is not a mandatory assert. // Mark in allure as Skipped OptionalAssertResponseT(asserts ...AssertResponseT) ExpectHTTPBuilder // BrokenAssertResponseT is function for validate response, if it's failed, then test will be Broken. // Mark in allure as Broken BrokenAssertResponseT(asserts ...AssertResponseT) ExpectHTTPBuilder After ControlTest } // ControlTest is function for manipulating tests type ControlTest interface { NextTest() NextTestBuilder // ExecuteTest is a function for execute Test ExecuteTest(ctx context.Context, t tProvider) []ResultsHTTPBuilder } // NextTestBuilder is a scope of methods for processing response, after Test. type NextTestBuilder interface { AfterTestExecute CreateBuilder } // ResultsHTTPBuilder is a scope of methods for processing results type ResultsHTTPBuilder interface { // GetHTTPResponse is a function, which returns http response GetHTTPResponse() *http.Response // GetErrors is a function, which returns all errors from test GetErrors() []error // GetName is a function, which returns name of Test GetName() string // GetResultState is a function, which returns state of test // State could be ResultStateSuccess, ResultStateBroken, ResultStateFail GetResultState() ResultState } // BeforeExecute is a function for processing request before test execution type BeforeExecute func(*http.Request) error // BeforeExecuteT is a function for processing request before test execution type BeforeExecuteT func(T, *http.Request) error // AfterExecute is a function for processing response after test execution type AfterExecute func(*http.Response, []error) error // AfterExecuteT is a function for processing response after test execution type AfterExecuteT func(T, *http.Response, []error) error ================================================ FILE: internal/utils/body.go ================================================ package utils import ( "bytes" "io" "net/http" ) // GetBody get body from IO func GetBody(body io.ReadCloser) ([]byte, error) { var ( err error buf = new(bytes.Buffer) ) _, err = io.Copy(buf, body) if err != nil { return nil, err } return buf.Bytes(), nil } // DrainBody ... func DrainBody(body io.ReadCloser) (r1, r2 io.ReadCloser, err error) { if body == nil || body == http.NoBody { // No copying needed. Preserve the magic sentinel meaning of NoBody. return http.NoBody, http.NoBody, nil } var buf bytes.Buffer if _, err = buf.ReadFrom(body); err != nil { return nil, body, err } if err = body.Close(); err != nil { return nil, body, err } return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), nil } ================================================ FILE: internal/utils/json.go ================================================ package utils import ( "bytes" "encoding/json" ) // ToJSON returns string Json representation of any object that can be marshaled. func ToJSON(v interface{}) (string, error) { j, err := json.Marshal(v) return string(j), err } // PrettyJSON make indent to json byte array. Returns prettified json as []byte or error if is it impossible func PrettyJSON(b []byte) ([]byte, error) { var out bytes.Buffer err := json.Indent(&out, b, "", " ") return out.Bytes(), err } ================================================ FILE: json_marshaler.go ================================================ package cute import "encoding/json" // JSONMarshaler is marshaler which use for marshal/unmarshal JSON to/from struct type JSONMarshaler interface { Marshal(v any) ([]byte, error) Unmarshal(data []byte, v any) error } type jsonMarshaler struct { } func (j jsonMarshaler) Marshal(v any) ([]byte, error) { return json.Marshal(v) } func (j jsonMarshaler) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) } ================================================ FILE: jsonschema.go ================================================ package cute import ( "fmt" "github.com/ozontech/cute/errors" "github.com/xeipuuv/gojsonschema" ) // Validate is a function to validate json by json schema. // Automatically add information about validation to allure. func (it *Test) validateJSONSchema(t internalT, body []byte) []error { var ( expect gojsonschema.JSONLoader ) switch { case it.Expect.JSONSchema.String != "": expect = gojsonschema.NewStringLoader(it.Expect.JSONSchema.String) case it.Expect.JSONSchema.Byte != nil: expect = gojsonschema.NewBytesLoader(it.Expect.JSONSchema.Byte) case it.Expect.JSONSchema.File != "": expect = gojsonschema.NewReferenceLoader(it.Expect.JSONSchema.File) default: return nil } return it.executeWithStep(t, "Validate body by JSON schema", func(_ T) []error { return checkJSONSchema(expect, body) }) } func checkJSONSchema(expect gojsonschema.JSONLoader, data []byte) []error { scope := make([]error, 0) validateResult, err := gojsonschema.Validate(expect, gojsonschema.NewBytesLoader(data)) if err != nil { return []error{errors.NewEmptyAssertError("could not validate json schema", err.Error())} } if !validateResult.Valid() && len(validateResult.Errors()) > 0 { for _, resultError := range validateResult.Errors() { scope = append( scope, createJSONSchemaError(resultError), ) } } return scope } func createJSONSchemaError(err gojsonschema.ResultError) error { fields := make(map[string]interface{}) textError := "" if v, ok := err.Details()["context"]; ok { textError = fmt.Sprintf("On path: %v.", v) fields["Path"] = v } if v, ok := err.Details()["field"]; ok { textError = fmt.Sprintf("%v Error field: %v.", textError, v) fields["Field"] = v } textError = fmt.Sprintf("%v Error: %v.", textError, err.String()) assertError := errors.NewAssertError( fmt.Sprintf("Error \"%v\"", err.Type()), textError, err.Details()["given"], err.Details()["expected"]) assertError.(errors.WithFields).PutFields(fields) return assertError } ================================================ FILE: jsonschema_test.go ================================================ package cute import ( "testing" cuteErrors "github.com/ozontech/cute/errors" "github.com/stretchr/testify/require" ) func TestValidateJSONSchemaEmptySchema(t *testing.T) { var ( tBuilder = createDefaultTest(&HTTPTestMaker{middleware: new(Middleware)}) ) tBuilder.initEmptyFields() errs := tBuilder.validateJSONSchema(nil, []byte{}) require.Len(t, errs, 0) } func TestValidateJSONSchemaFromString(t *testing.T) { var ( tBuilder = createDefaultTest(&HTTPTestMaker{middleware: new(Middleware)}) tempT = createAllureT(t) ) tBuilder.initEmptyFields() body := []byte(` { "firstName": "Boris", "lastName": "Britva", "age": 77 } `) tBuilder.Expect.JSONSchema.String = ` { "$id": "https://example.com/person.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Person", "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" }, "age": { "type": "integer", "minimum": 0 } } } ` errs := tBuilder.validateJSONSchema(tempT, body) require.Len(t, errs, 0) } func TestValidateJSONSchemaFromStringWithError(t *testing.T) { var ( tBuilder = createDefaultTest(&HTTPTestMaker{middleware: new(Middleware)}) tempT = createAllureT(t) ) tBuilder.initEmptyFields() body := []byte(` { "firstName": "Boris", "lastName": "Britva", "age": "1" } `) tBuilder.Expect.JSONSchema.String = ` { "$id": "https://example.com/person.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Person", "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" }, "age": { "type": "integer" } } } ` errs := tBuilder.validateJSONSchema(tempT, body) require.Len(t, errs, 1) require.Error(t, errs[0]) errWithName := errs[0].(cuteErrors.WithNameError) require.NotEmpty(t, errWithName.GetName()) expectedError := errs[0].(cuteErrors.WithFields) require.Equal(t, "integer", expectedError.GetFields()["Expected"]) require.Equal(t, "string", expectedError.GetFields()["Actual"]) require.Equal(t, "age", expectedError.GetFields()["Field"]) require.Equal(t, "(root).age", expectedError.GetFields()["Path"]) } func TestValidateJSONSchemaFromByteWithTwoError(t *testing.T) { var ( tBuilder = createDefaultTest(&HTTPTestMaker{middleware: new(Middleware)}) tempT = createAllureT(t) ) tBuilder.initEmptyFields() body := []byte(` { "firstName": "Boris", "lastName": "Britva", "age": "1" } `) tBuilder.Expect.JSONSchema.String = ` { "$id": "https://example.com/person.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Person", "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "integer" }, "age": { "type": "integer" } } } ` errs := tBuilder.validateJSONSchema(tempT, body) require.Len(t, errs, 2) for _, err := range errs { errWithName := err.(cuteErrors.WithNameError) require.NotEmpty(t, errWithName.GetName()) expectedError := err.(cuteErrors.WithFields) require.NotEmpty(t, expectedError.GetFields()["Actual"]) require.NotEmpty(t, expectedError.GetFields()["Expected"]) } } ================================================ FILE: logger.go ================================================ package cute import ( "fmt" ) type tlogger interface { Name() string Logf(format string, args ...any) Errorf(format string, args ...interface{}) } // Info is a function to log info message func (it *Test) Info(t tlogger, format string, args ...interface{}) { it.logf(t, "INFO", format, args...) } // Error is a function to log error message func (it *Test) Error(t tlogger, format string, args ...interface{}) { it.errorf(t, "ERROR", format, args...) } // Debug is a function to log debug message func (it *Test) Debug(t tlogger, format string, args ...interface{}) { it.logf(t, "DEBUG", format, args...) } func (it *Test) logf(t tlogger, level, format string, args ...interface{}) { name := it.Name if it.Name == "" { name = t.Name() } // If we are in a retry context, add some indication in the logs about the current attempt if it.Retry.MaxAttempts != 1 { t.Logf("[%s][%s](Attempt #%d) %v\n", name, level, it.Retry.currentCount, fmt.Sprintf(format, args...)) } else { t.Logf("[%s][%s] %v\n", name, level, fmt.Sprintf(format, args...)) } } func (it *Test) errorf(t tlogger, level, format string, args ...interface{}) { name := it.Name if it.Name == "" { name = t.Name() } // If we are in a retry context, add some indication in the logs about the current attempt if it.Retry.MaxAttempts != 1 { t.Logf("[%s][%s](Attempt #%d) %v\n", name, level, it.Retry.currentCount, fmt.Sprintf(format, args...)) } else { t.Logf("[%s][%s] %v\n", name, level, fmt.Sprintf(format, args...)) } } ================================================ FILE: provider.go ================================================ package cute import ( "github.com/ozontech/allure-go/pkg/allure" "github.com/ozontech/allure-go/pkg/framework/provider" ) // T is internal testing.T provider type T interface { tProvider logProvider stepProvider attachmentProvider parametersProvider } type allureProvider interface { internalT Parallel() Broken() BrokenNow() Run(testName string, testBody func(provider.T), tags ...string) (res *allure.Result) infoAllureProvider labelsAllureProvider linksAllureProvider } type internalT interface { Broken() BrokenNow() tProvider logProvider stepProvider attachmentProvider } type tProvider interface { Fail() FailNow() Name() string Log(args ...interface{}) Logf(format string, args ...interface{}) Error(args ...interface{}) Errorf(format string, args ...interface{}) } type logProvider interface { LogStep(args ...interface{}) LogfStep(format string, args ...interface{}) } type stepProvider interface { Step(step *allure.Step) WithNewStep(stepName string, step func(ctx provider.StepCtx), params ...*allure.Parameter) } type attachmentProvider interface { WithAttachments(attachments ...*allure.Attachment) WithNewAttachment(name string, mimeType allure.MimeType, content []byte) } type parametersProvider interface { WithParameters(parameters ...*allure.Parameter) WithNewParameters(kv ...interface{}) } type infoAllureProvider interface { Title(args ...interface{}) Titlef(format string, args ...interface{}) Description(args ...interface{}) Descriptionf(format string, args ...interface{}) Stage(args ...interface{}) Stagef(format string, args ...interface{}) } type labelsAllureProvider interface { ID(value string) AllureID(value string) Epic(value string) Layer(value string) AddSuiteLabel(value string) AddSubSuite(value string) AddParentSuite(value string) Feature(value string) Story(value string) Tag(value string) Tags(values ...string) Severity(value allure.SeverityType) Owner(value string) Lead(value string) Label(label *allure.Label) Labels(labels ...*allure.Label) } type linksAllureProvider interface { SetIssue(issue string) SetTestCase(testCase string) Link(link *allure.Link) TmsLink(tmsCase string) TmsLinks(tmsCases ...string) } ================================================ FILE: request.go ================================================ package cute import ( "net/url" ) // RequestBuilder is a function for set options in request type RequestBuilder func(o *requestOptions) // File is struct for upload file in form field // If you set Path, file will read from file system // If you set Name and Body, file will set from this fields type File struct { Path string Name string Body []byte } type requestOptions struct { method string url *url.URL uri string headers map[string][]string query map[string][]string body []byte bodyMarshal interface{} fileForms map[string]*File forms map[string][]byte } func newRequestOptions() *requestOptions { return &requestOptions{ headers: make(map[string][]string), query: make(map[string][]string), fileForms: make(map[string]*File), forms: make(map[string][]byte), } } // WithMethod is a function for set method (GET, POST ...) in request func WithMethod(method string) func(o *requestOptions) { return func(o *requestOptions) { o.method = method } } // WithURL is a function for set url in request func WithURL(url *url.URL) func(o *requestOptions) { return func(o *requestOptions) { o.url = url } } // WithURI is a function for set url in request func WithURI(uri string) func(o *requestOptions) { return func(o *requestOptions) { o.uri = uri } } // WithHeaders is a function for set or merge headers in request func WithHeaders(headers map[string][]string) func(o *requestOptions) { return func(o *requestOptions) { for key, values := range headers { o.headers[key] = append(o.headers[key], values...) } } } // WithHeadersKV is a function for set headers in request func WithHeadersKV(name string, value string) func(o *requestOptions) { return func(o *requestOptions) { o.headers[name] = []string{value} } } // WithQueryKV is a function for set query in request func WithQueryKV(name string, value string) func(o *requestOptions) { return func(o *requestOptions) { o.query[name] = []string{value} } } // WithQuery is a function for set or merge query parameters in request func WithQuery(queries map[string][]string) func(o *requestOptions) { return func(o *requestOptions) { for key, values := range queries { o.query[key] = values } } } // WithBody is a function for set body in request func WithBody(body []byte) func(o *requestOptions) { return func(o *requestOptions) { o.body = body } } // WithMarshalBody is a function for marshal body and set body in request func WithMarshalBody(body interface{}) func(o *requestOptions) { return func(o *requestOptions) { o.bodyMarshal = body } } // WithFileFormKV is a function for set file form in request func WithFileFormKV(name string, file *File) func(o *requestOptions) { return func(o *requestOptions) { o.fileForms[name] = file } } // WithFileForm is a function for set file form in request func WithFileForm(fileForms map[string]*File) func(o *requestOptions) { return func(o *requestOptions) { for name, file := range fileForms { o.fileForms[name] = file } } } // WithFormKV is a function for set body in form request func WithFormKV(name string, body []byte) func(o *requestOptions) { return func(o *requestOptions) { o.forms[name] = body } } // WithForm is a function for set body in form request func WithForm(forms map[string][]byte) func(o *requestOptions) { return func(o *requestOptions) { for name, body := range forms { o.forms[name] = body } } } ================================================ FILE: request_test.go ================================================ package cute import ( "net/http" "net/url" "testing" "github.com/stretchr/testify/require" ) func TestRequest(t *testing.T) { var ( req = newRequestOptions() headers = map[string][]string{ "key": []string{ "value", }, } query = map[string][]string{ "query_key": []string{ "query_value", }, } method = http.MethodGet mBody = map[string]interface{}{ "key": map[string]interface{}{ "some": "value", }, "key_twi": "more", } uri = "https://goo.com" u, _ = url.Parse("https://ho.com") body = []byte("body") ) WithHeaders(headers)(req) WithMarshalBody(mBody)(req) WithURI(uri)(req) WithMethod(method)(req) WithURL(u)(req) WithBody(body)(req) WithQuery(query)(req) require.Equal(t, req.headers, headers) require.Equal(t, req.uri, uri) require.Equal(t, req.bodyMarshal, mBody) require.Equal(t, req.method, method) require.Equal(t, req.query, query) require.Equal(t, req.body, body) require.Equal(t, req.url, u) } ================================================ FILE: result.go ================================================ package cute import ( "net/http" ) // ResultState is state of test type ResultState int // ResultState is state of test const ( ResultStateSuccess ResultState = iota ResultStateBroken ResultStateFail // resultStateFailNow is state for require validations (execute failNow) resultStateFailNow ) type testResults struct { name string state ResultState resp *http.Response errors []error } func newTestResult(name string, resp *http.Response, state ResultState, errs []error) ResultsHTTPBuilder { return &testResults{ name: name, resp: resp, state: state, errors: errs, } } func (r *testResults) GetHTTPResponse() *http.Response { return r.resp } func (r *testResults) GetErrors() []error { return r.errors } func (r *testResults) GetName() string { return r.name } func (r *testResults) GetResultState() ResultState { if r.state == resultStateFailNow { return ResultStateFail } return r.state } ================================================ FILE: result_test.go ================================================ package cute import ( "errors" "net/http" "testing" "github.com/stretchr/testify/require" ) func TestResult(t *testing.T) { var ( firstErr = errors.New("first_error") secondErr = errors.New("second_error") resp = &http.Response{ Status: "OK", StatusCode: 200, } name = "Name" testResults ResultsHTTPBuilder = &testResults{ name: name, state: ResultStateBroken, resp: resp, errors: []error{ firstErr, secondErr, }, } ) require.Equal(t, name, testResults.GetName()) require.Equal(t, resp, testResults.GetHTTPResponse()) require.Equal(t, []error{firstErr, secondErr}, testResults.GetErrors()) } ================================================ FILE: roundtripper.go ================================================ package cute import ( "context" "errors" "fmt" "io" "net/http" "strings" "time" "github.com/ozontech/allure-go/pkg/allure" "moul.io/http2curl/v2" cuteErrors "github.com/ozontech/cute/errors" "github.com/ozontech/cute/internal/utils" ) func (it *Test) makeRequest(t internalT, req *http.Request) (*http.Response, []error) { var ( delay = defaultDelayRepeat countRepeat = 1 resp *http.Response err error scope = make([]error, 0) ) if it.Request.Retry.Delay != 0 { delay = it.Request.Retry.Delay } if it.Request.Retry.Count != 0 { countRepeat = it.Request.Retry.Count } for i := 1; i <= countRepeat; i++ { it.executeWithStep(t, it.createTitle(i, countRepeat, req), func(t T) []error { resp, err = it.doRequest(t, req) if err != nil { if it.Request.Retry.Broken { err = wrapBrokenError(err) } if it.Request.Retry.Optional { err = wrapOptionalError(err) } return []error{err} } return nil }) if err == nil { break } scope = append(scope, err) if i != countRepeat { time.Sleep(delay) } } return resp, scope } func (it *Test) doRequest(t T, baseReq *http.Request) (*http.Response, error) { // copy request, because body can be read once req, err := copyRequest(baseReq.Context(), baseReq) if err != nil { return nil, cuteErrors.NewCuteError("[Internal] Could not copy request", err) } resp, httpErr := it.httpClient.Do(req) // if the timeout is triggered, we properly log the timeout error on allure and in traces if errors.Is(httpErr, context.DeadlineExceeded) { // Add information (method, host, curl) about request to Allure step // should be after httpClient.Do and from resp.Request, because in roundTripper request may be changed if addErr := it.addInformationRequest(t, req); addErr != nil { // Ignore err return, because it's connected with test logic it.Error(t, "Could not log information about request. error %v", addErr) } return nil, cuteErrors.NewEmptyAssertError( "Request timeout", fmt.Sprintf("expected request to be completed in %v, but was not", it.Expect.ExecuteTime)) } // http client has case wheh it return response and error in one time // we have to check this case if resp == nil { if httpErr != nil { return nil, cuteErrors.NewCuteError("[HTTP] Could not do request", httpErr) } // if response is nil, we can't get information about request and response return nil, cuteErrors.NewCuteError("[HTTP] Response is nil", httpErr) } // BAD CODE. Need to copy body, because we can't read body again from resp.Request.Body. Problem is io.Reader resp.Request.Body, baseReq.Body, err = utils.DrainBody(baseReq.Body) if err != nil { it.Error(t, "Could not drain body from baseReq.Body. error %v", err) // Ignore err return, because it's connected with test logic } // Add information (method, host, curl) about request to Allure step // should be after httpClient.Do and from resp.Request, because in roundTripper request may be changed if addErr := it.addInformationRequest(t, resp.Request); addErr != nil { it.Error(t, "Could not log information about request. error %v", addErr) // Ignore err return, because it's connected with test logic } if httpErr != nil { return nil, cuteErrors.NewCuteError("[HTTP] Could not do request", httpErr) } // Add information (code, body, headers) about response to Allure step if addErr := it.addInformationResponse(t, resp); addErr != nil { // Ignore err return, because it's connected with test logic it.Error(t, "Could not log information about response. error %v", addErr) } if validErr := it.validateResponseCode(resp); validErr != nil { return resp, validErr } return resp, nil } func (it *Test) validateResponseCode(resp *http.Response) error { if it.Expect.Code != 0 && it.Expect.Code != resp.StatusCode { return cuteErrors.NewAssertError( "Assert response code", fmt.Sprintf("Response code expect %v, but was %v", it.Expect.Code, resp.StatusCode), resp.StatusCode, it.Expect.Code) } return nil } func (it *Test) addInformationRequest(t T, req *http.Request) error { var ( saveBody io.ReadCloser err error ) if it.RequestSanitizer != nil { it.RequestSanitizer(req) } it.lastRequestURL = req.URL.String() curl, err := http2curl.GetCurlCommand(req) if err != nil { return err } if c := curl.String(); len(c) <= 2048 { it.Info(t, "[Request] "+c) } else { it.Info(t, "[Request] Do request") } // Do not change to JSONMarshaler // In this case we can keep default for keep JSON, independence from JSONMarshaler headers, err := utils.ToJSON(req.Header) if err != nil { return err } t.WithParameters( allure.NewParameters( "method", req.Method, "host", req.Host, "headers", headers, "curl", curl.String(), )..., ) if req.Body != nil { saveBody, req.Body, err = utils.DrainBody(req.Body) if err != nil { return err } body, err := utils.GetBody(saveBody) if err != nil { return err } if len(body) != 0 { t.WithNewParameters("body", string(body)) } } return nil } func copyRequest(ctx context.Context, req *http.Request) (*http.Request, error) { var ( err error clone = req.Clone(ctx) ) req.Body, clone.Body, err = utils.DrainBody(req.Body) if err != nil { return nil, err } return clone, nil } func (it *Test) addInformationResponse(t T, response *http.Response) error { var ( saveBody io.ReadCloser err error ) if it.ResponseSanitizer != nil { it.ResponseSanitizer(response) } headers, _ := utils.ToJSON(response.Header) if headers != "" { t.WithNewParameters("response_headers", headers) } t.WithNewParameters("response_code", fmt.Sprint(response.StatusCode)) it.Info(t, "[Response] Status: "+response.Status) if response.Body == nil { return nil } saveBody, response.Body, err = utils.DrainBody(response.Body) // if could not get body from response, no add to allure if err != nil { return err } body, err := utils.GetBody(saveBody) // if could not get body from response, no add to allure if err != nil { return err } // if body is empty - skip if len(body) == 0 { return nil } responseType := allure.Text if _, ok := response.Header["Content-Type"]; ok { if len(response.Header["Content-Type"]) > 0 { if strings.Contains(response.Header["Content-Type"][0], "application/json") { responseType = allure.JSON } else { responseType = allure.MimeType(response.Header["Content-Type"][0]) } } } if responseType == allure.JSON { body, _ = utils.PrettyJSON(body) } t.WithAttachments(allure.NewAttachment("response", responseType, body)) return nil } func (it *Test) createTitle(try, countRepeat int, req *http.Request) string { toProcess := req // We have to execute sanitizer hook because // we need to log it and it can contain sensitive data if it.RequestSanitizer != nil { clone, err := copyRequest(req.Context(), req) // ignore error, because we want to log request // and it does not matter if we can copy request if err == nil { it.RequestSanitizer(clone) toProcess = clone } } title := toProcess.Method + " " + toProcess.URL.String() if countRepeat == 1 { return title } return fmt.Sprintf("[%v/%v] %v", try, countRepeat, title) } ================================================ FILE: step.go ================================================ package cute import ( "fmt" "github.com/ozontech/allure-go/pkg/allure" "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/cute/errors" ) func (it *Test) executeWithStep(t internalT, stepName string, execute func(t T) []error) []error { var ( errs []error ) // Add attempt indication in Allure if more than 1 attempt if it.Retry.MaxAttempts != 1 { stepName = fmt.Sprintf("[Attempt #%d] %v", it.Retry.currentCount, stepName) } t.WithNewStep(stepName, func(stepCtx provider.StepCtx) { errs = execute(stepCtx) processStepErrors(stepCtx, errs) }) return errs } func processStepErrors(stepCtx provider.StepCtx, errs []error) { var ( step = stepCtx.CurrentStep() statuses = make([]allure.Status, 0) ) if len(errs) == 0 { return } for _, err := range errs { currentStatus := allure.Failed currentStep := step if tErr, ok := err.(errors.OptionalError); ok { if tErr.IsOptional() { currentStatus = allure.Skipped } } if tErr, ok := err.(errors.BrokenError); ok { if tErr.IsBroken() { currentStatus = allure.Broken } } if tErr, ok := err.(errors.WithNameError); ok { currentStep = allure.NewSimpleStep(tErr.GetName()) currentStep.Status = currentStatus currentStep.WithParent(step) } if tErr, ok := err.(errors.WithFields); ok { for k, v := range tErr.GetFields() { if v == nil { continue } currentStep.WithNewParameters(k, v) } } if tErr, ok := err.(errors.WithAttachments); ok { for _, v := range tErr.GetAttachments() { if v == nil { continue } currentStep.WithAttachments(allure.NewAttachment(v.Name, allure.MimeType(v.MimeType), v.Content)) } } statuses = append(statuses, currentStatus) currentStep.WithAttachments(allure.NewAttachment("Error", allure.Text, []byte(err.Error()))) } // If one error was not optional, parent step should be failed for _, status := range statuses { step.Status = status if status == allure.Failed { break } } } ================================================ FILE: test.go ================================================ package cute import ( "bytes" "context" "errors" "fmt" "io" "mime/multipart" "net/http" "net/url" "os" "testing" "time" "github.com/ozontech/allure-go/pkg/framework/provider" cuteErrors "github.com/ozontech/cute/errors" "github.com/ozontech/cute/internal/utils" ) const ( defaultExecuteTestTime = 10 * time.Second defaultDelayRepeat = 1 * time.Second ) var ( errorRequestMethodEmpty = errors.New("request method must be not empty") errorRequestURLEmpty = errors.New("url request must be not empty") ) // RequestSanitizerHook is a function used to modify the request URL // before it is logged or attached to test reports (e.g., for hiding secrets). type RequestSanitizerHook func(req *http.Request) // ResponseSanitizerHook is a function used to modify the response // before it is logged or attached to test reports (e.g., for hiding secrets). type ResponseSanitizerHook func(resp *http.Response) // Test is a main struct of test. // You may field Request and Expect for create simple test // Parallel can be used to control the parallelism of a Test type Test struct { httpClient *http.Client jsonMarshaler JSONMarshaler lastRequestURL string Name string Parallel bool Retry *Retry AllureStep *AllureStep Middleware *Middleware Request *Request Expect *Expect RequestSanitizer RequestSanitizerHook ResponseSanitizer ResponseSanitizerHook } // Retry is a struct to control the retry of a whole single test (not only the request) // The test will be retried up to MaxAttempts times // The retries will only be executed if the test is having errors // If the test is successful at any iteration between attempt 1 and MaxAttempts, the loop will break and return the result as successful // The status of the test (success or fail) will be based on either the first attempt that is successful, or, if no attempt // is successful, it will be based on the latest execution // Delay is the number of seconds to wait before attempting to run the test again. It will only wait if Delay is set. type Retry struct { currentCount int MaxAttempts int Delay time.Duration } // Request is struct with HTTP request. // You may use your *http.Request or create new with help Builders type Request struct { Base *http.Request Builders []RequestBuilder Retry *RequestRetryPolitic } // RequestRetryPolitic is struct for repeat politic // if Optional is true and request is failed, than test step allure will be skip, and t.Fail() will not execute. // If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will not execute. // If Optional and Broken is false, than test step will be failed, and t.Fail() will execute. // If response.Code != Expect.Code, than request will repeat Count counts with Delay delay. type RequestRetryPolitic struct { Count int Delay time.Duration Optional bool Broken bool } // RequestRepeatPolitic is struct for repeat politic // Deprecated: use RequestRetryPolitic type RequestRepeatPolitic struct { Count int Delay time.Duration Optional bool Broken bool } // Middleware is struct for executeInsideAllure something before or after test type Middleware struct { After []AfterExecute AfterT []AfterExecuteT Before []BeforeExecute BeforeT []BeforeExecuteT } // AllureStep is struct with test name type AllureStep struct { Name string } // Expect is structs with validate politics for response type Expect struct { ExecuteTime time.Duration Code int JSONSchema *ExpectJSONSchema AssertBody []AssertBody AssertHeaders []AssertHeaders AssertResponse []AssertResponse AssertBodyT []AssertBodyT AssertHeadersT []AssertHeadersT AssertResponseT []AssertResponseT } // ExpectJSONSchema is structs with JSON politics for response type ExpectJSONSchema struct { String string Byte []byte File string } // Execute is common method for run test from builder func (it *Test) Execute(ctx context.Context, t tProvider) ResultsHTTPBuilder { var ( internalT allureProvider res ResultsHTTPBuilder ) if t == nil { panic("could not start test without testing.T") } stepCtx, isStepCtx := t.(provider.StepCtx) if isStepCtx { return it.executeInsideStep(ctx, stepCtx) } tOriginal, ok := t.(*testing.T) if ok { tOriginal.Helper() internalT = createAllureT(tOriginal) } allureT, ok := t.(provider.T) if ok { internalT = allureT } internalT.Run(it.Name, func(inT provider.T) { if it.Parallel { inT.Parallel() } res = it.executeInsideAllure(ctx, inT) }) return res } func (it *Test) clearFields() { it.AllureStep = new(AllureStep) it.Middleware = new(Middleware) it.Expect = new(Expect) it.Request = new(Request) it.Request.Retry = new(RequestRetryPolitic) it.Expect.JSONSchema = new(ExpectJSONSchema) } func (it *Test) initEmptyFields() { if it.httpClient == nil { it.httpClient = http.DefaultClient } if it.jsonMarshaler == nil { it.jsonMarshaler = jsonMarshaler{} } if it.AllureStep == nil { it.AllureStep = new(AllureStep) } if it.Middleware == nil { it.Middleware = new(Middleware) } if it.Expect == nil { it.Expect = new(Expect) } if it.Request == nil { it.Request = new(Request) } if it.Request.Retry == nil { it.Request.Retry = new(RequestRetryPolitic) } if it.Expect.JSONSchema == nil { it.Expect.JSONSchema = new(ExpectJSONSchema) } if it.Retry == nil { it.Retry = &Retry{ // we set the default value to 1, because we count the first attempt as 1 MaxAttempts: 1, currentCount: 1, } } // We need to set the current count to 1 here, because we count the first attempt as 1 it.Retry.currentCount = 1 } // executeInsideStep is method for start test with provider.StepCtx // It's test inside the step func (it *Test) executeInsideStep(ctx context.Context, t internalT) ResultsHTTPBuilder { // Set empty fields in test it.initEmptyFields() // we don't want to defer the finish message, because it will be logged in processTestErrors it.Info(t, "Start test") return it.startRepeatableTest(ctx, t) } func (it *Test) executeInsideAllure(ctx context.Context, allureProvider allureProvider) ResultsHTTPBuilder { // Set empty fields in test it.initEmptyFields() // we don't want to defer the finish message, because it will be logged in processTestErrors it.Info(allureProvider, "Start test") if it.AllureStep.Name != "" { // Execute test inside step return it.startTestInsideStep(ctx, allureProvider) } else { // Execute Test return it.startRepeatableTest(ctx, allureProvider) } } // startRepeatableTest is method for start test with repeatable execution func (it *Test) startRepeatableTest(ctx context.Context, t internalT) ResultsHTTPBuilder { var ( resp *http.Response errs []error resultState ResultState ) for ; it.Retry.currentCount <= it.Retry.MaxAttempts; it.Retry.currentCount++ { resp, errs = it.startTest(ctx, t) resultState = it.processTestErrors(t, errs) // we don't want to keep errors if we will retry test // we have to return to user only errors from last try // if the test is successful, we break the loop if resultState == ResultStateSuccess { break } // if we have a delay, we wait before the next attempt // and we only wait if we are not at the last attempt if it.Retry.currentCount != it.Retry.MaxAttempts && it.Retry.Delay != 0 { it.Info(t, "The test had errors, retrying...") time.Sleep(it.Retry.Delay) } } switch resultState { case ResultStateBroken: t.BrokenNow() it.Info(t, "Test broken") case ResultStateFail: t.Fail() it.Error(t, "Test failed") case resultStateFailNow: t.FailNow() it.Error(t, "Test failed") case ResultStateSuccess: it.Info(t, "Test finished successfully") } return newTestResult(it.Name, resp, resultState, errs) } func (it *Test) startTestInsideStep(ctx context.Context, t internalT) ResultsHTTPBuilder { var ( result ResultsHTTPBuilder ) t.WithNewStep(it.AllureStep.Name, func(stepCtx provider.StepCtx) { it.Info(t, "Start step %v", it.AllureStep.Name) defer it.Info(t, "Finish step %v", it.AllureStep.Name) result = it.startRepeatableTest(ctx, stepCtx) if result.GetResultState() == ResultStateFail { stepCtx.Fail() } }) return result } // processTestErrors returns flag, which mean finish test or not. // If test has only optional errors, than test will be success // If test has broken errors, than test will be broken on allure // If test has require errors, than test will be failed on allure func (it *Test) processTestErrors(t internalT, errs []error) ResultState { if len(errs) == 0 { it.Info(t, "Test finished successfully") return ResultStateSuccess } var ( countNotOptionalErrors = 0 state ResultState ) for _, err := range errs { message := fmt.Sprintf("error %v", err.Error()) if tErr, ok := err.(cuteErrors.OptionalError); ok { if tErr.IsOptional() { it.Info(t, "[OPTIONAL ERROR] %v", err.Error()) state = ResultStateSuccess continue } } if tErr, ok := err.(cuteErrors.BrokenError); ok { if tErr.IsBroken() { it.Error(t, "[BROKEN ERROR], error %v", err.Error()) state = ResultStateBroken continue } } if tErr, ok := err.(cuteErrors.RequireError); ok { if tErr.IsRequire() { state = resultStateFailNow } } if tErr, ok := err.(cuteErrors.WithFields); ok { actual := tErr.GetFields()[cuteErrors.ActualField] expected := tErr.GetFields()[cuteErrors.ExpectedField] if actual != nil || expected != nil { message = fmt.Sprintf("%s\nActual %v\nExpected %v", message, actual, expected) } } it.Error(t, message) countNotOptionalErrors++ } if countNotOptionalErrors != 0 { state = ResultStateFail it.Error(t, "Test finished with %v errors", countNotOptionalErrors) } return state } func (it *Test) startTest(ctx context.Context, t internalT) (*http.Response, []error) { var ( resp *http.Response err error ) // CreateWithStep executeInsideAllure timer if it.Expect.ExecuteTime == 0 { it.Expect.ExecuteTime = defaultExecuteTestTime } ctx, cancel := context.WithTimeout(ctx, it.Expect.ExecuteTime) defer cancel() // CreateWithStep request req, err := it.createRequest(ctx) if err != nil { return nil, []error{err} } // Execute Before if errs := it.beforeTest(t, req); len(errs) > 0 { return nil, errs } it.Info(t, "Start make request") // Make request resp, errs := it.makeRequest(t, req) if len(errs) > 0 { return resp, errs } it.Info(t, "Finish make request") // Validate response body errs = it.validateResponse(t, resp) // Execute After afterTestErrs := it.afterTest(t, resp, errs) // Return results errs = append(errs, afterTestErrs...) if len(errs) > 0 { return resp, errs } return resp, nil } func (it *Test) afterTest(t internalT, resp *http.Response, errs []error) []error { if len(it.Middleware.After) == 0 && len(it.Middleware.AfterT) == 0 { return nil } return it.executeWithStep(t, "After", func(t T) []error { scope := make([]error, 0) for _, execute := range it.Middleware.After { if err := execute(resp, errs); err != nil { scope = append(scope, err) } } for _, executeSuite := range it.Middleware.AfterT { if err := executeSuite(t, resp, errs); err != nil { scope = append(scope, err) } } return scope }) } func (it *Test) beforeTest(t internalT, req *http.Request) []error { if len(it.Middleware.Before) == 0 && len(it.Middleware.BeforeT) == 0 { return nil } return it.executeWithStep(t, "Before", func(t T) []error { scope := make([]error, 0) for _, execute := range it.Middleware.Before { if err := execute(req); err != nil { scope = append(scope, err) } } for _, executeT := range it.Middleware.BeforeT { if err := executeT(t, req); err != nil { scope = append(scope, err) } } return scope }) } // createRequest builds the final *http.Request to be executed by the test. func (it *Test) createRequest(ctx context.Context) (*http.Request, error) { var ( req = it.Request.Base err error ) if req == nil { req, err = it.buildRequest(ctx) if err != nil { return nil, err } } // Validate Request if err := it.validateRequest(req); err != nil { return nil, err } return req, nil } // buildRequest // Priority for create body: // 1. requestOptions.body <- low priority // 2. requestOptions.bodyMarshal // 3. requestOptions.forms and requestOptions.fileForms <- high priority. func (it *Test) buildRequest(ctx context.Context) (*http.Request, error) { var ( req *http.Request err error o = newRequestOptions() ) // Set builder parameters for _, builder := range it.Request.Builders { builder(o) } reqURL := o.url if reqURL == nil { reqURL, err = url.Parse(o.uri) if err != nil { return nil, err } } // Set query parameters query := reqURL.Query() for key, values := range o.query { for _, value := range values { query.Add(key, value) } } reqURL.RawQuery = query.Encode() // Set body body := o.body if o.bodyMarshal != nil { body, err = it.jsonMarshaler.Marshal(o.bodyMarshal) if err != nil { return nil, err } } // Set multipart if len(o.fileForms) != 0 || len(o.forms) != 0 { var ( buffer = new(bytes.Buffer) mp = multipart.NewWriter(buffer) ) // set file forms for fieldName, file := range o.fileForms { err = createFormFile(mp, fieldName, file) if err != nil { return nil, err } } // set forms for fieldName, fieldBody := range o.forms { err = createFormField(mp, fieldName, fieldBody) if err != nil { return nil, err } } if err = mp.Close(); err != nil { return nil, err } req, err = http.NewRequestWithContext(ctx, o.method, reqURL.String(), buffer) if err != nil { return nil, err } req.Header.Add("Content-Type", mp.FormDataContentType()) } else { req, err = http.NewRequestWithContext(ctx, o.method, reqURL.String(), io.NopCloser(bytes.NewReader(body))) if err != nil { return nil, err } } // Set headers for nameHeader, valuesHeader := range o.headers { req.Header[nameHeader] = valuesHeader } return req, nil } func createFormFile(mp *multipart.Writer, fieldName string, file *File) error { var ( data = file.Body name = file.Name ) // read file, if path is not empty if len(file.Path) != 0 { f, err := os.Open(file.Path) if err != nil { return err } data, err = io.ReadAll(f) if err != nil { return err } name = f.Name() } field, err := mp.CreateFormFile(fieldName, name) if err != nil { return fmt.Errorf("error when creating %v file form field, %w", fieldName, err) } _, err = field.Write(data) if err != nil { return fmt.Errorf("error when writing %v file form field, %w", fieldName, err) } return nil } func createFormField(mp *multipart.Writer, fieldName string, body []byte) error { field, err := mp.CreateFormField(fieldName) if err != nil { return fmt.Errorf("error when creating %v form field, %w", fieldName, err) } _, err = field.Write(body) if err != nil { return fmt.Errorf("error when writing %v form field, %w", fieldName, err) } return nil } func (it *Test) validateRequest(req *http.Request) error { if req.URL == nil { return errorRequestURLEmpty } if req.Method == "" { return errorRequestMethodEmpty } return nil } func (it *Test) validateResponse(t internalT, resp *http.Response) []error { var ( err error saveBody io.ReadCloser scope = make([]error, 0) ) // Execute asserts for headers if errs := it.assertHeaders(t, resp.Header); len(errs) > 0 { scope = append(scope, errs...) } // Prepare body for validate if resp.Body == nil { // todo create errors if body is empty, but assert is not empty return scope } saveBody, resp.Body, err = utils.DrainBody(resp.Body) if err != nil { return append(scope, fmt.Errorf("could not drain response body. error %w", err)) } body, err := utils.GetBody(saveBody) if err != nil { return append(scope, fmt.Errorf("could not get response body. error %w", err)) } // Execute asserts for body if errs := it.assertBody(t, body); len(errs) > 0 { // add assert scope = append(scope, errs...) } // Validate response by json schema if errs := it.validateJSONSchema(t, body); len(errs) > 0 { scope = append(scope, errs...) } // Execute asserts for response body if errs := it.assertResponse(t, resp); len(errs) > 0 { scope = append(scope, errs...) } return scope } ================================================ FILE: test_test.go ================================================ package cute import ( "bytes" "context" "errors" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/ozontech/allure-go/pkg/framework/core/common" "github.com/stretchr/testify/require" "github.com/ozontech/cute/internal/utils" ) func TestCreateRequest(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://go.com", nil) require.NoError(t, err) ht := &Test{ Request: &Request{ Base: req, }, } resReq, err := ht.createRequest(context.Background()) require.NoError(t, err) require.Equal(t, req, resReq) } func TestCreateRequestBuilder(t *testing.T) { var ( body = []byte("HELLO") headers = map[string][]string{ "Good": []string{"Day"}, "Mad": []string{"Max"}, } url = "http://go.com" ) req, err := http.NewRequest(http.MethodGet, url, io.NopCloser(bytes.NewReader(body))) require.NoError(t, err) req.Header = headers ht := &Test{ Request: &Request{ Builders: []RequestBuilder{ WithURI(url), WithMethod("GET"), WithHeaders(headers), WithBody(body), }, }, } resReq, err := ht.createRequest(context.Background()) require.NoError(t, err) require.Equal(t, req, resReq) } func TestCreateRequestBuilder_MarshalBody(t *testing.T) { var ( str = struct { name string }{ "hello", } ) ht := &Test{ jsonMarshaler: jsonMarshaler{}, Request: &Request{ Builders: []RequestBuilder{ WithMarshalBody(str), }, }, } resReq, err := ht.createRequest(context.Background()) require.NoError(t, err) getBody, err := utils.GetBody(resReq.Body) require.NoError(t, err) require.NotEmpty(t, getBody) } func TestValidateRequestEmptyUrl(t *testing.T) { ht := &Test{} require.Error(t, ht.validateRequest(&http.Request{})) } func TestValidateRequestEmptyMethod(t *testing.T) { ht := &Test{} u, _ := url.Parse("https://go.com") require.Error(t, ht.validateRequest(&http.Request{ URL: u, })) } func TestValidateResponseEmpty(t *testing.T) { ht := &Test{ Expect: new(Expect), } temp := common.NewT(t) errs := ht.validateResponse(temp, &http.Response{}) require.Empty(t, errs) } func TestValidateResponseCode(t *testing.T) { ht := &Test{ Expect: &Expect{Code: 200}, } temp := common.NewT(t) errs := ht.validateResponse(temp, &http.Response{StatusCode: http.StatusOK}) require.Empty(t, errs) } func TestValidateResponseWithErrors(t *testing.T) { var ( ht = &Test{ Expect: &Expect{ Code: 200, JSONSchema: new(ExpectJSONSchema), AssertHeaders: []AssertHeaders{ func(headers http.Header) error { return errors.New("two error") }, }, AssertResponse: []AssertResponse{ func(response *http.Response) error { if response.StatusCode != http.StatusOK || len(response.Header["auth"]) == 0 { //nolint return errors.New("bad response") } return nil }, }, }, } reader = bytes.NewReader([]byte(`{"a":"ab","b":"bc"}`)) temp = createAllureT(t) resp = &http.Response{ StatusCode: http.StatusBadRequest, Header: map[string][]string{ "key": []string{"value"}, "auth": []string{"sometoken"}, }, Body: io.NopCloser(reader), } ) ht.initEmptyFields() errs := ht.validateResponse(temp, resp) require.Len(t, errs, 2) } type mockRoundTripper struct{} func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 200, Request: req, Body: io.NopCloser(strings.NewReader("mock response")), }, nil } func TestSanitizeURLHook(t *testing.T) { client := &http.Client{ Transport: &mockRoundTripper{}, } test := &Test{ httpClient: client, Retry: &Retry{ currentCount: 0, MaxAttempts: 0, Delay: 0, }, Request: &Request{ Builders: []RequestBuilder{ WithMethod(http.MethodGet), WithURI("http://localhost/api?key=123"), }, Retry: &RequestRetryPolitic{ Count: 1, Delay: 2, }, }, RequestSanitizer: sanitizeKeyParam("****"), } req, err := test.createRequest(context.Background()) require.NoError(t, err) require.NotNil(t, req) newT := createAllureT(t) err = test.addInformationRequest(newT, req) require.NoError(t, err) decodedQuery, err := url.QueryUnescape(req.URL.RawQuery) require.NoError(t, err) require.Equal(t, "key=****", decodedQuery) } func TestSanitizeURL_LastRequestURL(t *testing.T) { client := &http.Client{ Transport: &mockRoundTripper{}, } test := &Test{ httpClient: client, Request: &Request{ Builders: []RequestBuilder{ WithMethod(http.MethodGet), WithURI("http://localhost/api?key=123"), }, }, RequestSanitizer: sanitizeKeyParam("****"), } allureT := createAllureT(t) test.Execute(context.Background(), allureT) decodedURL, err := url.QueryUnescape(test.lastRequestURL) require.NoError(t, err) require.Contains(t, decodedURL, "key=****", "Expected masked key in lastRequestURL") } func sanitizeKeyParam(mask string) RequestSanitizerHook { return func(req *http.Request) { q := req.URL.Query() q.Set("key", mask) req.URL.RawQuery = q.Encode() } } func TestSanitizeURL_RealRequest(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) t.Logf("Server received URL: %s, Body: %s", r.URL.String(), string(body)) require.Contains(t, r.URL.String(), "key=123", "Sanitizer must not change real request") w.WriteHeader(200) })) defer ts.Close() client := &http.Client{} test := &Test{ httpClient: client, Request: &Request{ Builders: []RequestBuilder{ WithMethod(http.MethodGet), WithURI(ts.URL + "/api?key=123"), }, }, RequestSanitizer: sanitizeKeyParam("****"), } allureT := createAllureT(t) test.Execute(context.Background(), allureT) }