From 04e225cdeb9b9d91f623b0836f591762e7b16034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Tue, 10 Jan 2023 13:09:46 +0000 Subject: [PATCH 01/24] do rand operation on slice environments --- core/scenario/scripting/extraction/json.go | 5 ++ .../scripting/injection/environment.go | 53 +++++++++++++++---- core/scenario/service.go | 5 +- core/types/regex/regex.go | 4 +- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/core/scenario/scripting/extraction/json.go b/core/scenario/scripting/extraction/json.go index 715466a4..58652f71 100644 --- a/core/scenario/scripting/extraction/json.go +++ b/core/scenario/scripting/extraction/json.go @@ -46,6 +46,11 @@ var unmarshalJsonCapture = func(result gjson.Result) (interface{}, error) { return jBoolSlice, err } + jObjectSlice := []map[string]interface{}{} + err = json.Unmarshal(bRaw, &jObjectSlice) + if err == nil { + return jObjectSlice, err + } } if result.IsBool() { diff --git a/core/scenario/scripting/injection/environment.go b/core/scenario/scripting/injection/environment.go index 280c6f45..ea57428e 100644 --- a/core/scenario/scripting/injection/environment.go +++ b/core/scenario/scripting/injection/environment.go @@ -3,9 +3,11 @@ package injection import ( "encoding/json" "fmt" + "math/rand" "reflect" "regexp" "strings" + "time" "go.ddosify.com/ddosify/core/types/regex" ) @@ -22,6 +24,7 @@ func (ei *EnvironmentInjector) Init() { ei.jr = regexp.MustCompile(regex.JsonEnvironmentVarRegex) ei.dr = regexp.MustCompile(regex.DynamicVariableRegex) ei.jdr = regexp.MustCompile(regex.JsonDynamicVariableRegex) + rand.Seed(time.Now().UnixNano()) } func (ei *EnvironmentInjector) getFakeData(key string) (interface{}, error) { @@ -57,11 +60,7 @@ func (ei *EnvironmentInjector) InjectEnv(text string, envs map[string]interface{ var err error truncated = truncateTag(string(s), regex.EnvironmentVariableRegex) - - env, ok := envs[truncated] - if !ok { - err = fmt.Errorf("env not found") - } + env, err = getEnv(envs, truncated) if err == nil { switch env.(type) { @@ -91,11 +90,7 @@ func (ei *EnvironmentInjector) InjectEnv(text string, envs map[string]interface{ var err error truncated = truncateTag(string(s), regex.JsonEnvironmentVarRegex) - - env, ok := envs[truncated] - if !ok { - err = fmt.Errorf("env not found") - } + env, err = getEnv(envs, truncated) if err == nil { mEnv, err := json.Marshal(env) @@ -199,6 +194,44 @@ func (ei *EnvironmentInjector) InjectDynamic(text string) (string, error) { } +func getEnv(envs map[string]interface{}, key string) (interface{}, error) { + var err error + var val interface{} + + if strings.HasPrefix(key, "rand(") && strings.HasSuffix(key, ")") { // get random val from []interface{} + key = key[5 : len(key)-1] + var exists bool + val, exists = envs[key] + if !exists { + err = fmt.Errorf("env not found") + } + + switch v := val.(type) { + case []interface{}: + val = v[rand.Intn(len(v))] + case []string: + val = v[rand.Intn(len(v))] + case []bool: + val = v[rand.Intn(len(v))] + case []int: + val = v[rand.Intn(len(v))] + case []float64: + val = v[rand.Intn(len(v))] + default: + err = fmt.Errorf("can not perform rand() operation on non-array value") + } + + } else { + var exists bool + val, exists = envs[key] + if !exists { + err = fmt.Errorf("env not found") + } + } + + return val, err +} + func unifyErrors(errors []error) error { sb := strings.Builder{} diff --git a/core/scenario/service.go b/core/scenario/service.go index 1a602752..f3705a76 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -173,7 +173,10 @@ func injectDynamicVars(envs map[string]interface{}) { vi := &injection.EnvironmentInjector{} vi.Init() for k, v := range envs { - vStr := v.(string) + vStr, isStr := v.(string) + if !isStr { + continue + } if dynamicRgx.MatchString(vStr) { injected, err := vi.InjectDynamic(vStr) if err != nil { diff --git a/core/types/regex/regex.go b/core/types/regex/regex.go index 8a20d01d..325d6086 100644 --- a/core/types/regex/regex.go +++ b/core/types/regex/regex.go @@ -3,5 +3,5 @@ package regex const DynamicVariableRegex = `\{{(_)[^}]+\}}` const JsonDynamicVariableRegex = `\"{{(_)[^}]+\}}"` -const EnvironmentVariableRegex = `\{{[^_]\w*\}}` -const JsonEnvironmentVarRegex = `\"{{[^_]\w*\}}"` +const EnvironmentVariableRegex = `\{{[^_][a-zA-Z0-9_()]*\}}` +const JsonEnvironmentVarRegex = `\"{{[^_][a-zA-Z0-9_()]*\}}"` From 3e3350c3f9b9fee5392357f267e05797398c6a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Tue, 10 Jan 2023 14:29:32 +0000 Subject: [PATCH 02/24] read csv data --- config/csv.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ config/json.go | 23 +++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 config/csv.go diff --git a/config/csv.go b/config/csv.go new file mode 100644 index 00000000..816853bb --- /dev/null +++ b/config/csv.go @@ -0,0 +1,54 @@ +package config + +import ( + "encoding/csv" + "fmt" + "os" + "strconv" +) + +func readCsv(conf CsvConf) ([]map[string]interface{}, error) { + if conf.Src == "local" { + f, err := os.Open(conf.Path) + if err != nil { + return nil, err + } + defer f.Close() + + // read csv values using csv.Reader + csvReader := csv.NewReader(f) + csvReader.Comma = []rune(conf.Delimiter)[0] + csvReader.TrimLeadingSpace = true + data, err := csvReader.ReadAll() + if err != nil { + return nil, err + } + + if conf.SkipFirstLine { + data = data[1:] + } + + rt := make([]map[string]interface{}, len(data)) + + // TODOcorr: empty line check full "" + for _, row := range data { + x := map[string]interface{}{} + for index, tag := range conf.Vars { // "0":"name", "1":"city","2":"team" + i, err := strconv.Atoi(index) + if err != nil { + return nil, err + } + x[tag] = row[i] + } + rt = append(rt, x) + } + + return rt, nil + + } else if conf.Src == "remote" { + // TODOcorr, http call + } + + return nil, fmt.Errorf("csv read error") + +} diff --git a/config/json.go b/config/json.go index 4b3d514b..1f058718 100644 --- a/config/json.go +++ b/config/json.go @@ -108,6 +108,17 @@ func (s *step) UnmarshalJSON(data []byte) error { return nil } +type CsvConf struct { + Path string `json:"path"` + Src string `json:"src"` + Delimiter string `json:"delimiter"` + SkipFirstLine bool `json:"skipFirstLine"` + Vars map[string]string `json:"vars"` // "0":"name", "1":"city","2":"team" + SkipEmptyLine bool `json:"skipEmptyLine"` + AllowQuota bool `json:"allowQuota"` + Order string `json:"order"` +} + type JsonReader struct { ReqCount *int `json:"request_count"` IterCount *int `json:"iteration_count"` @@ -118,6 +129,7 @@ type JsonReader struct { Output string `json:"output"` Proxy string `json:"proxy"` Envs map[string]interface{} `json:"env"` + Data map[string]CsvConf `json:"data"` Debug bool `json:"debug"` } @@ -152,6 +164,17 @@ func (j *JsonReader) Init(jsonByte []byte) (err error) { } func (j *JsonReader) CreateHammer() (h types.Hammer, err error) { + // Read Data + data := make(map[string][]map[string]interface{}, len(j.Data)) + for k, conf := range j.Data { + var rows []map[string]interface{} + rows, err = readCsv(conf) + if err != nil { + return + } + data[k] = rows + } + // Scenario s := types.Scenario{ Envs: j.Envs, From 10b5631a761c7998281a58adf9cd153f85e2bf3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Tue, 10 Jan 2023 16:23:00 +0000 Subject: [PATCH 03/24] pass random data from csv to envs --- config/csv.go | 14 +++++- config/json.go | 13 +++++- core/scenario/requester/base.go | 3 +- core/scenario/requester/http.go | 5 +-- core/scenario/requester/http_test.go | 10 ++--- .../scripting/injection/environment.go | 6 +-- core/scenario/service.go | 44 ++++++++++++++++++- core/scenario/service_test.go | 3 +- core/types/regex/regex.go | 4 +- core/types/scenario.go | 6 +++ 10 files changed, 87 insertions(+), 21 deletions(-) diff --git a/config/csv.go b/config/csv.go index 816853bb..53e167c8 100644 --- a/config/csv.go +++ b/config/csv.go @@ -28,10 +28,12 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { data = data[1:] } - rt := make([]map[string]interface{}, len(data)) + rt := make([]map[string]interface{}, 0) // unclear how many empty line exist - // TODOcorr: empty line check full "" for _, row := range data { + if conf.SkipEmptyLine && emptyLine(row) { + continue + } x := map[string]interface{}{} for index, tag := range conf.Vars { // "0":"name", "1":"city","2":"team" i, err := strconv.Atoi(index) @@ -50,5 +52,13 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { } return nil, fmt.Errorf("csv read error") +} +func emptyLine(row []string) bool { + for _, field := range row { + if field != "" { + return false + } + } + return true } diff --git a/config/json.go b/config/json.go index 1f058718..e95faba6 100644 --- a/config/json.go +++ b/config/json.go @@ -165,19 +165,28 @@ func (j *JsonReader) Init(jsonByte []byte) (err error) { func (j *JsonReader) CreateHammer() (h types.Hammer, err error) { // Read Data - data := make(map[string][]map[string]interface{}, len(j.Data)) + var readData map[string]types.CsvData + if len(j.Data) > 0 { + readData = make(map[string]types.CsvData, len(j.Data)) + } for k, conf := range j.Data { var rows []map[string]interface{} rows, err = readCsv(conf) if err != nil { return } - data[k] = rows + var csvData types.CsvData + csvData.Rows = rows + if conf.Order == "random" { + csvData.Random = true + } + readData[k] = csvData } // Scenario s := types.Scenario{ Envs: j.Envs, + Data: readData, } var si types.ScenarioStep for _, step := range j.Steps { diff --git a/core/scenario/requester/base.go b/core/scenario/requester/base.go index 73dcd6a3..d5bbe239 100644 --- a/core/scenario/requester/base.go +++ b/core/scenario/requester/base.go @@ -24,13 +24,14 @@ import ( "context" "net/url" + "go.ddosify.com/ddosify/core/scenario/scripting/injection" "go.ddosify.com/ddosify/core/types" ) // Requester is the interface that abstracts different protocols' request sending implementations. // Protocol field in the types.ScenarioStep determines which requester implementation to use. type Requester interface { - Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool) error + Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error Send(envs map[string]interface{}) *types.ScenarioStepResult Done() } diff --git a/core/scenario/requester/http.go b/core/scenario/requester/http.go index 281d884a..8b0d8a81 100644 --- a/core/scenario/requester/http.go +++ b/core/scenario/requester/http.go @@ -59,12 +59,11 @@ type HttpRequester struct { } // Init creates a client with the given scenarioItem. HttpRequester uses the same http.Client for all requests -func (h *HttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool) (err error) { +func (h *HttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool, ei *injection.EnvironmentInjector) (err error) { h.ctx = ctx h.packet = s h.proxyAddr = proxyAddr - h.ei = &injection.EnvironmentInjector{} - h.ei.Init() + h.ei = ei h.containsDynamicField = make(map[string]bool) h.containsEnvVar = make(map[string]bool) h.debug = debug diff --git a/core/scenario/requester/http_test.go b/core/scenario/requester/http_test.go index 3d3d41dc..60029782 100644 --- a/core/scenario/requester/http_test.go +++ b/core/scenario/requester/http_test.go @@ -45,7 +45,7 @@ func TestInit(t *testing.T) { ctx := context.TODO() h := &HttpRequester{} - h.Init(ctx, s, p, false) + h.Init(ctx, s, p, false, nil) if !reflect.DeepEqual(h.packet, s) { t.Errorf("Expected %v, Found %v", s, h.packet) @@ -155,7 +155,7 @@ func TestInitClient(t *testing.T) { for _, test := range tests { tf := func(t *testing.T) { h := &HttpRequester{} - h.Init(test.ctx, test.scenarioItem, test.proxy, false) + h.Init(test.ctx, test.scenarioItem, test.proxy, false, nil) transport := h.client.Transport.(*http.Transport) tls := transport.TLSClientConfig @@ -309,7 +309,7 @@ func TestInitRequest(t *testing.T) { for _, test := range tests { tf := func(t *testing.T) { h := &HttpRequester{} - err := h.Init(ctx, test.scenarioItem, p, false) + err := h.Init(ctx, test.scenarioItem, p, false, nil) if test.shouldErr { if err == nil { @@ -378,7 +378,7 @@ func TestSendOnDebugModePopulatesDebugInfo(t *testing.T) { h := &HttpRequester{} debug := true var proxy *url.URL - _ = h.Init(ctx, test.scenarioStep, proxy, debug) + _ = h.Init(ctx, test.scenarioStep, proxy, debug, nil) envs := map[string]interface{}{} res := h.Send(envs) @@ -443,7 +443,7 @@ func TestCaptureEnvShouldSetEmptyStringWhenReqFails(t *testing.T) { h := &HttpRequester{} debug := true var proxy *url.URL - _ = h.Init(ctx, test.scenarioStep, proxy, debug) + _ = h.Init(ctx, test.scenarioStep, proxy, debug, nil) envs := map[string]interface{}{} res := h.Send(envs) diff --git a/core/scenario/scripting/injection/environment.go b/core/scenario/scripting/injection/environment.go index ea57428e..d00ff9f8 100644 --- a/core/scenario/scripting/injection/environment.go +++ b/core/scenario/scripting/injection/environment.go @@ -60,7 +60,7 @@ func (ei *EnvironmentInjector) InjectEnv(text string, envs map[string]interface{ var err error truncated = truncateTag(string(s), regex.EnvironmentVariableRegex) - env, err = getEnv(envs, truncated) + env, err = ei.getEnv(envs, truncated) if err == nil { switch env.(type) { @@ -90,7 +90,7 @@ func (ei *EnvironmentInjector) InjectEnv(text string, envs map[string]interface{ var err error truncated = truncateTag(string(s), regex.JsonEnvironmentVarRegex) - env, err = getEnv(envs, truncated) + env, err = ei.getEnv(envs, truncated) if err == nil { mEnv, err := json.Marshal(env) @@ -194,7 +194,7 @@ func (ei *EnvironmentInjector) InjectDynamic(text string) (string, error) { } -func getEnv(envs map[string]interface{}, key string) (interface{}, error) { +func (ei *EnvironmentInjector) getEnv(envs map[string]interface{}, key string) (interface{}, error) { var err error var val interface{} diff --git a/core/scenario/service.go b/core/scenario/service.go index f3705a76..8efa6636 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -48,6 +48,9 @@ type ScenarioService struct { clientMutex sync.Mutex debug bool + ei *injection.EnvironmentInjector + indexMu sync.Mutex + iterIndex int } // NewScenarioService is the constructor of the ScenarioService. @@ -55,13 +58,25 @@ func NewScenarioService() *ScenarioService { return &ScenarioService{} } +func (s *ScenarioService) incrementIterIndex() { + s.indexMu.Lock() + defer s.indexMu.Unlock() + s.iterIndex++ +} + // Init initializes the ScenarioService.clients with the given types.Scenario and proxies. // Passes the given ctx to the underlying requestor so we are able to control the life of each request. -func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, proxies []*url.URL, debug bool) (err error) { +func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, + proxies []*url.URL, debug bool) (err error) { s.scenario = scenario s.ctx = ctx s.debug = debug s.clients = make(map[*url.URL][]scenarioItemRequester, len(proxies)) + + ei := &injection.EnvironmentInjector{} + ei.Init() + s.ei = ei + for _, p := range proxies { err = s.createRequesters(p) if err != nil { @@ -79,6 +94,7 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( response = &types.ScenarioResult{StepResults: []*types.ScenarioStepResult{}} response.StartTime = startTime response.ProxyAddr = proxy + rand.Seed(time.Now().UnixNano()) requesters, e := s.getOrCreateRequesters(proxy) if e != nil { @@ -94,6 +110,8 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( injectDynamicVars(envs) for _, sr := range requesters { + s.enrichEnvFromData(envs) + s.incrementIterIndex() res := sr.requester.Send(envs) if res.Err.Type == types.ErrorProxy || res.Err.Type == types.ErrorIntented { @@ -121,6 +139,28 @@ func enrichEnvFromPrevStep(m1 map[string]interface{}, m2 map[string]interface{}) } } +func (s *ScenarioService) enrichEnvFromData(envs map[string]interface{}) { + var row map[string]interface{} + sb := strings.Builder{} + for key, csvData := range s.scenario.Data { + if csvData.Random { + row = csvData.Rows[rand.Intn(len(csvData.Rows))] + } else { + row = csvData.Rows[s.iterIndex%len(csvData.Rows)] + } + + for tag, v := range row { + sb.WriteString("data.") + sb.WriteString(key) + sb.WriteString(".") + sb.WriteString(tag) + // data.info.name + envs[sb.String()] = v + sb.Reset() + } + } +} + func (s *ScenarioService) Done() { for _, v := range s.clients { for _, r := range v { @@ -160,7 +200,7 @@ func (s *ScenarioService) createRequesters(proxy *url.URL) (err error) { }, ) - err = r.Init(s.ctx, si, proxy, s.debug) + err = r.Init(s.ctx, si, proxy, s.debug, s.ei) if err != nil { return } diff --git a/core/scenario/service_test.go b/core/scenario/service_test.go index 1bb68d02..17687b41 100644 --- a/core/scenario/service_test.go +++ b/core/scenario/service_test.go @@ -29,6 +29,7 @@ import ( "time" "go.ddosify.com/ddosify/core/scenario/requester" + "go.ddosify.com/ddosify/core/scenario/scripting/injection" "go.ddosify.com/ddosify/core/types" ) @@ -45,7 +46,7 @@ type MockRequester struct { ReturnSend *types.ScenarioStepResult } -func (m *MockRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool) (err error) { +func (m *MockRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool, ei *injection.EnvironmentInjector) (err error) { m.InitCalled = true if m.FailInit { return fmt.Errorf(m.FailInitMsg) diff --git a/core/types/regex/regex.go b/core/types/regex/regex.go index 325d6086..67eb2d5d 100644 --- a/core/types/regex/regex.go +++ b/core/types/regex/regex.go @@ -3,5 +3,5 @@ package regex const DynamicVariableRegex = `\{{(_)[^}]+\}}` const JsonDynamicVariableRegex = `\"{{(_)[^}]+\}}"` -const EnvironmentVariableRegex = `\{{[^_][a-zA-Z0-9_()]*\}}` -const JsonEnvironmentVarRegex = `\"{{[^_][a-zA-Z0-9_()]*\}}"` +const EnvironmentVariableRegex = `\{{[^_][a-zA-Z0-9_().]*\}}` +const JsonEnvironmentVarRegex = `\"{{[^_][a-zA-Z0-9_().]*\}}"` diff --git a/core/types/scenario.go b/core/types/scenario.go index 561b246a..747b7d3e 100644 --- a/core/types/scenario.go +++ b/core/types/scenario.go @@ -70,6 +70,7 @@ func init() { type Scenario struct { Steps []ScenarioStep Envs map[string]interface{} + Data map[string]CsvData } func (s *Scenario) validate() error { @@ -206,6 +207,11 @@ type EnvCaptureConf struct { Key *string `json:"headerKey"` // headerKey } +type CsvData struct { + Rows []map[string]interface{} + Random bool +} + // Auth struct should be able to include all necessary authentication realated data for supportedAuthentications. type Auth struct { Type string From 0b6ca909b136ac2ba2d886fbae008c6db5284e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 11 Jan 2023 08:16:01 +0000 Subject: [PATCH 04/24] add default values for csv config --- config/json.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config/json.go b/config/json.go index e95faba6..88b472a5 100644 --- a/config/json.go +++ b/config/json.go @@ -119,6 +119,17 @@ type CsvConf struct { Order string `json:"order"` } +func (c *CsvConf) UnmarshalJSON(data []byte) error { + // default values + c.SkipEmptyLine = true + c.SkipFirstLine = false + c.AllowQuota = false + c.Delimiter = "," + + type tempCsv CsvConf + return json.Unmarshal(data, (*tempCsv)(c)) +} + type JsonReader struct { ReqCount *int `json:"request_count"` IterCount *int `json:"iteration_count"` From de1cbbbcbc916d4166de2866ef1ed52183d2da6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 11 Jan 2023 09:54:40 +0000 Subject: [PATCH 05/24] add type field to vars tags --- config/csv.go | 31 ++++++++++++++++++++++++++++++- config/json.go | 28 ++++++++++++++++++++-------- core/scenario/service.go | 5 +++-- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/config/csv.go b/config/csv.go index 53e167c8..d11f5e9a 100644 --- a/config/csv.go +++ b/config/csv.go @@ -2,6 +2,7 @@ package config import ( "encoding/csv" + "encoding/json" "fmt" "os" "strconv" @@ -19,6 +20,8 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { csvReader := csv.NewReader(f) csvReader.Comma = []rune(conf.Delimiter)[0] csvReader.TrimLeadingSpace = true + csvReader.LazyQuotes = conf.AllowQuota + data, err := csvReader.ReadAll() if err != nil { return nil, err @@ -40,7 +43,33 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { if err != nil { return nil, err } - x[tag] = row[i] + // convert + var val interface{} + switch tag.Type { + case "json": + json.Unmarshal([]byte(row[i]), &val) + case "int": + var err error + val, err = strconv.Atoi(row[i]) + if err != nil { + return nil, err + } + case "float": + var err error + val, err = strconv.ParseFloat(row[i], 64) + if err != nil { + return nil, err + } + case "bool": + var err error + val, err = strconv.ParseBool(row[i]) + if err != nil { + return nil, err + } + default: + val = row[i] + } + x[tag.Tag] = val } rt = append(rt, x) } diff --git a/config/json.go b/config/json.go index 88b472a5..db99c2f3 100644 --- a/config/json.go +++ b/config/json.go @@ -108,15 +108,27 @@ func (s *step) UnmarshalJSON(data []byte) error { return nil } +type Tag struct { + Tag string `json:"tag"` + Type string `json:"type"` +} + +func (t *Tag) UnmarshalJSON(data []byte) error { + // default values + t.Type = "string" + type tempTag Tag + return json.Unmarshal(data, (*tempTag)(t)) +} + type CsvConf struct { - Path string `json:"path"` - Src string `json:"src"` - Delimiter string `json:"delimiter"` - SkipFirstLine bool `json:"skipFirstLine"` - Vars map[string]string `json:"vars"` // "0":"name", "1":"city","2":"team" - SkipEmptyLine bool `json:"skipEmptyLine"` - AllowQuota bool `json:"allowQuota"` - Order string `json:"order"` + Path string `json:"path"` + Src string `json:"src"` + Delimiter string `json:"delimiter"` + SkipFirstLine bool `json:"skipFirstLine"` + Vars map[string]Tag `json:"vars"` // "0":"name", "1":"city","2":"team" + SkipEmptyLine bool `json:"skipEmptyLine"` + AllowQuota bool `json:"allowQuota"` + Order string `json:"order"` } func (c *CsvConf) UnmarshalJSON(data []byte) error { diff --git a/core/scenario/service.go b/core/scenario/service.go index 8efa6636..1b0722b0 100644 --- a/core/scenario/service.go +++ b/core/scenario/service.go @@ -108,10 +108,11 @@ func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( } // inject dynamic variables beforehand for each iteration injectDynamicVars(envs) + // pass a row from data for each iteration + s.enrichEnvFromData(envs) + s.incrementIterIndex() for _, sr := range requesters { - s.enrichEnvFromData(envs) - s.incrementIterIndex() res := sr.requester.Send(envs) if res.Err.Type == types.ErrorProxy || res.Err.Type == types.ErrorIntented { From a5fa8762bf5277790ee6609507b3585596bee49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 11 Jan 2023 10:10:39 +0000 Subject: [PATCH 06/24] validate csv conf --- config/csv.go | 12 ++++++++++++ config/json.go | 2 ++ 2 files changed, 14 insertions(+) diff --git a/config/csv.go b/config/csv.go index d11f5e9a..67ceca92 100644 --- a/config/csv.go +++ b/config/csv.go @@ -8,7 +8,19 @@ import ( "strconv" ) +func validateConf(conf CsvConf) error { + if conf.Order == "random" || conf.Order == "sequential" { + return nil + } + return fmt.Errorf("unsupported order %s, should be random|sequential", conf.Order) +} + func readCsv(conf CsvConf) ([]map[string]interface{}, error) { + err := validateConf(conf) + if err != nil { + return nil, err + } + if conf.Src == "local" { f, err := os.Open(conf.Path) if err != nil { diff --git a/config/json.go b/config/json.go index db99c2f3..6434ea9a 100644 --- a/config/json.go +++ b/config/json.go @@ -137,6 +137,7 @@ func (c *CsvConf) UnmarshalJSON(data []byte) error { c.SkipFirstLine = false c.AllowQuota = false c.Delimiter = "," + c.Order = "random" type tempCsv CsvConf return json.Unmarshal(data, (*tempCsv)(c)) @@ -200,6 +201,7 @@ func (j *JsonReader) CreateHammer() (h types.Hammer, err error) { } var csvData types.CsvData csvData.Rows = rows + if conf.Order == "random" { csvData.Random = true } From 7ace38b525c5e2a006995e609d80a1ce43edd477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 11 Jan 2023 11:37:31 +0000 Subject: [PATCH 07/24] add remote read from csv --- config/csv.go | 121 ++++++++++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 53 deletions(-) diff --git a/config/csv.go b/config/csv.go index 67ceca92..b21d0472 100644 --- a/config/csv.go +++ b/config/csv.go @@ -4,15 +4,20 @@ import ( "encoding/csv" "encoding/json" "fmt" + "io" + "net/http" "os" "strconv" ) func validateConf(conf CsvConf) error { - if conf.Order == "random" || conf.Order == "sequential" { - return nil + if !(conf.Order == "random" || conf.Order == "sequential") { + return fmt.Errorf("unsupported order %s, should be random|sequential", conf.Order) } - return fmt.Errorf("unsupported order %s, should be random|sequential", conf.Order) + if !(conf.Src == "local" || conf.Src == "remote") { + return fmt.Errorf("unsupported src %s, should be local|remote", conf.Order) + } + return nil } func readCsv(conf CsvConf) ([]map[string]interface{}, error) { @@ -21,78 +26,88 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { return nil, err } + var reader io.Reader + if conf.Src == "local" { f, err := os.Open(conf.Path) if err != nil { return nil, err } + reader = f defer f.Close() - // read csv values using csv.Reader - csvReader := csv.NewReader(f) - csvReader.Comma = []rune(conf.Delimiter)[0] - csvReader.TrimLeadingSpace = true - csvReader.LazyQuotes = conf.AllowQuota - - data, err := csvReader.ReadAll() + } else if conf.Src == "remote" { + req, err := http.NewRequest(http.MethodGet, conf.Path, nil) if err != nil { return nil, err } - - if conf.SkipFirstLine { - data = data[1:] + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err } + reader = resp.Body + defer resp.Body.Close() + } + + // read csv values using csv.Reader + csvReader := csv.NewReader(reader) + csvReader.Comma = []rune(conf.Delimiter)[0] + csvReader.TrimLeadingSpace = true + csvReader.LazyQuotes = conf.AllowQuota + + data, err := csvReader.ReadAll() + if err != nil { + return nil, err + } + + if conf.SkipFirstLine { + data = data[1:] + } - rt := make([]map[string]interface{}, 0) // unclear how many empty line exist + rt := make([]map[string]interface{}, 0) // unclear how many empty line exist - for _, row := range data { - if conf.SkipEmptyLine && emptyLine(row) { - continue + for _, row := range data { + if conf.SkipEmptyLine && emptyLine(row) { + continue + } + x := map[string]interface{}{} + for index, tag := range conf.Vars { // "0":"name", "1":"city","2":"team" + i, err := strconv.Atoi(index) + if err != nil { + return nil, err } - x := map[string]interface{}{} - for index, tag := range conf.Vars { // "0":"name", "1":"city","2":"team" - i, err := strconv.Atoi(index) + // convert + var val interface{} + switch tag.Type { + case "json": + json.Unmarshal([]byte(row[i]), &val) + case "int": + var err error + val, err = strconv.Atoi(row[i]) + if err != nil { + return nil, err + } + case "float": + var err error + val, err = strconv.ParseFloat(row[i], 64) if err != nil { return nil, err } - // convert - var val interface{} - switch tag.Type { - case "json": - json.Unmarshal([]byte(row[i]), &val) - case "int": - var err error - val, err = strconv.Atoi(row[i]) - if err != nil { - return nil, err - } - case "float": - var err error - val, err = strconv.ParseFloat(row[i], 64) - if err != nil { - return nil, err - } - case "bool": - var err error - val, err = strconv.ParseBool(row[i]) - if err != nil { - return nil, err - } - default: - val = row[i] + case "bool": + var err error + val, err = strconv.ParseBool(row[i]) + if err != nil { + return nil, err } - x[tag.Tag] = val + default: + val = row[i] } - rt = append(rt, x) + x[tag.Tag] = val } - - return rt, nil - - } else if conf.Src == "remote" { - // TODOcorr, http call + rt = append(rt, x) } - return nil, fmt.Errorf("csv read error") + return rt, nil } func emptyLine(row []string) bool { From 90fac4ccb95dad9f6ca411888a8648463f250741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 11 Jan 2023 12:19:04 +0000 Subject: [PATCH 08/24] add csv read test --- config/config_testdata/test.csv | 100 ++++++++++++++++++++++++++++++ config/csv_test.go | 104 ++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 config/config_testdata/test.csv create mode 100644 config/csv_test.go diff --git a/config/config_testdata/test.csv b/config/config_testdata/test.csv new file mode 100644 index 00000000..6936a2c4 --- /dev/null +++ b/config/config_testdata/test.csv @@ -0,0 +1,100 @@ +Username;City;Team;Payload;Age;Percent;BoolField;;; +Kenan;Tokat;Galatasaray;{"data":{"profile":{"name":"Kenan"}}};25;22.3;true;;; +Fatih;Bolu;Galatasaray;[5,6,7];29;44.3;false;;; +Kursat;Samsun;Besiktas;{"a":"b"};28;12.54;True;;; +Semih;Duzce;Besiktas;{"a":"b"};27;663.67;False;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; +;;;;;;;;; \ No newline at end of file diff --git a/config/csv_test.go b/config/csv_test.go new file mode 100644 index 00000000..7dcee520 --- /dev/null +++ b/config/csv_test.go @@ -0,0 +1,104 @@ +package config + +import ( + "reflect" + "strings" + "testing" +) + +func TestValidateCsvConf(t *testing.T) { + t.Parallel() + conf := CsvConf{ + Path: "", + Src: "", + Delimiter: "", + SkipFirstLine: false, + Vars: map[string]Tag{}, + SkipEmptyLine: false, + AllowQuota: false, + Order: "", + } + + conf.Order = "invalidOrder" + err := validateConf(conf) + + if err == nil { + t.Errorf("TestValidateCsvConf should be errored") + } + + conf.Order = "random" + conf.Src = "invalidSrc" + err = validateConf(conf) + + if err == nil { + t.Errorf("TestValidateCsvConf should be errored") + } +} + +func TestReadCsv(t *testing.T) { + t.Parallel() + conf := CsvConf{ + Path: "config_testdata/test.csv", + Src: "local", + Delimiter: ";", + SkipFirstLine: true, + Vars: map[string]Tag{ + "0": {Tag: "name", Type: "string"}, + "3": {Tag: "payload", Type: "json"}, + "4": {Tag: "age", Type: "int"}, + "5": {Tag: "percent", Type: "float"}, + "6": {Tag: "boolField", Type: "bool"}, + }, + SkipEmptyLine: true, + AllowQuota: true, + Order: "sequential", + } + + rows, err := readCsv(conf) + + if err != nil { + t.Errorf("TestReadCsv %v", err) + } + + firstName := rows[0]["name"].(string) + expectedName := "Kenan" + if !strings.EqualFold(firstName, expectedName) { + t.Errorf("TestReadCsv found: %s , expected: %s", firstName, expectedName) + } + + firstAge := rows[0]["age"].(int) + expectedAge := 25 + if firstAge != expectedAge { + t.Errorf("TestReadCsv found: %d , expected: %d", firstAge, expectedAge) + } + + firstPercent := rows[0]["percent"].(float64) + expectedPercent := 22.3 + if firstPercent != expectedPercent { + t.Errorf("TestReadCsv found: %f , expected: %f", firstPercent, expectedPercent) + } + + firstBool := rows[0]["boolField"].(bool) + expectedBool := true + if firstBool != expectedBool { + t.Errorf("TestReadCsv found: %t , expected: %t", firstBool, expectedBool) + } + + firstPayload := rows[0]["payload"].(map[string]interface{}) + expectedPayload := map[string]interface{}{ + "data": map[string]interface{}{ + "profile": map[string]interface{}{ + "name": "Kenan", + }, + }, + } + if !reflect.DeepEqual(firstPayload, expectedPayload) { + t.Errorf("TestReadCsv found: %#v , expected: %#v", firstPayload, expectedPayload) + } + + secondPayload := rows[1]["payload"].([]interface{}) + expectedPayload2 := []interface{}{5.0, 6.0, 7.0} // underlying type float64 + if !reflect.DeepEqual(secondPayload, expectedPayload2) { + t.Errorf("TestReadCsv found: %#v , expected: %#v", secondPayload, expectedPayload2) + } +} From c3f0b3c4fb573cfae5529578641a29650e7c2674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 11 Jan 2023 12:56:00 +0000 Subject: [PATCH 09/24] fix json injection err check --- .../scripting/injection/environment.go | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/core/scenario/scripting/injection/environment.go b/core/scenario/scripting/injection/environment.go index d00ff9f8..89c9136f 100644 --- a/core/scenario/scripting/injection/environment.go +++ b/core/scenario/scripting/injection/environment.go @@ -100,7 +100,7 @@ func (ei *EnvironmentInjector) InjectEnv(text string, envs map[string]interface{ } errors = append(errors, - fmt.Errorf("%s could not be found in vars global and extracted from previous steps", truncated)) + fmt.Errorf("%s could not be found in vars global and extracted from previous steps: %v", truncated, err)) return s } @@ -109,7 +109,10 @@ func (ei *EnvironmentInjector) InjectEnv(text string, envs map[string]interface{ if json.Valid(bText) { if ei.jr.Match(bText) { replacedBytes := ei.jr.ReplaceAllFunc(bText, injectToJsonByteFunc) - return string(replacedBytes), nil + if len(errors) == 0 { + return string(replacedBytes), nil + } + return "", unifyErrors(errors) } } @@ -198,14 +201,18 @@ func (ei *EnvironmentInjector) getEnv(envs map[string]interface{}, key string) ( var err error var val interface{} - if strings.HasPrefix(key, "rand(") && strings.HasSuffix(key, ")") { // get random val from []interface{} + pickRand := strings.HasPrefix(key, "rand(") && strings.HasSuffix(key, ")") + if pickRand { key = key[5 : len(key)-1] - var exists bool - val, exists = envs[key] - if !exists { - err = fmt.Errorf("env not found") - } + } + var exists bool + val, exists = envs[key] + if !exists { + err = fmt.Errorf("env not found") + } + + if pickRand { switch v := val.(type) { case []interface{}: val = v[rand.Intn(len(v))] @@ -220,13 +227,6 @@ func (ei *EnvironmentInjector) getEnv(envs map[string]interface{}, key string) ( default: err = fmt.Errorf("can not perform rand() operation on non-array value") } - - } else { - var exists bool - val, exists = envs[key] - if !exists { - err = fmt.Errorf("env not found") - } } return val, err From 30f84aca18d5b4ccf2baacbdf81abeb4cd786e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 11 Jan 2023 13:20:29 +0000 Subject: [PATCH 10/24] add sequential data load test --- core/engine_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/core/engine_test.go b/core/engine_test.go index b8730571..f17033df 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -1421,6 +1421,85 @@ func TestDynamicVarAndEnvVarInSameSection(t *testing.T) { } } +func TestLoadRandomInfoFromData(t *testing.T) { + t.Parallel() + + // Test server + requestCalled := false + kenan := "Kenan" + fatih := "Fatih" + expectedKenanAge := "25" + expectedFatihAge := "29" + + ageMap := map[string]string{kenan: "", fatih: ""} + handler := func(w http.ResponseWriter, r *http.Request) { + requestCalled = true + kenanAge := r.Header.Get(kenan) + fatihAge := r.Header.Get(fatih) + if kenanAge != "" { + ageMap[kenan] = kenanAge + } + + if fatihAge != "" { + ageMap[fatih] = fatihAge + } + } + + path := "/xxx" + mux := http.NewServeMux() + mux.HandleFunc(path, handler) + + server := httptest.NewServer(mux) + defer server.Close() + + // Prepare + h := newDummyHammer() + var csvData types.CsvData + csvData.Random = false + csvData.Rows = []map[string]interface{}{{ + "name": kenan, + "age": expectedKenanAge, + }, { + "name": fatih, + "age": expectedFatihAge, + }} + h.Scenario.Data = map[string]types.CsvData{"info": csvData} + h.Scenario.Envs = map[string]interface{}{ + "A": "B", + "URL_PATH": path, + } + h.IterationCount = 2 + h.Scenario.Steps[0] = types.ScenarioStep{ + ID: 1, + Method: "GET", + URL: server.URL + "{{URL_PATH}}", + Headers: map[string]string{ + "{{data.info.name}}": "{{data.info.age}}", + }, + } + + // Act + e, err := NewEngine(context.TODO(), h) + if err != nil { + t.Errorf("TestLoadRandomInfoFromData error occurred %v", err) + } + + err = e.Init() + if err != nil { + t.Errorf("TestLoadRandomInfoFromData error occurred %v", err) + } + + e.Start() + + if !requestCalled { + t.Errorf("TestLoadRandomInfoFromData test server has not been called, url path injection failed") + } + + if ageMap[kenan] != expectedKenanAge || ageMap[fatih] != expectedFatihAge { + t.Errorf("TestLoadRandomInfoFromData did not match") + } +} + // The test creates a web server with Certificate auth, // then it spawns an Engine and verifies that the auth was successfully passsed. func TestTLSMutualAuth(t *testing.T) { From bb22e19a594cf426853ac40dafbe7de92a80e73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Wed, 11 Jan 2023 14:46:09 +0000 Subject: [PATCH 11/24] add tests for rand operation and csv read --- config/config_testdata/config_data_csv.json | 47 +++++ config/config_testdata/data_json_payload.json | 7 + config/json_test.go | 37 ++++ .../scripting/extraction/json_test.go | 23 +++ .../scripting/injection/environment_test.go | 160 ++++++++++++++++++ 5 files changed, 274 insertions(+) create mode 100644 config/config_testdata/config_data_csv.json create mode 100644 config/config_testdata/data_json_payload.json diff --git a/config/config_testdata/config_data_csv.json b/config/config_testdata/config_data_csv.json new file mode 100644 index 00000000..c09694b7 --- /dev/null +++ b/config/config_testdata/config_data_csv.json @@ -0,0 +1,47 @@ +{ + "iteration_count": 4, + "load_type": "waved", + "duration": 1, + "steps": [ + { + "id": 2, + "url": "{{LOCAL}}/body", + "name": "JSON", + "method": "GET", + "others": { + "h2": false, + "keep-alive": true, + "disable-redirect": true, + "disable-compression": false + }, + "payload_file": "../config/config_testdata/data_json_payload.json", + "timeout": 10 + } + ], + "output": "stdout", + "env":{ + "HTTPBIN" : "https://httpbin.ddosify.com", + "LOCAL" : "http://localhost:8084", + "RANDOM_NAMES" : ["kenan","fatih","kursat","semih","sertac"] , + "RANDOM_INT" : [52,99,60,33], + "RANDOM_BOOL" : [true,true,true,false] + }, + "data":{ + "info": { + "path" : "../config/config_testdata/test.csv", + "src" : "local", + "delimiter": ";", + "vars": { + "0":{"tag":"name"}, + "1":{"tag":"city"}, + "2":{"tag":"team"}, + "3":{"tag":"payload", "type":"json"}, + "4":{"tag":"age", "type":"int"} + }, + "allowQuota" : true, + "order": "random", + "skipFirstLine" : true + } + }, + "debug" : false +} \ No newline at end of file diff --git a/config/config_testdata/data_json_payload.json b/config/config_testdata/data_json_payload.json new file mode 100644 index 00000000..ad89dd11 --- /dev/null +++ b/config/config_testdata/data_json_payload.json @@ -0,0 +1,7 @@ +{ + "name" : "{{data.info.name}}", + "team" : "{{data.info.team}}", + "city" : "{{data.info.city}}", + "payload" : "{{rand(data.info.payload)}}", + "age" : "{{data.info.age}}" +} \ No newline at end of file diff --git a/config/json_test.go b/config/json_test.go index 10986afe..2801252b 100644 --- a/config/json_test.go +++ b/config/json_test.go @@ -434,6 +434,43 @@ func TestCreateHammerCaptureEnvs(t *testing.T) { } } +func TestCreateHammerDataCsv(t *testing.T) { + t.Parallel() + jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_data_csv.json"), ConfigTypeJson) + + expectedRandom := true + + h, err := jsonReader.CreateHammer() + if err != nil { + t.Errorf("TestCreateHammerDataCsv error occurred: %v", err) + } + + csvData := h.Scenario.Data["info"] + + if !reflect.DeepEqual(csvData.Random, expectedRandom) { + t.Errorf("TestCreateHammerDataCsv got: %t expected: %t", csvData.Random, expectedRandom) + } + + expectedRow := map[string]interface{}{ + "name": "Kenan", + "city": "Tokat", + "team": "Galatasaray", + "payload": map[string]interface{}{ + "data": map[string]interface{}{ + "profile": map[string]interface{}{ + "name": "Kenan", + }, + }, + }, + "age": 25, + } + + if !reflect.DeepEqual(expectedRow, csvData.Rows[0]) { + t.Errorf("TestCreateHammerDataCsv got: %#v expected: %#v", csvData.Rows[0], expectedRow) + } + +} + func TestCreateHammerInvalidTarget(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_invalid_target.json"), ConfigTypeJson) diff --git a/core/scenario/scripting/extraction/json_test.go b/core/scenario/scripting/extraction/json_test.go index e0ef7683..38568c4f 100644 --- a/core/scenario/scripting/extraction/json_test.go +++ b/core/scenario/scripting/extraction/json_test.go @@ -238,6 +238,29 @@ func TestJsonExtract_JsonBoolArray(t *testing.T) { } } +func TestJsonExtract_ObjectArray(t *testing.T) { + expected := []map[string]interface{}{ + {"x": "cc"}, + } + payload := map[string]interface{}{ + "age": expected, + } + + byteSlice, _ := json.Marshal(payload) + je := jsonExtractor{} + val, _ := je.extractFromByteSlice(byteSlice, "age") + + if !reflect.DeepEqual(val, expected) { + t.Errorf("TestJsonExtract_JsonBoolArray failed, expected %#v, found %#v", expected, val) + } + + val, _ = je.extractFromString(string(byteSlice), "age") + + if !reflect.DeepEqual(val, expected) { + t.Errorf("TestJsonExtract_JsonBoolArray failed, expected %#v, found %#v", expected, val) + } +} + func TestJsonExtract_JsonPathNotFound(t *testing.T) { payload := map[string]interface{}{ "age": "24", diff --git a/core/scenario/scripting/injection/environment_test.go b/core/scenario/scripting/injection/environment_test.go index d643dd92..010d251d 100644 --- a/core/scenario/scripting/injection/environment_test.go +++ b/core/scenario/scripting/injection/environment_test.go @@ -108,3 +108,163 @@ func ExampleEnvironmentInjector() { fmt.Println(randInt) } } + +func TestRandomInjectionStringSlice(t *testing.T) { + replacer := EnvironmentInjector{} + replacer.Init() + + vals := []string{ + "Kenan", "Kursat", "Fatih", + } + + envs := map[string]interface{}{ + "vals": vals, + } + + val, err := replacer.getEnv(envs, "rand(vals)") + if err != nil { + t.Errorf("%v", err) + } + + found := false + + for _, n := range vals { + if reflect.DeepEqual(val, n) { + found = true + break + } + } + + if !found { + t.Errorf("rand method did not return one of the expecteds") + } +} + +func TestRandomInjectionBoolSlice(t *testing.T) { + replacer := EnvironmentInjector{} + replacer.Init() + + vals := []bool{ + true, false, true, + } + + envs := map[string]interface{}{ + "vals": vals, + } + + val, err := replacer.getEnv(envs, "rand(vals)") + if err != nil { + t.Errorf("%v", err) + } + + found := false + + for _, n := range vals { + if reflect.DeepEqual(val, n) { + found = true + break + } + } + + if !found { + t.Errorf("rand method did not return one of the expecteds") + } + +} + +func TestRandomInjectionIntSlice(t *testing.T) { + replacer := EnvironmentInjector{} + replacer.Init() + + vals := []int{ + 3, 55, 42, + } + + envs := map[string]interface{}{ + "vals": vals, + } + + val, err := replacer.getEnv(envs, "rand(vals)") + if err != nil { + t.Errorf("%v", err) + } + + found := false + + for _, n := range vals { + if reflect.DeepEqual(val, n) { + found = true + break + } + } + + if !found { + t.Errorf("rand method did not return one of the expecteds") + } + +} + +func TestRandomInjectionFloat64Slice(t *testing.T) { + replacer := EnvironmentInjector{} + replacer.Init() + + vals := []float64{ + 3.3, 55.23, 42.1, + } + + envs := map[string]interface{}{ + "vals": vals, + } + + val, err := replacer.getEnv(envs, "rand(vals)") + if err != nil { + t.Errorf("%v", err) + } + + found := false + + for _, n := range vals { + if reflect.DeepEqual(val, n) { + found = true + break + } + } + + if !found { + t.Errorf("rand method did not return one of the expecteds") + } + +} + +func TestRandomInjectionInterfaceSlice(t *testing.T) { + replacer := EnvironmentInjector{} + replacer.Init() + + vals := []interface{}{ + map[string]int{"s": 33}, + []string{"v", "c"}, + } + + envs := map[string]interface{}{ + "vals": vals, + } + + val, err := replacer.getEnv(envs, "rand(vals)") + if err != nil { + t.Errorf("%v", err) + } + + found := false + + for _, n := range vals { + if reflect.DeepEqual(val, n) { + found = true + break + } + } + + if !found { + t.Errorf("rand method did not return one of the expecteds") + } + +} From 6861b6e4d3363e795233642b6fd3722fccc2ab7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 12 Jan 2023 12:42:10 +0000 Subject: [PATCH 12/24] remove src from csv data conf --- config/csv.go | 18 ++++++++++------- config/csv_test.go | 48 ++++++++++++++++++++++++++++++++++++---------- config/json.go | 1 - 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/config/csv.go b/config/csv.go index b21d0472..ceacf3a4 100644 --- a/config/csv.go +++ b/config/csv.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "strconv" ) @@ -14,9 +15,6 @@ func validateConf(conf CsvConf) error { if !(conf.Order == "random" || conf.Order == "sequential") { return fmt.Errorf("unsupported order %s, should be random|sequential", conf.Order) } - if !(conf.Src == "local" || conf.Src == "remote") { - return fmt.Errorf("unsupported src %s, should be local|remote", conf.Order) - } return nil } @@ -28,15 +26,14 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { var reader io.Reader - if conf.Src == "local" { + if _, err = os.Stat(conf.Path); err == nil { // local file path f, err := os.Open(conf.Path) if err != nil { return nil, err } reader = f defer f.Close() - - } else if conf.Src == "remote" { + } else if _, err = url.ParseRequestURI(conf.Path); err == nil { // url req, err := http.NewRequest(http.MethodGet, conf.Path, nil) if err != nil { return nil, err @@ -47,6 +44,8 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { } reader = resp.Body defer resp.Body.Close() + } else { + return nil, fmt.Errorf("given path is neither local path nor url") } // read csv values using csv.Reader @@ -71,11 +70,16 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { continue } x := map[string]interface{}{} - for index, tag := range conf.Vars { // "0":"name", "1":"city","2":"team" + for index, tag := range conf.Vars { i, err := strconv.Atoi(index) if err != nil { return nil, err } + + if i >= len(row) { + return nil, fmt.Errorf("index number out of range, check your vars or delimiter") + } + // convert var val interface{} switch tag.Type { diff --git a/config/csv_test.go b/config/csv_test.go index 7dcee520..84855735 100644 --- a/config/csv_test.go +++ b/config/csv_test.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "reflect" "strings" "testing" @@ -10,7 +11,6 @@ func TestValidateCsvConf(t *testing.T) { t.Parallel() conf := CsvConf{ Path: "", - Src: "", Delimiter: "", SkipFirstLine: false, Vars: map[string]Tag{}, @@ -25,21 +25,12 @@ func TestValidateCsvConf(t *testing.T) { if err == nil { t.Errorf("TestValidateCsvConf should be errored") } - - conf.Order = "random" - conf.Src = "invalidSrc" - err = validateConf(conf) - - if err == nil { - t.Errorf("TestValidateCsvConf should be errored") - } } func TestReadCsv(t *testing.T) { t.Parallel() conf := CsvConf{ Path: "config_testdata/test.csv", - Src: "local", Delimiter: ";", SkipFirstLine: true, Vars: map[string]Tag{ @@ -102,3 +93,40 @@ func TestReadCsv(t *testing.T) { t.Errorf("TestReadCsv found: %#v , expected: %#v", secondPayload, expectedPayload2) } } + +var table = []struct { + conf CsvConf + latency float64 +}{ + { + conf: CsvConf{ + Path: "config_testdata/test.csv", + Delimiter: ";", + SkipFirstLine: true, + Vars: map[string]Tag{ + "0": {Tag: "name", Type: "string"}, + "3": {Tag: "payload", Type: "json"}, + "4": {Tag: "age", Type: "int"}, + "5": {Tag: "percent", Type: "float"}, + "6": {Tag: "boolField", Type: "bool"}, + }, + SkipEmptyLine: true, + AllowQuota: true, + Order: "sequential", + }, + }, +} + +func TestBenchmarkCsvRead(t *testing.T) { + for _, v := range table { + + res := testing.Benchmark(func(b *testing.B) { + for i := 0; i < b.N; i++ { + readCsv(v.conf) + } + }) + + fmt.Printf("ns:%d", res.T.Nanoseconds()) + fmt.Printf("N:%d", res.N) + } +} diff --git a/config/json.go b/config/json.go index 6434ea9a..612ee237 100644 --- a/config/json.go +++ b/config/json.go @@ -122,7 +122,6 @@ func (t *Tag) UnmarshalJSON(data []byte) error { type CsvConf struct { Path string `json:"path"` - Src string `json:"src"` Delimiter string `json:"delimiter"` SkipFirstLine bool `json:"skipFirstLine"` Vars map[string]Tag `json:"vars"` // "0":"name", "1":"city","2":"team" From 9d8a1f88ed5a0a624ba5ffc83332e66db52d4af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 12 Jan 2023 15:01:40 +0000 Subject: [PATCH 13/24] test data readme --- README.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d3988fec..8d09a996 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,11 @@ This section aims to show you how to use Ddosify without deep dive into its deta ddosify -config ddosify_config_correlation.json Ddosify allows you to specify variables at the global level and use them throughout the scenario, as well as extract variables from previous steps and inject them to the next steps in each iteration individually. You can inject those variables in requests *url*, *headers* and *payload(body)*. The example config can be found in [correlation-config-example](#Correlation). + +7. ### Test Data + + ddosify -config ddosify_data_csv.json + Ddosify allows you to load test data from a file, tag specific columns for later use. You can inject those variables in requests *url*, *headers* and *payload(body)*. The example config can be found in [test-data-example](##Test-Data). ## Details You can configure your load test by the CLI options or a config file. Config file supports more features than the CLI. For example, you can't create a scenario-based load test with CLI options. @@ -262,18 +267,32 @@ There is an example config file at [config_examples/config.json](/config_example - `env` *optional* Scenario-scoped global variables. Note that dynamic variables changes every iteration. ```json - "steps": [ - { - "id": 1, - "url": "http://target.com/endpoint1", - "env": { - "COMPANY_NAME" :"Ddosify", - "randomCountry" : "{{_randomCountry}}" - } - }, - ] + "env": { + "COMPANY_NAME" :"Ddosify", + "randomCountry" : "{{_randomCountry}}" + } + ``` +- `data` *optional* + Config for loading test data from a csv file. + ```json + "data":{ + "info": { + "path" : "config/config_testdata/test.csv", + "delimiter": ";", + "vars": { + "0":{"tag":"name"}, + "1":{"tag":"city"}, + "2":{"tag":"team"}, + "3":{"tag":"payload", "type":"json"}, + "4":{"tag":"age", "type":"int"} + }, + "allowQuota" : true, + "order": "random|sequential", + "skipFirstLine" : true, + "skipEmptyLine" : true + } + } ``` - - `steps` *mandatory* This parameter lets you create your scenario. Ddosify runs the provided steps, respectively. For the given example file step id: 2 will be executed immediately after the response of step id: 1 is received. The order of the execution is the same as the order of the steps in the config file. @@ -662,7 +681,39 @@ ddosify -config ddosify_config_correlation.json -debug } ``` +## Test Data Set +Ddosify enables you to load test data from **csv** files. Later, in your scenario, you can inject variables that you tagged. +```json +// config_data_csv.json +"data":{ + "csv_test": { + "path" : "config/config_testdata/test.csv", + "delimiter": ";", + "vars": { + "0":{"tag":"name"}, + "1":{"tag":"city"}, + "2":{"tag":"team"}, + "3":{"tag":"payload", "type":"json"}, + "4":{"tag":"age", "type":"int"} + }, + "allowQuota" : true, + "order": "random", + "skipFirstLine" : true + } + } +``` +You can refer to tagged variables in your request like below. +```json +// payload.json +{ + "name" : "{{data.csv_test.name}}", + "team" : "{{data.csv_test.team}}", + "city" : "{{data.csv_test.city}}", + "payload" : "{{data.csv_test.payload}}", + "age" : "{{data.csv_test.age}}" +} +``` ## Common Issues ### macOS Security Issue From 1ba5318182147882b4e7a0964fce93adee032912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Thu, 12 Jan 2023 15:08:30 +0000 Subject: [PATCH 14/24] add rand to readme --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8d09a996..674e8bac 100644 --- a/README.md +++ b/README.md @@ -606,7 +606,8 @@ ddosify -config ddosify_config_correlation.json -debug "TARGET_URL" : "http://localhost:8084/hello", "USER_KEY" : "ABC", "COMPANY_NAME" : "Ddosify", - "RANDOM_COUNTRY" : "{{_randomCountry}}" + "RANDOM_COUNTRY" : "{{_randomCountry}}", + "NUMBERS" : [22,33,10,52] }, } ``` @@ -614,6 +615,7 @@ ddosify -config ddosify_config_correlation.json -debug ### :hammer: Overall Config and Injection +On array-like captured variables or environment vars, the **rand( )** function can be utilized. ```json // ddosify_config_correlation.json { @@ -626,7 +628,8 @@ ddosify -config ddosify_config_correlation.json -debug "url": "{{TARGET_URL}}", "method": "POST", "headers": { - "User-Key": "{{USER_KEY}}" + "User-Key": "{{USER_KEY}}", + "Rand-Selected-Num" : "{{rand(NUMBERS)}}" }, "payload" : "{{COMPANY_NAME}}", "captureEnv": { @@ -659,7 +662,8 @@ ddosify -config ddosify_config_correlation.json -debug "TARGET_URL" : "http://localhost:8084/hello", "USER_KEY" : "ABC", "COMPANY_NAME" : "Ddosify", - "RANDOM_COUNTRY" : "{{_randomCountry}}" + "RANDOM_COUNTRY" : "{{_randomCountry}}", + "NUMBERS" : [22,33,10,52] }, } ``` From f8f17ebeb8b11300f6167e8d8c5c60f4f4fea07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 13 Jan 2023 08:14:39 +0000 Subject: [PATCH 15/24] fix readme test data link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 674e8bac..a03dde24 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ This section aims to show you how to use Ddosify without deep dive into its deta 7. ### Test Data ddosify -config ddosify_data_csv.json - Ddosify allows you to load test data from a file, tag specific columns for later use. You can inject those variables in requests *url*, *headers* and *payload(body)*. The example config can be found in [test-data-example](##Test-Data). + Ddosify allows you to load test data from a file, tag specific columns for later use. You can inject those variables in requests *url*, *headers* and *payload(body)*. The example config can be found in [test-data-example](##test-data-set). ## Details You can configure your load test by the CLI options or a config file. Config file supports more features than the CLI. For example, you can't create a scenario-based load test with CLI options. From 85649892bab41cc2469a274a88ef0a2db408dc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 13 Jan 2023 08:16:25 +0000 Subject: [PATCH 16/24] fix readme test data link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a03dde24..9eed4d50 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ This section aims to show you how to use Ddosify without deep dive into its deta 7. ### Test Data ddosify -config ddosify_data_csv.json - Ddosify allows you to load test data from a file, tag specific columns for later use. You can inject those variables in requests *url*, *headers* and *payload(body)*. The example config can be found in [test-data-example](##test-data-set). + Ddosify allows you to load test data from a file, tag specific columns for later use. You can inject those variables in requests *url*, *headers* and *payload(body)*. The example config can be found in [test-data-example](#test-data-set). ## Details You can configure your load test by the CLI options or a config file. Config file supports more features than the CLI. For example, you can't create a scenario-based load test with CLI options. From 63e1c13858922e1193bca9819a3be0e018187e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 13 Jan 2023 11:59:28 +0000 Subject: [PATCH 17/24] update readme and change csv cast error --- README.md | 19 +++++-- config/config_testdata/test.csv | 93 --------------------------------- config/csv.go | 11 ++-- 3 files changed, 23 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 9eed4d50..c6c49669 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,8 @@ There is an example config file at [config_examples/config.json](/config_example } ``` - `data` *optional* - Config for loading test data from a csv file. + Config for loading test data from a csv file. + [Csv data](https://github.com/ddosify/ddosify/tree/master/config/config_testdata/test.csv) used in below config. ```json "data":{ "info": { @@ -287,12 +288,22 @@ There is an example config file at [config_examples/config.json](/config_example "4":{"tag":"age", "type":"int"} }, "allowQuota" : true, - "order": "random|sequential", + "order": "sequential", "skipFirstLine" : true, "skipEmptyLine" : true } } - ``` + ``` + | Field | Description | Type | Default | Required? | + | ------ | -------------------------------------------------------- | ------ | ------- | --------- | + | `path` | Local path or remote url for your csv file | `string` | - | Yes | + | `delimiter` | Delimiter for reading csv | `string` | `,` | No | + | `vars` | Tag columns using column index as key, use `type` field if you want to cast a column to a specific type, default is `string`, can be one of the following: `json`, `int`, `float`,`bool`. | `map` | - | Yes | + | `allowQuota` | If set to true, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field | `bool` | `false` | No | + | `order` | Order of reading records from csv. Can be `random` or `sequential` | `string` | `random` | No | + | `skipFirstLine` | Skips first line while reading records from csv. | `bool` | `false` | No | + | `skipEmptyLine` | Skips empty lines while reading records from csv. | `bool` | `true` | No | + - `steps` *mandatory* This parameter lets you create your scenario. Ddosify runs the provided steps, respectively. For the given example file step id: 2 will be executed immediately after the response of step id: 1 is received. The order of the execution is the same as the order of the steps in the config file. @@ -688,6 +699,8 @@ On array-like captured variables or environment vars, the **rand( )** function c ## Test Data Set Ddosify enables you to load test data from **csv** files. Later, in your scenario, you can inject variables that you tagged. +We are using this [csv data](https://github.com/ddosify/ddosify/tree/master/config/config_testdata/test.csv) in below config. + ```json // config_data_csv.json "data":{ diff --git a/config/config_testdata/test.csv b/config/config_testdata/test.csv index 6936a2c4..20f97825 100644 --- a/config/config_testdata/test.csv +++ b/config/config_testdata/test.csv @@ -4,97 +4,4 @@ Fatih;Bolu;Galatasaray;[5,6,7];29;44.3;false;;; Kursat;Samsun;Besiktas;{"a":"b"};28;12.54;True;;; Semih;Duzce;Besiktas;{"a":"b"};27;663.67;False;;; ;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; -;;;;;;;;; ;;;;;;;;; \ No newline at end of file diff --git a/config/csv.go b/config/csv.go index ceacf3a4..043dfb12 100644 --- a/config/csv.go +++ b/config/csv.go @@ -84,24 +84,27 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { var val interface{} switch tag.Type { case "json": - json.Unmarshal([]byte(row[i]), &val) + err := json.Unmarshal([]byte(row[i]), &val) + if err != nil { + return nil, fmt.Errorf("can not convert %s to json,%v", row[i], err) + } case "int": var err error val, err = strconv.Atoi(row[i]) if err != nil { - return nil, err + return nil, fmt.Errorf("can not convert %s to int,%v", row[i], err) } case "float": var err error val, err = strconv.ParseFloat(row[i], 64) if err != nil { - return nil, err + return nil, fmt.Errorf("can not convert %s to float,%v", row[i], err) } case "bool": var err error val, err = strconv.ParseBool(row[i]) if err != nil { - return nil, err + return nil, fmt.Errorf("can not convert %s to bool,%v", row[i], err) } default: val = row[i] From b8b0e75387c7c92b2078f2b58c98fbfcf3153cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 13 Jan 2023 12:17:10 +0000 Subject: [PATCH 18/24] refactor data path errors --- config/csv.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/config/csv.go b/config/csv.go index 043dfb12..eddda833 100644 --- a/config/csv.go +++ b/config/csv.go @@ -26,14 +26,7 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { var reader io.Reader - if _, err = os.Stat(conf.Path); err == nil { // local file path - f, err := os.Open(conf.Path) - if err != nil { - return nil, err - } - reader = f - defer f.Close() - } else if _, err = url.ParseRequestURI(conf.Path); err == nil { // url + if _, err = url.ParseRequestURI(conf.Path); err == nil { // url req, err := http.NewRequest(http.MethodGet, conf.Path, nil) if err != nil { return nil, err @@ -42,10 +35,21 @@ func readCsv(conf CsvConf) ([]map[string]interface{}, error) { if err != nil { return nil, err } + + if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) { + return nil, fmt.Errorf("request to remote url failed: %d", resp.StatusCode) + } reader = resp.Body defer resp.Body.Close() + } else if _, err = os.Stat(conf.Path); err == nil { // local file path + f, err := os.Open(conf.Path) + if err != nil { + return nil, err + } + reader = f + defer f.Close() } else { - return nil, fmt.Errorf("given path is neither local path nor url") + return nil, err } // read csv values using csv.Reader From bb410248cdf999ce8b792b0903950bc0e1da14d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 13 Jan 2023 13:50:25 +0000 Subject: [PATCH 19/24] seperate test data from env in report --- core/report/debug.go | 15 ++++++++++++++- core/report/stdout.go | 24 ++++++++++++++++++++++++ core/report/stdoutJson.go | 6 ++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/core/report/debug.go b/core/report/debug.go index f1f32ba5..7ed2e972 100644 --- a/core/report/debug.go +++ b/core/report/debug.go @@ -28,6 +28,7 @@ type verboseHttpRequestInfo struct { Request verboseRequest `json:"request"` Response verboseResponse `json:"response"` Envs map[string]interface{} `json:"envs"` + TestData map[string]interface{} `json:"ffff"` FailedCaptures map[string]string `json:"failedCaptures"` Error string `json:"error"` } @@ -74,7 +75,19 @@ func ScenarioStepResultToVerboseHttpRequestInfo(sr *types.ScenarioStepResult) ve Body: responseBody, } } - verboseInfo.Envs = sr.UsableEnvs + + envs := make(map[string]interface{}) + testData := make(map[string]interface{}) + for key, val := range sr.UsableEnvs { + if strings.HasPrefix(key, "data.") { + testData[key] = val + } else { + envs[key] = val + } + } + + verboseInfo.Envs = envs + verboseInfo.TestData = testData verboseInfo.FailedCaptures = sr.FailedCaptures return verboseInfo } diff --git a/core/report/stdout.go b/core/report/stdout.go index 5e5dfae4..a777716b 100644 --- a/core/report/stdout.go +++ b/core/report/stdout.go @@ -170,6 +170,30 @@ func (s *stdout) printInDebugMode(input chan *types.ScenarioResult) { fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), fmt.Sprint(eVal)) } } + fmt.Fprintf(w, "\n") + fmt.Fprintf(w, "%s\n", blue(fmt.Sprintf("- Test Data"))) + + for eKey, eVal := range verboseInfo.TestData { + switch eVal.(type) { + case map[string]interface{}: + valPretty, _ := json.Marshal(eVal) + fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) + case []int: + valPretty, _ := json.Marshal(eVal) + fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) + case []string: + valPretty, _ := json.Marshal(eVal) + fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) + case []float64: + valPretty, _ := json.Marshal(eVal) + fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) + case []bool: + valPretty, _ := json.Marshal(eVal) + fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) + default: + fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), fmt.Sprint(eVal)) + } + } if verboseInfo.Error != "" && isVerboseInfoRequestEmpty(verboseInfo.Request) { fmt.Fprintf(w, "%s Error: \t%-5s \n", emoji.SosButton, verboseInfo.Error) diff --git a/core/report/stdoutJson.go b/core/report/stdoutJson.go index 7bb2cdf1..768c5630 100644 --- a/core/report/stdoutJson.go +++ b/core/report/stdoutJson.go @@ -164,6 +164,7 @@ func (v verboseHttpRequestInfo) MarshalJSON() ([]byte, error) { StepId uint16 `json:"stepId"` StepName string `json:"stepName"` Envs map[string]interface{} `json:"envs"` + TestData map[string]interface{} `json:"testData"` FailedCaptures map[string]string `json:"failedCaptures"` Error string `json:"error"` } @@ -174,6 +175,7 @@ func (v verboseHttpRequestInfo) MarshalJSON() ([]byte, error) { StepName: v.StepName, FailedCaptures: v.FailedCaptures, Envs: v.Envs, + TestData: v.TestData, } return json.Marshal(a) } @@ -183,6 +185,7 @@ func (v verboseHttpRequestInfo) MarshalJSON() ([]byte, error) { StepId uint16 `json:"stepId"` StepName string `json:"stepName"` Envs map[string]interface{} `json:"envs"` + TestData map[string]interface{} `json:"testData"` FailedCaptures map[string]string `json:"failedCaptures"` Request struct { Url string `json:"url"` @@ -200,6 +203,7 @@ func (v verboseHttpRequestInfo) MarshalJSON() ([]byte, error) { StepName: v.StepName, FailedCaptures: v.FailedCaptures, Envs: v.Envs, + TestData: v.TestData, } return json.Marshal(a) } @@ -208,6 +212,7 @@ func (v verboseHttpRequestInfo) MarshalJSON() ([]byte, error) { StepId uint16 `json:"stepId"` StepName string `json:"stepName"` Envs map[string]interface{} `json:"envs"` + TestData map[string]interface{} `json:"testData"` FailedCaptures map[string]string `json:"failedCaptures"` Request struct { Url string `json:"url"` @@ -229,6 +234,7 @@ func (v verboseHttpRequestInfo) MarshalJSON() ([]byte, error) { Response: v.Response, FailedCaptures: v.FailedCaptures, Envs: v.Envs, + TestData: v.TestData, } return json.Marshal(a) From 2b3ab6b9c401653b60d9821992c620f950dfcd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenan=20Faruk=20=C3=87ak=C4=B1r?= Date: Fri, 13 Jan 2023 14:22:56 +0000 Subject: [PATCH 20/24] update config example --- config_examples/config.json | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/config_examples/config.json b/config_examples/config.json index ccdb3c52..d5e26eb0 100644 --- a/config_examples/config.json +++ b/config_examples/config.json @@ -10,6 +10,31 @@ {"duration": 6, "count": 10}, {"duration": 7, "count": 20} ], + "envs" : { + "HTTPBIN" : "https://httpbin.ddosify.com", + "LOCAL" : "http://localhost:8084", + "NAMES" : ["kenan","fatih","kursat","semih","sertac"] , + "NUMBERS" : [52,99,60,33], + "BOOLS" : [true,true,true,false], + "randomIntPerIteration": "{{_randomInt}}" + }, + "data":{ + "info": { + "path" : "config/config_testdata/test.csv", + "delimiter": ";", + "vars": { + "0":{"tag":"name"}, + "1":{"tag":"city"}, + "2":{"tag":"team"}, + "3":{"tag":"payload", "type":"json"}, + "4":{"tag":"age", "type":"int"} + }, + "allowQuota" : true, + "order": "random", + "skipFirstLine" : true, + "skipEmptyLine" : true + } + }, "proxy": "http://proxy_host.com:proxy_port", "output": "stdout", "steps": [ @@ -33,15 +58,23 @@ "disableCompression": false, "h2": true, "disable-redirect": true + }, + "captureEnv": { + "NUM" :{ "from":"body","jsonPath":"num"} } }, { "id": 2, - "url": "https://test_site1.com/endpoint_2", + "url": "{{LOCAL}}", "method": "GET", "payload_file": "config_examples/payload.txt", "timeout": 2, - "sleep": "1000" + "sleep": "1000", + "headers":{ + "num": "{{NUM}}", + "randNum": "{{rand(NUMBERS)}}", + "randInt" : "{{randomIntPerIteration}}" + } }, { "id": 3, From a476779f760869b2dd82c219c3e3ed09c2376e46 Mon Sep 17 00:00:00 2001 From: fatihbaltaci Date: Mon, 16 Jan 2023 17:51:05 +0300 Subject: [PATCH 21/24] Update goreleaser --- .goreleaser.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index f45f203e..29fff213 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -17,7 +17,7 @@ builds: goarm: - 6 ldflags: - - -s -w -X main.GitVersion={{ .Version }} -X main.GitCommit={{ .ShortCommit }} -X main.BuildDate={{ .CommitDate }} + - -s -w -X main.GitVersion={{ .Tag }} -X main.GitCommit={{ .ShortCommit }} -X main.BuildDate={{ .CommitDate }} ignore: - goos: darwin goarch: 386 @@ -50,7 +50,7 @@ universal_binaries: checksum: name_template: 'checksums.txt' snapshot: - name_template: "{{ incpatch .Version }}-next" + name_template: "{{ incpatch .Tag }}-next" changelog: sort: asc use: github @@ -92,7 +92,7 @@ dockers: - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.name={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.version={{.Tag}}" - "--label=org.opencontainers.image.source={{.GitURL}}" - "--platform=linux/amd64" extra_files: @@ -106,7 +106,7 @@ dockers: - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.name={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.version={{.Tag}}" - "--label=org.opencontainers.image.source={{.GitURL}}" - "--platform=linux/arm64" extra_files: From 34736fae461665c766532ba956df39712dc642ff Mon Sep 17 00:00:00 2001 From: fatihbaltaci Date: Mon, 16 Jan 2023 15:24:39 +0000 Subject: [PATCH 22/24] Update jenkinsfile --- Jenkinsfile_benchmark | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile_benchmark b/Jenkinsfile_benchmark index 51b1bddf..6c776f92 100644 --- a/Jenkinsfile_benchmark +++ b/Jenkinsfile_benchmark @@ -24,6 +24,7 @@ pipeline { allOf { expression { env.CHANGE_ID != null } expression { env.CHANGE_TARGET != null } + expression { env.BRANCH_NAME != "develop" } } } steps { From a7790687b71165e8576d028ad8987ea8f24a0c5a Mon Sep 17 00:00:00 2001 From: fatihbaltaci Date: Mon, 16 Jan 2023 15:34:20 +0000 Subject: [PATCH 23/24] Update jenkinsfile --- Jenkinsfile_benchmark | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile_benchmark b/Jenkinsfile_benchmark index 6c776f92..04d8f2fe 100644 --- a/Jenkinsfile_benchmark +++ b/Jenkinsfile_benchmark @@ -24,7 +24,7 @@ pipeline { allOf { expression { env.CHANGE_ID != null } expression { env.CHANGE_TARGET != null } - expression { env.BRANCH_NAME != "develop" } + expression { env.BRANCH_NAME != 'develop' } } } steps { From fa4c744c910698d9349f3eec90d0018e7012388c Mon Sep 17 00:00:00 2001 From: fatihbaltaci Date: Mon, 16 Jan 2023 15:36:24 +0000 Subject: [PATCH 24/24] Update jenkinsfile --- Jenkinsfile_benchmark | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile_benchmark b/Jenkinsfile_benchmark index 04d8f2fe..d66bb898 100644 --- a/Jenkinsfile_benchmark +++ b/Jenkinsfile_benchmark @@ -24,7 +24,7 @@ pipeline { allOf { expression { env.CHANGE_ID != null } expression { env.CHANGE_TARGET != null } - expression { env.BRANCH_NAME != 'develop' } + expression { env.CHANGE_BRANCH != 'develop' } } } steps {