package main import ( "bytes" "crypto/tls" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "reflect" "strconv" "strings" ) // RunTest Executes each individual test case func RunTest(testCase *TestCase) error { var err error var requestUrl string var requestType string var req *http.Request //var bodyMap map[string]interface{} tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: testDefinitions.Insecure}, } client := &http.Client{Transport: tr} fmt.Printf("\nRunning %s : %s\n\n", testCase.Name, testCase.Description) // Determine URL if len(testDefinitions.BaseUrl) > 0 { //fmt.Printf("Joining '%s' with '%s'\n", testDefinitions.BaseUrl, testCase.Path) requestUrl, err = url.JoinPath(testDefinitions.BaseUrl, testCase.Path) if err != nil { errMessage := fmt.Sprintf("error combining request URL : '%s'\n", err) return errors.New(errMessage) } decoded, err := url.QueryUnescape(requestUrl) if err != nil { errMessage := fmt.Sprintf("error unescaping request URL : '%s'\n", err) return errors.New(errMessage) } requestUrl = decoded //fmt.Printf("url path decoded '%s'\n", decoded) } else { requestUrl = testCase.Path } // Populate request body // Get request type, default to GET if len(testCase.Method) == 0 { //return errors.New("no test method specifed, must be either GET or POST") // Assume GET if no method specified requestType = "GET" } else { requestType = testCase.Method } fmt.Printf("Preparing %s for endpoint %s\n", requestType, requestUrl) // Create request if len(testCase.Body) > 0 { // unmarshal testCase.Body so we can search for replacements var jsonBody map[string]interface{} err = json.Unmarshal(testCase.Body, &jsonBody) if err != nil { error := fmt.Sprintf("Error processing request body for test case : '%s'\n", err) panic(error) } // iterate through the key-value pairs and check for replacements //fmt.Printf("Capture data : '%+v'\n", captureValues.Data) BodyReplaceCapture(jsonBody, captureValues.Data) // turn the unmarshalled object back into a []byte so that we can send it modifiedJsonData, err := json.Marshal(jsonBody) if err != nil { panic(err) } // Check for capture tokens that need to be replaced before adding the value //newBody := ReplaceCaptures(string(testCase.Body[:])) fmt.Printf("Adding body to request : %s\n", modifiedJsonData) req, err = http.NewRequest(requestType, requestUrl, bytes.NewBuffer(modifiedJsonData)) if err != nil { errMessage := fmt.Sprintf("error submitting request : '%s'\n", err) return errors.New(errMessage) } } else { req, err = http.NewRequest(requestType, requestUrl, nil) } if err != nil { errMessage := fmt.Sprintf("error submitting request : '%s'\n", err) return errors.New(errMessage) } // Assemble headers // Global headers first if len(testDefinitions.Headers) > 0 { //fmt.Printf("Adding global headers to request\n") for k, v := range testDefinitions.Headers { // Check for capture tokens that need to be replaced before adding the value key := HeaderReplaceCaptures(k) val := HeaderReplaceCaptures(v) //fmt.Printf("Add global header %s = %s\n", key, val) req.Header.Add(key, val) } } // Then any test case specific headers for k, v := range testCase.Header { // Check for capture tokens that need to be replaced before adding the value key := HeaderReplaceCaptures(k) val := HeaderReplaceCaptures(v) //fmt.Printf("Add header %s = %s\n", key, val) req.Header.Add(key, val) } // Perform request fmt.Printf("Sending request\n") resp, err := client.Do(req) if err != nil { fmt.Println(err) return err } defer resp.Body.Close() // Store response testCase.ResultStatusCode = resp.StatusCode testCase.ResultHeaders = resp.Header fmt.Printf("Status Code: %s\n", resp.Status) fmt.Printf("Header response:\n%+v\n", testCase.ResultHeaders) body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println(err) return err } testCase.ResultBody = string(body) fmt.Printf("Body response:\n%s\n", testCase.ResultBody) // We have no idea what the response will look like so use an interface and unmarshal manually as needed if len(body) > 0 { var b interface{} json.Unmarshal(body, &b) if b != nil { testCase.ResultBodyMap = b.(map[string]interface{}) //fmt.Printf("Body map: '%v'\n", testCase.ResultBodyMap) //bodyMap = b.(map[string]interface{}) } } // Iterate through testDefinitions.CaptureCases and see if there are any that match this test // If we found one, add the appropriate replacement to captureValues for i := range testDefinitions.CaptureCases { if testDefinitions.CaptureCases[i].TestCaseName == testCase.Name { fmt.Printf("\nProcessing capture(s) for test case '%s'\n", testCase.Name) captureData := testDefinitions.CaptureCases[i].CaptureData for k, v := range captureData.Body.Data { fmt.Printf("Searching body response for match on capture string '%s'\n", k) results := findKey(k, testCase.ResultBodyMap) //fmt.Printf("Results : '%v'\n", results) if len(results) > 0 { //fmt.Printf("Found %d results but only storing the first one\n", len(results)) // Get the type of the first element valueType := reflect.TypeOf(results[0]) // Check if the type is not string if valueType.Kind() != reflect.String { // Convert the value to string results[0] = fmt.Sprintf("%v", results[0]) } fmt.Printf("Storing capture '%s' = '%s'\n", k, results[0].(string)) captureValues.Data[v] = results[0].(string) } } for k, v := range captureData.Header.Data { fmt.Printf("Searching header response for match on capture string '%s'\n", k) results := findKey(k, testCase.ResultBodyMap) //fmt.Printf("Results : '%v'\n", results) if len(results) > 0 { //fmt.Printf("Found %d results but only storing the first one\n", len(results)) // Get the type of the first element valueType := reflect.TypeOf(results[0]) // Check if the type is not string if valueType.Kind() != reflect.String { // Convert the value to string results[0] = fmt.Sprintf("%v", results[0]) } fmt.Printf("Storing capture '%s' = '%s'\n", k, results[0].(string)) captureValues.Data[v] = results[0].(string) } } // Capture anything needed break } } // No errors if we reached this point return nil } // HeaderReplaceCaptures iterates through captureValues to apply any text replacements necessary for input string func HeaderReplaceCaptures(input string) string { for k, v := range captureValues.Data { input = strings.Replace(input, k, v, -1) /* if strings.Contains(input, k) { //fmt.Printf("Found key '%s' in input string '%s', replacing with '%s'\n", k, input, v) } */ } return input } // Search unmarshalled json data for any replacements to make from data already captured by previous tests func BodyReplaceCapture(data interface{}, captureValues map[string]string) { switch v := data.(type) { case map[string]interface{}: for key, val := range v { //fmt.Printf("NewReplaceCapture checking '%v' = '%v'\n", key, val) // Check if key needs to be replaced if replaceKey, ok := captureValues[key]; ok { //fmt.Printf("Replacing key '%s'\n", key) delete(v, key) v[replaceKey] = val key = replaceKey } // Check if value needs to be replaced if strVal, ok := val.(string); ok { //fmt.Printf("Checking value '%s'\n", strVal) if replaceVal, ok := captureValues[strVal]; ok { if strings.HasPrefix(strVal, "int:") { fmt.Printf("Replacing capture token '%s' with int version of '%s'\n", strVal, replaceVal) i, err := strconv.Atoi(replaceVal) if err != nil { panic(err) } v[key] = i } else { fmt.Printf("Replacing capture token '%s' with '%s'\n", strVal, replaceVal) v[key] = replaceVal } } } if replaceVal, ok := captureValues[key]; ok { //fmt.Printf("Replacing value '%s'\n", captureValues[key]) v[key] = replaceVal } BodyReplaceCapture(val, captureValues) } case []interface{}: for i, val := range v { fmt.Printf("i val : '%v' = '%v'\n", i, val) BodyReplaceCapture(val, captureValues) v[i] = val } } }