diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..9febe6e --- /dev/null +++ b/.drone.yml @@ -0,0 +1,14 @@ +kind: pipeline +type: docker +name: pipeline + +steps: + - name: lint + image: golang:1.20 + commands: + - make lint + + - name: test with race and cover + image: golang:1.20 + 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..f4f7464 --- /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" + + "git.tiburon.su/OCTOPUS/api-crowler/internal/config" + "git.tiburon.su/OCTOPUS/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..0d486b9 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module git.tiburon.su/OCTOPUS/api-crowler + +go 1.20 + +require ( + github.com/go-playground/validator/v10 v10.12.0 + 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/leodido/go-urn v1.2.3 // indirect + golang.org/x/crypto v0.8.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3fe56ca --- /dev/null +++ b/go.sum @@ -0,0 +1,34 @@ +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-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.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= +github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= +github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= +github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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