diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..b71f55f --- /dev/null +++ b/.drone.yml @@ -0,0 +1,14 @@ +kind: pipeline +type: docker +name: pipeline + +steps: + - name: lint + image: golang + commands: + - make lint + + - name: test with race and cover + image: golang + commands: + - make test \ No newline at end of file diff --git a/.gitignore b/.gitignore index adf8f72..3ab39f3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ # Go workspace file go.work +bin +.idea +.vscode + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c7f64ea --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +VERSION=$(shell date +%Y.%m) +PROJECT_NAME=w3back + +.PHONY: lint +lint: ## Линт всего проекта + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.51.1 + golangci-lint run --config=./golangci.yml ./... + +.PHONY: test +test: ## Юнит тестирование всего проекта + go test -race -count 100 -timeout 30s ./... + +help: ## Print this help and exit + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..6904a73 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,93 @@ +package main + +// validate doc - https://pkg.go.dev/github.com/go-playground/validator/v10 + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + + "github.com/tiburon-777/api-crowler/internal/config" + "github.com/tiburon-777/api-crowler/internal/scenario" +) + +//nolint:gocognit,gocyclo,cyclop +func main() { + // Парсим флаги приложения. + conf, err := config.Get() + if err != nil { + log.Fatalf("invalid config: %v ", err) + } + + // Читаем файл сценария. + scenarios, err := scenario.Get(conf.Scenario) + if err != nil { + log.Fatalf("invalid scenario: %v ", err) + } + + ctx := context.Background() + cli := http.Client{Timeout: scenarios.Params.Timeout} + + // Запускаем сценарий + for key, step := range scenarios.Steps { + log.Printf("Step#%d - %s", key, step.Name) + req, err := http.NewRequestWithContext( + ctx, + step.Query.Method, + fmt.Sprintf("%s%s", scenarios.Params.Address, step.Query.URL), + strings.NewReader(step.Query.Data), + ) + if err != nil { + log.Printf("request error: %v ", err) + } + defer req.Body.Close() + for hKey, hVal := range step.Query.Headers { + req.Header.Add(hKey, hVal) + } + resp, err := cli.Do(req) + if err != nil { + log.Printf("client error: %v ", err) + } + defer resp.Body.Close() + var body []byte + if _, err = resp.Body.Read(body); err != nil { + log.Printf("client read body: %v ", err) + } + if step.Response.Code != 0 && resp.StatusCode != step.Response.Code { + log.Printf("wrong response code: %v ", err) + } + //nolint:nestif + if len(step.Response.Body) != 0 { + for _, sample := range step.Response.Body { + switch sample.Function { + case "contains": + if !strings.Contains(string(body), sample.Text) { + log.Printf("wrong body") + } + case "not_contains": + if strings.Contains(string(body), sample.Text) { + log.Printf("wrong body") + } + case "begin": + if !strings.HasPrefix(string(body), sample.Text) { + log.Printf("wrong body") + } + case "not_begin": + if strings.HasPrefix(string(body), sample.Text) { + log.Printf("wrong body") + } + case "ends": + if !strings.HasSuffix(string(body), sample.Text) { + log.Printf("wrong body") + } + case "not_ends": + if strings.HasSuffix(string(body), sample.Text) { + log.Printf("wrong body") + } + } + } + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3bf5026 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/tiburon-777/api-crowler + +go 1.18 + +require ( + github.com/go-playground/validator/v10 v10.11.2 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + golang.org/x/crypto v0.5.0 // indirect + golang.org/x/sys v0.4.0 // indirect + golang.org/x/text v0.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9544376 --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= +github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +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/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/golangci.yml b/golangci.yml new file mode 100644 index 0000000..9b45172 --- /dev/null +++ b/golangci.yml @@ -0,0 +1,111 @@ +# This file contains all available configuration options +# with their default values. + +# options for analysis running +run: + # default concurrency is a available CPU number + #concurrency: 4 + + # timeout for analysis, e.g. 30s, 5m, default is 1m + deadline: 30m + + # include test files or not, default is true + tests: false + + # which dirs to skip: they won't be analyzed; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but next dirs are always skipped independently + # from this option's value: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs: + - bin$ + - \.git$ + - etc$ + - protobuf$ + - scripts$ + - vendor$ + - ^benches/ + + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. + skip-files: + - "_easyjson.go" + - ".pb.go" + + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + modules-download-mode: mod + +# all available settings of specific linters +linters-settings: + errcheck: + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: true + govet: + # report about shadowed variables + check-shadowing: true + golint: + # minimal confidence for issues, default is 0.8 + min-confidence: 0.3 + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 20 + dupl: + # tokens count to trigger issue, 150 by default + threshold: 200 + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 150 + funlen: + statements: 50 + lines: 150 + +linters: + enable-all: true + disable: + - gci + - exhaustivestruct + - exhaustruct + - gochecknoglobals + - whitespace + - wsl + - wrapcheck + - nlreturn + - gofmt + - gofumpt + - varcheck + - golint + - structcheck + - nosnakecase + - maligned + - interfacer + - deadcode + - scopelint + - ifshort + fast: false + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + + exclude-rules: + # Exclude lll issues for long lines with go:generate + - linters: + - lll + source: "^//go:generate " + - linters: + - golint + text: "receiver name should be a reflection of its identity" + +output: + format: tab diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..b88bd92 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,23 @@ +package config + +import ( + "flag" + + "github.com/go-playground/validator/v10" +) + +type Config struct { + Scenario string `validate:"required"` +} + +func Get() (*Config, error) { + conf := &Config{} + flag.StringVar(&conf.Scenario, "scenario", "", "URL base") + flag.Parse() + + // Валидируем параметры + if err := validator.New().Struct(conf); err != nil { + return nil, err + } + return conf, nil +} diff --git a/internal/scenario/models.go b/internal/scenario/models.go new file mode 100644 index 0000000..ddbcd06 --- /dev/null +++ b/internal/scenario/models.go @@ -0,0 +1,37 @@ +package scenario + +import "time" + +type Object struct { + Params Params `yaml:"params" validate:"required"` + Steps []Step `yaml:"steps" validate:"required,dive"` +} + +type Params struct { + Address string `yaml:"address" validate:"required"` + Timeout time.Duration `yaml:"timeout" validate:"required"` +} + +type Step struct { + Name string `yaml:"name" validate:"required"` + Query Query `yaml:"query" validate:"required"` + Response Response `yaml:"response" validate:"required"` +} + +type Query struct { + Method string `yaml:"method" validate:"required"` + URL string `yaml:"url" validate:"required"` + Headers map[string]string `yaml:"headers"` + Data string `yaml:"data"` +} + +type Response struct { + Code int `yaml:"code" validate:"required_without=Body"` + Headers map[string]string `yaml:"headers"` + Body []Sample `yaml:"body" validate:"required_without=Code"` +} + +type Sample struct { + Function string `yaml:"function"` + Text string `yaml:"text"` +} diff --git a/internal/scenario/scenario.go b/internal/scenario/scenario.go new file mode 100644 index 0000000..f41866b --- /dev/null +++ b/internal/scenario/scenario.go @@ -0,0 +1,26 @@ +package scenario + +import ( + "os" + + "github.com/go-playground/validator/v10" + "gopkg.in/yaml.v2" +) + +func Get(file string) (*Object, error) { + var obj Object + yamlFile, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + err = yaml.Unmarshal(yamlFile, &obj) + if err != nil { + return nil, err + } + + if err := validator.New().Struct(obj); err != nil { + return nil, err + } + return &obj, nil +} diff --git a/test/test.yaml b/test/test.yaml new file mode 100644 index 0000000..f560300 --- /dev/null +++ b/test/test.yaml @@ -0,0 +1,100 @@ +{ + params: { + address: "http://localhost:9090", + timeout: 30s, + }, + steps: [ + { + name: "1.1 - попытка получить флаг intra_domain_share для несуществующего пользователя", + query: { + method: "GET", + url: "/api/v1/user/1/policy/ids", + headers: { + X-Request-ID: "1.1", + Content-Type: "application/json", + }, + }, + response: { + code: 404 + } + }, + { + name: "1.2 - попытка установить флаг intra_domain_share для несуществующего пользователя", + query: { + method: "POST", + url: "/api/v1/user/1/policy/ids", + headers: { + X-Request-ID: "1.2", + Content-Type: "application/json", + }, + data: '{"PID":1,"intra_domain_share":true}', + }, + response: { + code: 200, + } + }, + { + name: "1.3 - попытка получить флаг intra_domain_share для пользователя, созданного в 1.2", + query: { + method: "GET", + url: "/api/v1/user/1/policy/ids", + headers: { + X-Request-ID: "1.3", + Content-Type: "application/json", + }, + }, + response: { + code: 200, + body: [ + { + function: "contains", + text: "intra_domain_share" + }, + { + function: "contains", + text: "true" + }, + ] + } + }, + { + name: "1.4 - попытка установить флаг intra_domain_share для массива несуществующих пользователей", + query: { + method: "POST", + url: "/api/v1/users/policy/ids", + headers: { + X-Request-ID: "1.4", + Content-Type: "application/json", + }, + data: '{"users":[1,2],"pid":1,"intra_domain_share":true}', + }, + response: { + code: 200, + } + }, + { + name: "1.5 - попытка получить флаг intra_domain_share для одного из пользователей, созданных в 1.4", + query: { + method: "GET", + url: "/api/v1/user/2/policy/ids", + headers: { + X-Request-ID: "1.5", + Content-Type: "application/json", + }, + }, + response: { + code: 200, + body: [ + { + function: "contains", + text: "intra_domain_share" + }, + { + function: "contains", + text: "true" + }, + ] + } + }, + ] +} \ No newline at end of file