All checks were successful
continuous-integration/drone/push Build is passing
364 lines
11 KiB
Go
364 lines
11 KiB
Go
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
|
|
}
|