From 979ced46d53cfc24d44e4f74f150756d3b6f0cd4 Mon Sep 17 00:00:00 2001 From: sabevzenko Date: Fri, 15 Dec 2023 15:20:22 +0300 Subject: [PATCH] http-scenario refactoring & tests --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 5 +- .mapping.json | 2 + .../providers/http_scenario/ammo_hcl.go | 212 +++---------- .../providers/http_scenario/ammo_hcl_test.go | 65 ++-- components/providers/http_scenario/decode.go | 19 +- .../decode_sample_config_test.golden.hcl | 19 +- .../decode_sample_config_test.hcl | 12 +- .../providers/http_scenario/provider.go | 2 +- docs/eng/scenario-http-generator.md | 5 +- docs/rus/scenario-http-generator.md | 5 +- examples/http/server/server.go | 296 ++++++++++++++++++ examples/http/server/stats.go | 101 ++++++ 13 files changed, 510 insertions(+), 235 deletions(-) create mode 100644 examples/http/server/server.go create mode 100644 examples/http/server/stats.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6958e948f..af94d55f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: 1.20.x + go-version: 1.21.x cache: true - name: Test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fdfb33443..320827441 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,9 +4,11 @@ on: push: branches: - master + - dev pull_request: branches: - master + - dev jobs: run-unit-tests: @@ -17,7 +19,6 @@ jobs: strategy: fail-fast: false matrix: - go-version: [1.20.x, 1.21.x] os: [ubuntu, macOS] env: OS: ${{ matrix.os }}-latest @@ -30,7 +31,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: ${{ matrix.go-version }} + go-version: 1.21.x cache: true - name: Test diff --git a/.mapping.json b/.mapping.json index 0f902ec11..76a01c6f0 100644 --- a/.mapping.json +++ b/.mapping.json @@ -253,6 +253,8 @@ "examples/debug_and_profiling.yaml":"load/projects/pandora/examples/debug_and_profiling.yaml", "examples/http.jsonline":"load/projects/pandora/examples/http.jsonline", "examples/http.yaml":"load/projects/pandora/examples/http.yaml", + "examples/http/server/server.go":"load/projects/pandora/examples/http/server/server.go", + "examples/http/server/stats.go":"load/projects/pandora/examples/http/server/stats.go", "go.mod":"load/projects/pandora/gomod/go.mod", "go.sum":"load/projects/pandora/gomod/go.sum", "gomod/go.mod":"load/projects/pandora/gomod/go.mod", diff --git a/components/providers/http_scenario/ammo_hcl.go b/components/providers/http_scenario/ammo_hcl.go index b7e1a7e4b..05af53709 100644 --- a/components/providers/http_scenario/ammo_hcl.go +++ b/components/providers/http_scenario/ammo_hcl.go @@ -8,10 +8,11 @@ import ( "github.com/spf13/afero" "github.com/yandex/pandora/components/providers/http_scenario/postprocessor" "github.com/yandex/pandora/lib/str" + "gopkg.in/yaml.v2" ) type AmmoHCL struct { - VariableSources []SourceHCL `hcl:"variable_source,block"` + VariableSources []SourceHCL `hcl:"variable_source,block" config:"variable_sources" yaml:"variable_sources"` Requests []RequestHCL `hcl:"request,block"` Scenarios []ScenarioHCL `hcl:"scenario,block"` } @@ -19,30 +20,30 @@ type AmmoHCL struct { type SourceHCL struct { Name string `hcl:"name,label"` Type string `hcl:"type,label"` - File *string `hcl:"file"` - Fields *[]string `hcl:"fields"` - IgnoreFirstLine *bool `hcl:"ignore_first_line"` - Delimiter *string `hcl:"delimiter"` - Variables *map[string]string `hcl:"variables"` + File *string `hcl:"file" yaml:"file,omitempty"` + Fields *[]string `hcl:"fields" yaml:"fields,omitempty"` + IgnoreFirstLine *bool `hcl:"ignore_first_line" yaml:"ignore_first_line,omitempty"` + Delimiter *string `hcl:"delimiter" yaml:"delimiter,omitempty"` + Variables *map[string]string `hcl:"variables" yaml:"variables,omitempty"` } type RequestHCL struct { Name string `hcl:"name,label"` Method string `hcl:"method"` - Headers map[string]string `hcl:"headers"` - Tag *string `hcl:"tag"` - Body *string `hcl:"body"` URI string `hcl:"uri"` - Preprocessor *PreprocessorHCL `hcl:"preprocessor,block"` - Postprocessors []PostprocessorHCL `hcl:"postprocessor,block"` - Templater *string `hcl:"templater"` + Headers map[string]string `hcl:"headers" yaml:"headers,omitempty"` + Tag *string `hcl:"tag" yaml:"tag,omitempty"` + Body *string `hcl:"body" yaml:"body,omitempty"` + Preprocessor *PreprocessorHCL `hcl:"preprocessor,block" yaml:"preprocessor,omitempty"` + Postprocessors []PostprocessorHCL `hcl:"postprocessor,block" yaml:"postprocessors,omitempty"` + Templater *TemplaterHCL `hcl:"templater,block" yaml:"templater,omitempty"` } type ScenarioHCL struct { Name string `hcl:"name,label"` - Weight *int64 `hcl:"weight"` - MinWaitingTime *int64 `hcl:"min_waiting_time"` - Requests []string `hcl:"requests"` + Weight *int64 `hcl:"weight" yaml:"weight,omitempty"` + MinWaitingTime *int64 `hcl:"min_waiting_time" config:"min_waiting_time" yaml:"min_waiting_time,omitempty"` + Requests []string `hcl:"requests" yaml:"requests"` } type AssertSizeHCL struct { @@ -52,15 +53,19 @@ type AssertSizeHCL struct { type PostprocessorHCL struct { Type string `hcl:"type,label"` - Mapping *map[string]string `hcl:"mapping"` - Headers *map[string]string `hcl:"headers"` - Body *[]string `hcl:"body"` - StatusCode *int `hcl:"status_code"` - Size *AssertSizeHCL `hcl:"size,block"` + Mapping *map[string]string `hcl:"mapping" yaml:"mapping,omitempty"` + Headers *map[string]string `hcl:"headers" yaml:"headers,omitempty"` + Body *[]string `hcl:"body" yaml:"body,omitempty"` + StatusCode *int `hcl:"status_code" yaml:"status_code,omitempty"` + Size *AssertSizeHCL `hcl:"size,block" yaml:"size,omitempty"` } type PreprocessorHCL struct { - Mapping map[string]string `hcl:"mapping"` + Mapping map[string]string `hcl:"mapping" yaml:"mapping,omitempty"` +} + +type TemplaterHCL struct { + Type string `hcl:"type" yaml:"type"` } func ParseHCLFile(file afero.File) (AmmoHCL, error) { @@ -78,164 +83,17 @@ func ParseHCLFile(file afero.File) (AmmoHCL, error) { return config, nil } -func ConvertHCLToAmmo(ammo AmmoHCL, fs afero.Fs) (AmmoConfig, error) { +func ConvertHCLToAmmo(ammo AmmoHCL) (AmmoConfig, error) { const op = "scenario.ConvertHCLToAmmo" - - var sources []VariableSource - if len(ammo.VariableSources) > 0 { - sources = make([]VariableSource, len(ammo.VariableSources)) - for i, s := range ammo.VariableSources { - file := "" - if s.File != nil { - file = *s.File - } - switch s.Type { - case "variables": - if s.Variables == nil { - return AmmoConfig{}, fmt.Errorf("%s, variables cant be nil: %s", op, s.Type) - } - vars := make(map[string]any, len(*s.Variables)) - for k, v := range *s.Variables { - vars[k] = v - } - sources[i] = &VariableSourceVariables{ - Name: s.Name, - Variables: vars, - } - case "file/json": - sources[i] = &VariableSourceJSON{ - Name: s.Name, - File: file, - fs: fs, - } - case "file/csv": - var fields []string - if s.Fields != nil { - fields = make([]string, len(*s.Fields)) - copy(fields, *s.Fields) - } - skipHeader := false - if s.IgnoreFirstLine != nil { - skipHeader = *s.IgnoreFirstLine - } - headerAsFields := "" - if s.Delimiter != nil { - headerAsFields = *s.Delimiter - } - sources[i] = &VariableSourceCsv{ - Name: s.Name, - File: file, - Fields: fields, - IgnoreFirstLine: skipHeader, - Delimiter: headerAsFields, - fs: fs, - } - default: - return AmmoConfig{}, fmt.Errorf("%s, unknown variable source type: %s", op, s.Type) - } - } - } - - var requests []RequestConfig - if len(ammo.Requests) > 0 { - requests = make([]RequestConfig, len(ammo.Requests)) - for i, r := range ammo.Requests { - var postprocessors []postprocessor.Postprocessor - if len(r.Postprocessors) > 0 { - postprocessors = make([]postprocessor.Postprocessor, len(r.Postprocessors)) - for j, p := range r.Postprocessors { - switch p.Type { - case "var/header": - postprocessors[j] = &postprocessor.VarHeaderPostprocessor{ - Mapping: *p.Mapping, - } - case "var/xpath": - postprocessors[j] = &postprocessor.VarXpathPostprocessor{ - Mapping: *p.Mapping, - } - case "var/jsonpath": - postprocessors[j] = &postprocessor.VarJsonpathPostprocessor{ - Mapping: *p.Mapping, - } - case "assert/response": - postp := &postprocessor.AssertResponse{} - if p.Headers != nil { - postp.Headers = *p.Headers - } - if p.Body != nil { - postp.Body = *p.Body - } - if p.StatusCode != nil { - postp.StatusCode = *p.StatusCode - } - if p.Size != nil { - postp.Size = &postprocessor.AssertSize{} - if p.Size.Val != nil { - postp.Size.Val = *p.Size.Val - } - if p.Size.Op != nil { - postp.Size.Op = *p.Size.Op - } - } - if err := postp.Validate(); err != nil { - return AmmoConfig{}, fmt.Errorf("%s, invalid postprocessor.AssertResponse %w", op, err) - } - postprocessors[j] = postp - default: - return AmmoConfig{}, fmt.Errorf("%s, unknown postprocessor type: %s", op, p.Type) - } - } - } - templater := NewTextTemplater() - if r.Templater != nil && *r.Templater == "html" { - templater = NewHTMLTemplater() - } - tag := "" - if r.Tag != nil { - tag = *r.Tag - } - var variables map[string]string - if r.Preprocessor != nil { - variables = r.Preprocessor.Mapping - } - requests[i] = RequestConfig{ - Name: r.Name, - Method: r.Method, - Headers: r.Headers, - Tag: tag, - Body: r.Body, - URI: r.URI, - Preprocessor: Preprocessor{Mapping: variables}, - Postprocessors: postprocessors, - Templater: templater, - } - } - } - - var scenarios []ScenarioConfig - if len(ammo.Scenarios) > 0 { - scenarios = make([]ScenarioConfig, len(ammo.Scenarios)) - for i, s := range ammo.Scenarios { - scenarios[i] = ScenarioConfig{ - Name: s.Name, - Requests: s.Requests, - } - if s.Weight != nil { - scenarios[i].Weight = *s.Weight - } - if s.MinWaitingTime != nil { - scenarios[i].MinWaitingTime = *s.MinWaitingTime - } - } + bytes, err := yaml.Marshal(ammo) + if err != nil { + return AmmoConfig{}, fmt.Errorf("%s, cant yaml.Marshal: %w", op, err) } - - result := AmmoConfig{ - VariableSources: sources, - Requests: requests, - Scenarios: scenarios, + cfg, err := decodeMap(bytes) + if err != nil { + return AmmoConfig{}, fmt.Errorf("%s, decodeMap, %w", op, err) } - - return result, nil + return cfg, nil } func ConvertAmmoToHCL(ammo AmmoConfig) (AmmoHCL, error) { @@ -358,7 +216,7 @@ func ConvertAmmoToHCL(ammo AmmoConfig) (AmmoHCL, error) { if ok { templater = "html" } - req.Templater = &templater + req.Templater = &TemplaterHCL{Type: templater} requests[i] = req } diff --git a/components/providers/http_scenario/ammo_hcl_test.go b/components/providers/http_scenario/ammo_hcl_test.go index 3452cefad..ccd210d98 100644 --- a/components/providers/http_scenario/ammo_hcl_test.go +++ b/components/providers/http_scenario/ammo_hcl_test.go @@ -16,8 +16,10 @@ import ( "github.com/yandex/pandora/lib/pointer" ) +var testFS = afero.NewMemMapFs() + func Test_convertingYamlToHCL(t *testing.T) { - Import(nil) + Import(testFS) testOnce.Do(func() { pluginconfig.AddHooks() }) @@ -111,8 +113,10 @@ func Test_decodeHCL(t *testing.T) { } func TestConvertHCLToAmmo(t *testing.T) { - fs := afero.NewMemMapFs() - templater := "html" + Import(testFS) + testOnce.Do(func() { + pluginconfig.AddHooks() + }) tests := []struct { name string ammo AmmoHCL @@ -135,6 +139,7 @@ func TestConvertHCLToAmmo(t *testing.T) { {Type: "var/xpath", Mapping: &(map[string]string{"key": "var/xpath"})}, {Type: "var/jsonpath", Mapping: &(map[string]string{"key": "var/jsonpath"})}, }, + Templater: &TemplaterHCL{Type: "text"}, }, }, Scenarios: []ScenarioHCL{ @@ -143,7 +148,7 @@ func TestConvertHCLToAmmo(t *testing.T) { }, want: AmmoConfig{ VariableSources: []VariableSource{ - &VariableSourceJSON{Name: "source1", File: "data.json", fs: fs}, + &VariableSourceJSON{Name: "source1", File: "data.json", fs: testFS}, }, Requests: []RequestConfig{ { @@ -164,31 +169,6 @@ func TestConvertHCLToAmmo(t *testing.T) { }, wantErr: false, }, - { - name: "UnsupportedVariableSourceType", - ammo: AmmoHCL{ - VariableSources: []SourceHCL{ - {Name: "source1", Type: "unknown", File: pointer.ToString("data.csv")}, - }, - }, - want: AmmoConfig{}, - wantErr: true, - }, - { - name: "UnsupportedPostprocessorType", - ammo: AmmoHCL{ - Requests: []RequestHCL{ - { - Name: "req1", Method: "GET", URI: "/api", - Postprocessors: []PostprocessorHCL{ - {Type: "unknown", Mapping: &(map[string]string{"key": "value"})}, - }, - }, - }, - }, - want: AmmoConfig{}, - wantErr: true, - }, { name: "MultipleVariableSources", ammo: AmmoHCL{ @@ -200,10 +180,12 @@ func TestConvertHCLToAmmo(t *testing.T) { }, want: AmmoConfig{ VariableSources: []VariableSource{ - &VariableSourceJSON{Name: "source1", File: "data.json", fs: fs}, - &VariableSourceCsv{Name: "source2", File: "data.csv", fs: fs}, + &VariableSourceJSON{Name: "source1", File: "data.json", fs: testFS}, + &VariableSourceCsv{Name: "source2", File: "data.csv", fs: testFS}, &VariableSourceVariables{Name: "source3", Variables: map[string]any{"a": "b"}}, }, + Requests: []RequestConfig{}, + Scenarios: []ScenarioConfig{}, }, wantErr: false, }, @@ -212,14 +194,16 @@ func TestConvertHCLToAmmo(t *testing.T) { ammo: AmmoHCL{ Requests: []RequestHCL{ {Name: "req1", Method: "GET", URI: "/api/1"}, - {Name: "req2", Method: "POST", URI: "/api/2", Templater: &templater}, + {Name: "req2", Method: "POST", URI: "/api/2"}, }, }, want: AmmoConfig{ + VariableSources: []VariableSource{}, Requests: []RequestConfig{ - {Name: "req1", Method: "GET", URI: "/api/1", Templater: NewTextTemplater()}, - {Name: "req2", Method: "POST", URI: "/api/2", Templater: NewHTMLTemplater()}, + {Name: "req1", Method: "GET", URI: "/api/1"}, + {Name: "req2", Method: "POST", URI: "/api/2"}, }, + Scenarios: []ScenarioConfig{}, }, wantErr: false, }, @@ -242,6 +226,8 @@ func TestConvertHCLToAmmo(t *testing.T) { }, }, want: AmmoConfig{ + Requests: []RequestConfig{}, + VariableSources: []VariableSource{}, Scenarios: []ScenarioConfig{ { Name: "scenario1", @@ -262,13 +248,14 @@ func TestConvertHCLToAmmo(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ConvertHCLToAmmo(tt.ammo, fs) + got, err := ConvertHCLToAmmo(tt.ammo) if tt.wantErr { require.Error(t, err) return } + require.NoError(t, err) - assert.Equalf(t, tt.want, got, "ConvertHCLToAmmo(%v, %v)", tt.ammo, fs) + assert.Equalf(t, tt.want, got, "ConvertHCLToAmmo(%v, %v)", tt.ammo, testFS) }) } } @@ -313,7 +300,7 @@ func TestConvertAmmoToHCL(t *testing.T) { {Name: "source1", Type: "file/json", File: pointer.ToString("data.json")}, }, Requests: []RequestHCL{ - {Name: "req1", Method: "GET", URI: "/api", Templater: pointer.ToString("text")}, + {Name: "req1", Method: "GET", URI: "/api", Templater: &TemplaterHCL{Type: "text"}}, }, Scenarios: []ScenarioHCL{ {Name: "scenario1", Weight: pointer.ToInt64(1), MinWaitingTime: pointer.ToInt64(1000), Requests: []string{"shoot1"}}, @@ -390,8 +377,8 @@ func TestConvertAmmoToHCL(t *testing.T) { }, want: AmmoHCL{ Requests: []RequestHCL{ - {Name: "req1", Method: "GET", URI: "/api/1", Templater: pointer.ToString("text")}, - {Name: "req2", Method: "POST", URI: "/api/2", Templater: pointer.ToString("html")}, + {Name: "req1", Method: "GET", URI: "/api/1", Templater: &TemplaterHCL{Type: "text"}}, + {Name: "req2", Method: "POST", URI: "/api/2", Templater: &TemplaterHCL{Type: "html"}}, }, }, wantErr: false, diff --git a/components/providers/http_scenario/decode.go b/components/providers/http_scenario/decode.go index 24a664d04..068ac7388 100644 --- a/components/providers/http_scenario/decode.go +++ b/components/providers/http_scenario/decode.go @@ -17,14 +17,25 @@ import ( ) func ParseAmmoConfig(file io.Reader) (AmmoConfig, error) { - var ammoCfg AmmoConfig const op = "scenario/decoder.ParseAmmoConfig" - data := make(map[string]any) bytes, err := io.ReadAll(file) if err != nil { - return ammoCfg, fmt.Errorf("%s, io.ReadAll, %w", op, err) + return AmmoConfig{}, fmt.Errorf("%s, io.ReadAll, %w", op, err) } - err = yaml.Unmarshal(bytes, &data) + cfg, err := decodeMap(bytes) + if err != nil { + return AmmoConfig{}, fmt.Errorf("%s, decodeMap, %w", op, err) + } + return cfg, nil +} + +func decodeMap(bytes []byte) (AmmoConfig, error) { + const op = "scenario/decoder.decodeMap" + + var ammoCfg AmmoConfig + + data := make(map[string]any) + err := yaml.Unmarshal(bytes, &data) if err != nil { return ammoCfg, fmt.Errorf("%s, yaml.Unmarshal, %w", op, err) } diff --git a/components/providers/http_scenario/decode_sample_config_test.golden.hcl b/components/providers/http_scenario/decode_sample_config_test.golden.hcl index 3c61c1278..80d04013e 100644 --- a/components/providers/http_scenario/decode_sample_config_test.golden.hcl +++ b/components/providers/http_scenario/decode_sample_config_test.golden.hcl @@ -27,13 +27,13 @@ variable_source "variables" "variables" { request "auth_req" { method = "POST" + uri = "/auth" headers = { Content-Type = "application/json" Useragent = "Tank" } tag = "auth" body = "{\"user_id\": {{.preprocessor.user_id}}}" - uri = "/auth" preprocessor { mapping = { @@ -65,17 +65,19 @@ request "auth_req" { } } - templater = "text" + templater { + type = "text" + } } request "list_req" { method = "GET" + uri = "/list" headers = { Authorization = "Bearer {{.request.auth_req.token}}" Content-Type = "application/json" Useragent = "Tank" } tag = "list" - uri = "/list" postprocessor "var/jsonpath" { mapping = { @@ -84,10 +86,13 @@ request "list_req" { } } - templater = "html" + templater { + type = "html" + } } request "item_req" { method = "POST" + uri = "/item" headers = { Authorization = "Bearer {{.request.auth_req.token}}" Content-Type = "application/json" @@ -95,14 +100,16 @@ request "item_req" { } tag = "item_req" body = "{\"item_id\": {{.preprocessor.item}}}" - uri = "/item" preprocessor { mapping = { item = "request.list_req.items[3]" } } - templater = "text" + + templater { + type = "text" + } } scenario "scenario1" { diff --git a/components/providers/http_scenario/decode_sample_config_test.hcl b/components/providers/http_scenario/decode_sample_config_test.hcl index 55644db64..d6805b4c4 100644 --- a/components/providers/http_scenario/decode_sample_config_test.hcl +++ b/components/providers/http_scenario/decode_sample_config_test.hcl @@ -59,7 +59,9 @@ EOF } } - templater = "text" + templater { + type = "html" + } } request "list_req" { method = "GET" @@ -78,7 +80,9 @@ request "list_req" { } } - templater = "text" + templater { + type = "html" + } } request "item_req" { method = "POST" @@ -99,7 +103,9 @@ EOF } } - templater = "text" + templater { + type = "html" + } } scenario "scenario1" { diff --git a/components/providers/http_scenario/provider.go b/components/providers/http_scenario/provider.go index 26151416e..bcee4a2d3 100644 --- a/components/providers/http_scenario/provider.go +++ b/components/providers/http_scenario/provider.go @@ -45,7 +45,7 @@ func NewProvider(fs afero.Fs, conf Config) (core.Provider, error) { if er != nil { return nil, fmt.Errorf("%s ParseHCLFile %w", op, er) } - ammoCfg, err = ConvertHCLToAmmo(ammoHcl, fs) + ammoCfg, err = ConvertHCLToAmmo(ammoHcl) case strings.HasSuffix(lowerName, ".yaml") || strings.HasPrefix(lowerName, ".yml"): ammoCfg, err = ParseAmmoConfig(file) default: diff --git a/docs/eng/scenario-http-generator.md b/docs/eng/scenario-http-generator.md index 4455ca5f6..526a4c102 100644 --- a/docs/eng/scenario-http-generator.md +++ b/docs/eng/scenario-http-generator.md @@ -115,7 +115,10 @@ request "request_name" { body = < EOF - templater = "text" + + templater { + type = "text" + } preprocessor { mapping = { diff --git a/docs/rus/scenario-http-generator.md b/docs/rus/scenario-http-generator.md index 5a21ffb35..761553ebb 100644 --- a/docs/rus/scenario-http-generator.md +++ b/docs/rus/scenario-http-generator.md @@ -115,7 +115,10 @@ request "request_name" { body = < EOF - templater = "text" + + templater { + type = "text" + } preprocessor { mapping = { diff --git a/examples/http/server/server.go b/examples/http/server/server.go new file mode 100644 index 000000000..03db043bb --- /dev/null +++ b/examples/http/server/server.go @@ -0,0 +1,296 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "math/rand" + "mime" + "net" + "net/http" + "strconv" + "strings" + "sync" +) + +const ( + defaultPort = "8091" + + userCount = 10 + userMultiplicator = 1000 + itemMultiplicator = 100 +) + +type StatisticBodyResponse struct { + Code200 map[int64]uint64 `json:"200"` + Code400 uint64 `json:"400"` + Code500 uint64 `json:"500"` +} + +type StatisticResponse struct { + Auth StatisticBodyResponse `json:"auth"` + List StatisticBodyResponse `json:"list"` + Item StatisticBodyResponse `json:"item"` +} + +func checkContentTypeAndMethod(r *http.Request, methods []string) (int, error) { + contentType := r.Header.Get("Content-Type") + mt, _, err := mime.ParseMediaType(contentType) + if err != nil { + return http.StatusBadRequest, errors.New("malformed Content-Type header") + } + + if mt != "application/json" { + return http.StatusUnsupportedMediaType, errors.New("header Content-Type must be application/json") + } + + for _, method := range methods { + if r.Method == method { + return 0, nil + } + } + return http.StatusMethodNotAllowed, errors.New("method not allowed") +} + +func (s *Server) checkAuthorization(r *http.Request) (int64, int, error) { + authHeader := r.Header.Get("Authorization") + authHeader = strings.Replace(authHeader, "Bearer ", "", 1) + s.mu.RLock() + userID := s.keys[authHeader] + s.mu.RUnlock() + + if userID == 0 { + return 0, http.StatusUnauthorized, errors.New("StatusUnauthorized") + } + return userID, 0, nil +} + +func (s *Server) authHandler(w http.ResponseWriter, r *http.Request) { + code, err := checkContentTypeAndMethod(r, []string{http.MethodPost}) + if err != nil { + if code >= 500 { + s.stats.IncAuth500() + } else { + s.stats.IncAuth400() + } + http.Error(w, err.Error(), code) + return + } + + user := struct { + UserID int64 `json:"user_id"` + }{} + err = json.NewDecoder(r.Body).Decode(&user) + if err != nil { + s.stats.IncAuth500() + http.Error(w, "Incorrect body", http.StatusNotAcceptable) + return + } + if user.UserID > userCount { + s.stats.IncAuth400() + http.Error(w, "Incorrect user_id", http.StatusBadRequest) + return + } + + s.stats.IncAuth200(user.UserID) + + var authKey string + s.mu.RLock() + for k, v := range s.keys { + if v == user.UserID { + authKey = k + break + } + } + s.mu.RUnlock() + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(fmt.Sprintf(`{"auth_key": "%s"}`, authKey))) +} + +func (s *Server) listHandler(w http.ResponseWriter, r *http.Request) { + code, err := checkContentTypeAndMethod(r, []string{http.MethodGet}) + if err != nil { + if code >= 500 { + s.stats.IncList500() + } else { + s.stats.IncList400() + } + http.Error(w, err.Error(), code) + return + } + + userID, code, err := s.checkAuthorization(r) + if err != nil { + http.Error(w, err.Error(), code) + return + } + + s.stats.IncList200(userID) + + // Logic + userID *= userMultiplicator + result := make([]string, itemMultiplicator) + for i := int64(0); i < itemMultiplicator; i++ { + result[i] = strconv.FormatInt(userID+i, 10) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(fmt.Sprintf(`{"items": [%s]}`, strings.Join(result, ",")))) +} + +func (s *Server) orderHandler(w http.ResponseWriter, r *http.Request) { + code, err := checkContentTypeAndMethod(r, []string{http.MethodPost}) + if err != nil { + if code >= 500 { + s.stats.IncOrder500() + } else { + s.stats.IncOrder400() + } + http.Error(w, err.Error(), code) + return + } + + userID, code, err := s.checkAuthorization(r) + if err != nil { + http.Error(w, err.Error(), code) + return + } + + // Logic + itm := struct { + ItemID int64 `json:"item_id"` + }{} + err = json.NewDecoder(r.Body).Decode(&itm) + if err != nil { + s.stats.IncOrder500() + http.Error(w, "Incorrect body", http.StatusNotAcceptable) + return + } + + ranger := userID * userMultiplicator + if itm.ItemID < ranger || itm.ItemID >= ranger+itemMultiplicator { + s.stats.IncOrder400() + http.Error(w, "Incorrect user_id", http.StatusBadRequest) + return + } + + s.stats.IncOrder200(userID) + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(fmt.Sprintf(`{"item": %d}`, itm.ItemID))) +} + +func (s *Server) resetHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + s.stats.Reset() + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status": "ok"}`)) +} + +func (s *Server) statisticHandler(w http.ResponseWriter, r *http.Request) { + response := StatisticResponse{ + Auth: StatisticBodyResponse{ + Code200: s.stats.Auth200, + Code400: s.stats.auth400.Load(), + Code500: s.stats.auth500.Load(), + }, + List: StatisticBodyResponse{ + Code200: s.stats.list200, + Code400: s.stats.list400.Load(), + Code500: s.stats.list500.Load(), + }, + Item: StatisticBodyResponse{ + Code200: s.stats.Order200, + Code400: s.stats.order400.Load(), + Code500: s.stats.order500.Load(), + }, + } + b, err := json.Marshal(response) + if err != nil { + http.Error(w, "Internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(b) +} + +func NewServer(addr string, log *slog.Logger, seed int64) *Server { + r := rand.New(rand.NewSource(seed)) + var randStringRunes = func(n int) string { + var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[r.Intn(len(letterRunes))] + } + return string(b) + } + + keys := make(map[string]int64, userCount) + for i := int64(1); i <= userCount; i++ { + keys[randStringRunes(64)] = i + } + + result := &Server{Log: log, stats: newStats(userCount), keys: keys} + mux := http.NewServeMux() + + mux.Handle("/auth", http.HandlerFunc(result.authHandler)) + mux.Handle("/list", http.HandlerFunc(result.listHandler)) + mux.Handle("/order", http.HandlerFunc(result.orderHandler)) + mux.Handle("/stats", http.HandlerFunc(result.statisticHandler)) + mux.Handle("/reset", http.HandlerFunc(result.resetHandler)) + + ctx := context.Background() + result.srv = &http.Server{ + Addr: addr, + Handler: mux, + BaseContext: func(l net.Listener) context.Context { + return ctx + }, + } + log.Info("New server created", slog.String("addr", addr), slog.Any("keys", keys)) + + return result +} + +type Server struct { + srv *http.Server + + Log *slog.Logger + stats *Stats + keys map[string]int64 + mu sync.RWMutex + + runErr chan error + finish bool +} + +func (s *Server) Err() <-chan error { + return s.runErr +} + +func (s *Server) ServeAsync() { + go func() { + err := s.srv.ListenAndServe() + if err != nil { + s.runErr <- err + } else { + s.runErr <- nil + } + s.finish = true + }() +} + +func (s *Server) Shutdown(ctx context.Context) error { + return s.srv.Shutdown(ctx) +} + +func (s *Server) Stats() *Stats { + return s.stats +} diff --git a/examples/http/server/stats.go b/examples/http/server/stats.go new file mode 100644 index 000000000..d11d62962 --- /dev/null +++ b/examples/http/server/stats.go @@ -0,0 +1,101 @@ +package server + +import ( + "sync" + "sync/atomic" +) + +func newStats(capacity int) *Stats { + stats := Stats{ + Auth200: make(map[int64]uint64, capacity), + auth200Mutex: sync.Mutex{}, + auth400: atomic.Uint64{}, + auth500: atomic.Uint64{}, + list200: make(map[int64]uint64, capacity), + list200Mutex: sync.Mutex{}, + list400: atomic.Uint64{}, + list500: atomic.Uint64{}, + Order200: make(map[int64]uint64, capacity), + order200Mutex: sync.Mutex{}, + order400: atomic.Uint64{}, + order500: atomic.Uint64{}, + } + return &stats +} + +type Stats struct { + Auth200 map[int64]uint64 + auth200Mutex sync.Mutex + auth400 atomic.Uint64 + auth500 atomic.Uint64 + list200 map[int64]uint64 + list200Mutex sync.Mutex + list400 atomic.Uint64 + list500 atomic.Uint64 + Order200 map[int64]uint64 + order200Mutex sync.Mutex + order400 atomic.Uint64 + order500 atomic.Uint64 +} + +func (s *Stats) IncAuth400() { + s.auth400.Add(1) +} + +func (s *Stats) IncAuth500() { + s.auth500.Add(1) +} + +func (s *Stats) IncAuth200(userID int64) { + s.auth200Mutex.Lock() + s.Auth200[userID]++ + s.auth200Mutex.Unlock() +} + +func (s *Stats) IncList400() { + s.list400.Add(1) +} + +func (s *Stats) IncList500() { + s.list500.Add(1) +} + +func (s *Stats) IncList200(userID int64) { + s.list200Mutex.Lock() + s.list200[userID]++ + s.list200Mutex.Unlock() +} + +func (s *Stats) IncOrder400() { + s.order400.Add(1) +} + +func (s *Stats) IncOrder500() { + s.order500.Add(1) +} + +func (s *Stats) IncOrder200(userID int64) { + s.order200Mutex.Lock() + s.Order200[userID]++ + s.order200Mutex.Unlock() +} + +func (s *Stats) Reset() { + s.auth200Mutex.Lock() + s.Auth200 = map[int64]uint64{} + s.auth200Mutex.Unlock() + s.auth400.Store(0) + s.auth500.Store(0) + + s.list200Mutex.Lock() + s.list200 = map[int64]uint64{} + s.list200Mutex.Unlock() + s.list400.Store(0) + s.list500.Store(0) + + s.order200Mutex.Lock() + s.Order200 = map[int64]uint64{} + s.order200Mutex.Unlock() + s.order400.Store(0) + s.order500.Store(0) +}