package main import ( "encoding/binary" "encoding/json" "fmt" "math/rand" "net" "reflect" "github.com/iancoleman/orderedmap" ) func ReadInput(data []byte, testDefinitions *TestDefinitions) { var err error // Unmarshal the json into an orderedmap to preserve the ordering of json structure o := orderedmap.New() err = json.Unmarshal([]byte(data), &o) if err != nil { error := fmt.Sprintf("JSON Unmarshal error %s\n", err) panic(error) } topLevel := o.Keys() //fmt.Printf("Found %d top-level keys in json data\n", len(topLevel)) // TODO : Check required top level keys are present if len(topLevel) <= 1 { error := "Missing required keys in input json" panic(error) } // Get the name of the group of tests if data, ok := o.Get("name"); ok { testDefinitions.Name = data.(string) //fmt.Printf("test name: '%s'\n", testDefinitions.Name) } // Get the base url if defined if data, ok := o.Get("url"); ok { testDefinitions.BaseUrl = data.(string) } // Check for any headers to include with all requests if data, ok := o.Get("header"); ok { headerMap := data.(orderedmap.OrderedMap) testDefinitions.Headers = OrderedToStringMap(headerMap) } // Check if we should ignore certificate errors if data, ok := o.Get("insecure"); ok { testDefinitions.Insecure = data.(bool) } // Get a reference to the node containing each of the test cases testsInterface, ok := o.Get("testCases") if !ok { panic("No key defining test cases found\n") } // Process the "capture" settings that allow us to use values from one test case in subsequent test cases // each capture node has a name that links it to the test case that captures that data if capture, ok := o.Get("capture"); ok { captureMap := capture.(orderedmap.OrderedMap) captureKeys := captureMap.Keys() for _, outerKey := range captureKeys { // Define a new capture case object to record these details thisCaptureCase := new(CaptureCase) // Make sure we can access the capture defintiion if captureCase, ok := captureMap.Get(outerKey); ok { // Store the test case name that this capture will come from thisCaptureCase.TestCaseName = outerKey //fmt.Printf("[%d] : Capture from %s\n", i, outerKey) // Get capture data from body thisCaptureCaseMap := captureCase.(orderedmap.OrderedMap) if body, ok := thisCaptureCaseMap.Get("body"); ok { //fmt.Printf("This capture case has body defined\n") bodyMap := body.(orderedmap.OrderedMap) bodyMapKeys := bodyMap.Keys() bodyData := make(map[string]string) for _, bodyKey := range bodyMapKeys { if bodyVal, ok := bodyMap.Get(bodyKey); ok { switch vType := bodyVal.(type) { case string: //fmt.Printf("Capturing '%s' from test result and using '%s' for replacement token\n", bodyKey, bodyVal.(string)) // Store capture info bodyData[bodyKey] = bodyVal.(string) default: fmt.Printf("received unexpected value type, %T\n", vType) } } } captureBody := new(CaptureBody) captureBody.Data = bodyData thisCaptureCase.CaptureData.Body = *captureBody } // TODO : header } testDefinitions.CaptureCases = append(testDefinitions.CaptureCases, *thisCaptureCase) } } // Get the keys for the first test so we know what config options have been specified testCasesMap := testsInterface.(orderedmap.OrderedMap) testCasesKeys := testCasesMap.Keys() // Parse json into our testCaseDefinition // Parse each key into our config struct fmt.Printf("Loading %d test cases\n", len(testCasesKeys)) for _, outerKey := range testCasesKeys { //fmt.Printf("Test %d : %s\n", i, outerKey) // Get the name of the test case thisTestCase := new(TestCase) thisTestCase.Name = outerKey if testCase, ok := testCasesMap.Get(outerKey); ok { // Get the details of the test case thisTestCaseMap := testCase.(orderedmap.OrderedMap) // Path if val, ok := thisTestCaseMap.Get("path"); ok { thisTestCase.Path = val.(string) } // Method if val, ok := thisTestCaseMap.Get("method"); ok { thisTestCase.Method = val.(string) } // Description if val, ok := thisTestCaseMap.Get("description"); ok { thisTestCase.Description = val.(string) } // Disabled if val, ok := thisTestCaseMap.Get("disabled"); ok { thisTestCase.Disabled = val.(bool) } // Body if val, ok := thisTestCaseMap.Get("body"); ok { // Turn the original string into json bytes, err := json.Marshal(val) if err != nil { error := fmt.Sprintf("Error mnarshalling request body for test case : '%s'\n", err) panic(error) } // Now that we have json, convert it to an interface that we can play with var jsonData interface{} err = json.Unmarshal(bytes, &jsonData) if err != nil { error := fmt.Sprintf("Error processing request body for test case : '%s'\n", err) panic(error) } // Search for any keys in the body that we need to replace with a dynamic random value results := findRandom(nil, jsonData) // for debugging //resultJSON, _ := json.MarshalIndent(results, "", " ") //fmt.Println(string(resultJSON)) // store the results back into the body newBytes, err := json.Marshal(results) if err != nil { error := fmt.Sprintf("Error turning body interface back into json for test case : '%s'\n", err) panic(error) } thisTestCase.Body = newBytes //fmt.Printf("Body marshalled:\n%v\n", string(newBytes)) } // Header if val, ok := thisTestCaseMap.Get("header"); ok { // Create a normal string map for all the body key-value pairs thisTestCase.Header = OrderedToStringMap(val.(orderedmap.OrderedMap)) } // Expect - this is more tricky since it is yet another json fragment if val, ok := thisTestCaseMap.Get("expect"); ok { expectOptions := new(ExpectOptions) // expect can have header and body definitions expectMap := val.(orderedmap.OrderedMap) // Handle the possible checks for a header if expectVal, ok2 := expectMap.Get("header"); ok2 { ReadHeaderTestCases(expectVal.(orderedmap.OrderedMap), &expectOptions.Header) } // Handle the possible checks for a body if expectVal, ok2 := expectMap.Get("body"); ok2 { ReadBodyTestCases(expectVal.(orderedmap.OrderedMap), &expectOptions.Body) } thisTestCase.Expect = *expectOptions } } testDefinitions.TestCases = append(testDefinitions.TestCases, *thisTestCase) } } // Use this function to find and generate any random values we want to use during testing func findRandom(key interface{}, data interface{}) interface{} { switch v := data.(type) { case map[string]interface{}: if randomValue, ok := v["random"]; ok { if randomObj, ok := randomValue.(map[string]interface{}); ok { if isString, isStringOk := randomObj["isString"].(bool); isStringOk && isString { if length, lengthOk := randomObj["length"].(float64); lengthOk { newString := generateRandomString(int(length)) fmt.Printf("Generated random string '%s' to insert into JSON body for key '%s'\n", newString, key.(string)) return newString } } else if isInt, isIntOk := randomObj["isInt"].(bool); isIntOk && isInt { newInt := rand.Int() fmt.Printf("Generated random number '%d' to insert into JSON body\n", newInt) return newInt } else if isMac, isMacOk := randomObj["isMac"].(bool); isMacOk && isMac { newMac := generateRandomMAC() fmt.Printf("Generated random mac address '%s' to insert into JSON body for key '%s'\n", newMac, key.(string)) return newMac } else if isIP, isIpOk := randomObj["isIP"].(bool); isIpOk && isIP { if network, networkOk := randomObj["network"].(string); networkOk { newIp, err := generateRandomIP(network) if err != nil { fmt.Printf("Unable to generate random IP address: '%s'\n", err) return "" } else { fmt.Printf("Generated random IP '%s' address in network '%s' to insert into JSON body for key '%s'\n", newIp, network, key.(string)) return newIp } } } } } for key, value := range v { v[key] = findRandom(key, value) } case []interface{}: for i, value := range v { v[i] = findRandom(i, value) } } return data } func generateRandomString(length int) string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" result := make([]byte, length) for i := range result { result[i] = charset[rand.Intn(len(charset))] } return string(result) } func generateRandomMAC() string { mac := make([]byte, 6) for i := range mac { mac[i] = byte(rand.Intn(256)) } // Set the locally administered and unicast bits mac[0] = (mac[0] | 2) & 0xfe return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) } // generateRandomIP generates a random IP address within the given subnet in CIDR notation. func generateRandomIP(cidr string) (string, error) { ip, ipnet, err := net.ParseCIDR(cidr) if err != nil { return "", fmt.Errorf("invalid CIDR: %w", err) } // Convert IP to uint32 ip4 := ip.To4() if ip4 == nil { return "", fmt.Errorf("not an IPv4 subnet") } ipInt := binary.BigEndian.Uint32(ip4) mask := binary.BigEndian.Uint32(ipnet.Mask) // Calculate network and broadcast addresses network := ipInt & mask broadcast := network | ^mask if broadcast-network <= 1 { return "", fmt.Errorf("subnet too small to allocate address") } // Generate a random IP between network+1 and broadcast-1 (excluding network and broadcast) randomIP := network + uint32(rand.Intn(int(broadcast-network-1))) + 1 // Convert back to net.IP ipBytes := make([]byte, 4) binary.BigEndian.PutUint32(ipBytes, randomIP) return net.IP(ipBytes).String(), nil } func ReadHeaderTestCases(input orderedmap.OrderedMap, result *HeaderTests) { result.Contains = make(map[string]string) result.Equals = make(map[string]string) // Contains check if val, ok := input.Get("contains"); ok { containsMap := val.(orderedmap.OrderedMap) result.Contains = OrderedToStringMap(containsMap) } // Equals check if val, ok := input.Get("equals"); ok { equalsMap := val.(orderedmap.OrderedMap) result.Equals = OrderedToStringMap(equalsMap) } } func ReadBodyTestCases(input orderedmap.OrderedMap, result *BodyTests) { result.Contains = make(map[string]string) result.Equals = make(map[string]string) // TODO : Use tags in struct rather than hard coding all the different check types // using https://stackoverflow.com/a/23840419 as an idea // Contains check if val, ok := input.Get("contains"); ok { // Confirm this is an ordered map if _, ok := val.(orderedmap.OrderedMap); !ok { error := fmt.Sprintf("Unexpected json definition for Body Contains check. Type is '%v' but expected Object.\n", reflect.TypeOf(val)) panic(error) } containsMap := val.(orderedmap.OrderedMap) result.Contains = OrderedToStringMap(containsMap) } // Equals check if val, ok := input.Get("equals"); ok { equalsMap := val.(orderedmap.OrderedMap) result.Equals = OrderedToStringMap(equalsMap) } // Has Keys check if val, ok := input.Get("hasKeys"); ok { // This is an array not a map hasKeysArray := val.([]interface{}) result.HasKeys = OrderedToStringSlice(hasKeysArray) } // TODO : remaining tests }