From 2afdacaa78f6f38776b29cffc40257429a4e00bd Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Thu, 11 Jan 2024 16:14:00 +1100 Subject: [PATCH] implement feature to generate random test data --- .drone.sh | 6 +-- group-test.json | 53 ++++++++++++++++++- load_input.go | 76 ++++++++++++++++++++++---- main.go | 36 +++++++++++++ permissions-test.json | 75 ++++++++++++++++++++++++++ run_tests.go | 2 + safes-test.json | 120 ++++++++++++++++++++++++++++++++++++++++++ structs.go | 5 ++ test-random.json | 67 +++++++++++++++++++++++ tests.json | 9 +++- user-test.json | 66 +++++++++++++++++++++++ 11 files changed, 500 insertions(+), 15 deletions(-) create mode 100644 permissions-test.json create mode 100644 safes-test.json create mode 100644 test-random.json create mode 100644 user-test.json diff --git a/.drone.sh b/.drone.sh index 054d957..924457b 100644 --- a/.drone.sh +++ b/.drone.sh @@ -9,8 +9,8 @@ export CGO_ENABLED=0 commit=$(git rev-parse HEAD) #tag=$(git describe --tags --abbrev=0) buildtime=$(TZ=Australia/Sydney date +%Y-%m-%dT%T%z) -#git_version=$(git describe --tags --always --long --dirty) -package_name=api-tester +git_version=$(git describe --tags --always --long --dirty) +package_name=apitester #echo "Current directory:" #pwd @@ -35,7 +35,7 @@ do exit 1 fi echo "build complete : $output_name" - sha256sum build/$output_name > build/${output_name}_checksum.txt + #sha256sum build/$output_name > build/${output_name}_checksum.txt #ls -lah ./build/ done diff --git a/group-test.json b/group-test.json index e3bad13..7fa593e 100644 --- a/group-test.json +++ b/group-test.json @@ -38,7 +38,58 @@ } } } - } + }, + "test3": { + "path": "/api/admin/group/add", + "disabled": true, + "method": "POST", + "description": "add new local group", + "body": { + "groupName": "test local group", + "ldapGroup": false + }, + "expect": { + "header": { + "contains": { + "http_status": "200" + } + } + } + }, + "test4": { + "path": "/api/admin/groups", + "method": "GET", + "description": "get group listing again", + "expect": { + "header": { + "contains": { + "http_status": "200" + } + }, + "body": { + "hasKeys": ["groupName"], + "contains": { + "message": "success" + } + } + } + }, + "test5": { + "path": "/api/admin/group/delete", + "method": "POST", + "description": "remove new local group", + "body": { + "groupName": "test local group", + "ldapGroup": false + }, + "expect": { + "header": { + "contains": { + "http_status": "200" + } + } + } + } }, "url": "https://10.63.39.130:8443", "insecure": true, diff --git a/load_input.go b/load_input.go index 2361a09..29db9d5 100644 --- a/load_input.go +++ b/load_input.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "math/rand" "reflect" "github.com/iancoleman/orderedmap" @@ -145,20 +146,36 @@ func ReadInput(data []byte, testDefinitions *TestDefinitions) { } // Body if val, ok := thisTestCaseMap.Get("body"); ok { - // Create a normal string map for all the body key-value pairs - - // TODO : Support nested json instead of single level flat request - - //thisTestCase.Body = OrderedToStringMap(val.(orderedmap.OrderedMap)) - + // 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) - } else { - thisTestCase.Body = bytes - //fmt.Printf("Body marshalled:\n%v\n", string(bytes)) } + + // 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 { @@ -190,6 +207,47 @@ func ReadInput(data []byte, testDefinitions *TestDefinitions) { } } +// 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 + } + } + } + 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 ReadHeaderTestCases(input orderedmap.OrderedMap, result *HeaderTests) { result.Contains = make(map[string]string) result.Equals = make(map[string]string) diff --git a/main.go b/main.go index e269184..bebe620 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,9 @@ import ( "flag" "fmt" "os" + "reflect" "strconv" + "strings" "github.com/iancoleman/orderedmap" ) @@ -12,6 +14,36 @@ import ( var testDefinitions *TestDefinitions var captureValues *CaptureValues +func PrintStructContents(s interface{}, indentLevel int) string { + var result strings.Builder + + val := reflect.ValueOf(s) + + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + indent := strings.Repeat("\t", indentLevel) + result.WriteString(fmt.Sprintf("%s%s: ", indent, fieldType.Name)) + + switch field.Kind() { + case reflect.Struct: + result.WriteString("\n") + result.WriteString(PrintStructContents(field.Interface(), indentLevel+1)) + default: + result.WriteString(fmt.Sprintf("%v\n", field.Interface())) + } + } + + return result.String() +} + func main() { var s []byte var err error @@ -43,6 +75,8 @@ func main() { // Process the input json ReadInput(s, testDefinitions) + //fmt.Println(PrintStructContents(testDefinitions.TestCases[1], 0)) + // Perform specified test cases for i := range testDefinitions.TestCases { @@ -52,6 +86,7 @@ func main() { } RunTest(&testDefinitions.TestCases[i]) + fmt.Printf("\nRunning checks on output\n") success, err := CheckResults(&testDefinitions.TestCases[i]) if err != nil { @@ -71,6 +106,7 @@ func main() { // For debugging, just dump the output //fmt.Printf("%+v\n", testDefinitions) + //fmt.Println(PrintStructContents(testDefinitions, 0)) } // fileExists returns true if the specified file exists and is not a directory diff --git a/permissions-test.json b/permissions-test.json new file mode 100644 index 0000000..da27621 --- /dev/null +++ b/permissions-test.json @@ -0,0 +1,75 @@ +{ + "name": "CBS permissions testing", + "testCases": { + "test1": { + "path": "/api/login", + "method": "POST", + "description": "Perform login", + "body": { + "username": "Administrator", + "password": "Password123" + }, + "expect": { + "header": { + "contains": { + "http_status": "200", + "Content-Type": "application/json" + } + }, + "body": { + "hasKeys": ["access_token"] + } + } + }, + "test2": { + "path": "/api/admin/permissions", + "method": "GET", + "description": "get list of permissions", + "expect": { + "header": { + "contains": { + "http_status": "200" + } + }, + "body": { + "hasKeys": ["permissionId"], + "contains": { + "message": "success" + } + } + } + }, + "test3": { + "path": "/api/admin/permission/add", + "method": "POST", + "description": "create new permission", + "body": { + "Description": "Readonly access to default safe", + "safeId": 1, + "userId": 2, + "readOnly": true + }, + "expect": { + "header": { + "contains": { + "http_status": "200" + } + } + } + } + }, + "url": "https://10.63.39.130:8443", + "insecure": true, + "header": { + "Content-Type": "application/json", + "Authorization": "Bearer %ACCESS_TOKEN%" + }, + "capture": { + "test1": { + "body": { + "access_token": "%ACCESS_TOKEN%" + } + } + } +} + \ No newline at end of file diff --git a/run_tests.go b/run_tests.go index e011a88..b40a238 100644 --- a/run_tests.go +++ b/run_tests.go @@ -128,12 +128,14 @@ func RunTest(testCase *TestCase) error { // 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("Body capture %s = %s\n", k, v) if val, ok := testCase.ResultBodyMap[k]; ok { + fmt.Printf("Found capture %s\n", k) // TODO handle values other than string using a switch //fmt.Printf("Found matching capture in body with value : '%v', storing replacement with key '%s'\n", val, v) captureValues.Data[v] = val.(string) diff --git a/safes-test.json b/safes-test.json new file mode 100644 index 0000000..6501740 --- /dev/null +++ b/safes-test.json @@ -0,0 +1,120 @@ +{ + "name": "CBS safes testing", + "testCases": { + "test1": { + "path": "/api/login", + "method": "POST", + "description": "Perform login", + "body": { + "username": "Administrator", + "password": "Password123" + }, + "expect": { + "header": { + "contains": { + "http_status": "200", + "Content-Type": "application/json" + } + }, + "body": { + "hasKeys": ["access_token"] + } + } + }, + "test2": { + "path": "/api/safe/list", + "method": "GET", + "description": "get user allowed safe listing", + "expect": { + "header": { + "contains": { + "http_status": "200" + } + }, + "body": { + "hasKeys": ["safeName"], + "contains": { + "message": "success" + } + } + } + }, + "test3": { + "path": "/api/admin/safe/add", + "method": "POST", + "description": "create new safe", + "body": { + "safeName": "API test created safe" + }, + "expect": { + "header": { + "contains": { + "http_status": "200" + } + } + } + }, + "test4": { + "path": "/api/admin/safe/add", + "method": "POST", + "description": "create new safe", + "body": { + "safeName": "API test created second safe" + }, + "expect": { + "header": { + "contains": { + "http_status": "200" + } + } + } + }, + "test5": { + "path": "/api/admin/safe/listall", + "method": "GET", + "description": "get all defined safe listing", + "expect": { + "header": { + "contains": { + "http_status": "200" + } + }, + "body": { + "hasKeys": ["safeName"], + "contains": { + "message": "success" + } + } + } + }, + "test6": { + "path": "/api/admin/safe/delete", + "method": "POST", + "description": "get all defined safe listing", + "body": { + "safeName": "API test created safe" + }, + "expect": { + "header": { + "contains": { + "http_status": "200" + } + } + } + } + }, + "url": "https://10.63.39.130:8443", + "insecure": true, + "header": { + "Content-Type": "application/json", + "Authorization": "Bearer %ACCESS_TOKEN%" + }, + "capture": { + "test1": { + "body": { + "access_token": "%ACCESS_TOKEN%" + } + } + } +} + \ No newline at end of file diff --git a/structs.go b/structs.go index 93b92ca..7588725 100644 --- a/structs.go +++ b/structs.go @@ -28,6 +28,11 @@ type TestCase struct { ResultBodyMap map[string]interface{} } +type RandomBody struct { + IsString bool `json:"isString"` + Length int `json:"length"` +} + type CaptureCase struct { TestCaseName string CaptureData CaptureCaseData diff --git a/test-random.json b/test-random.json new file mode 100644 index 0000000..d43bed3 --- /dev/null +++ b/test-random.json @@ -0,0 +1,67 @@ +{ + "name": "CBS test cases", + "testCases": { + "test1": { + "path": "/api/login", + "method": "POST", + "description": "Perform login", + "body": { + "username": "Administrator", + "password": "Password123" + }, + "expect": { + "header": { + "contains": { + "http_status": "200", + "Content-Type": "application/json" + } + }, + "body": { + "hasKeys": ["access_token"] + } + } + }, + "test12": { + "path": "/api/secret/store", + "method": "POST", + "description": "store first new secret", + "body": { + "safeId": 1, + "deviceName": { + "random": { + "isString": true, + "length": 12 + } + }, + "deviceCategory": "appliance", + "userName": "service@cdc.home", + "secretValue": { + "random": { + "isString": true, + "length": 20 + } + } + }, + "expect": { + "header": { + "contains": { + "http_status": "200" + } + } + } + } + }, + "url": "https://10.63.39.130:8443", + "insecure": true, + "header": { + "Content-Type": "application/json", + "Authorization": "Bearer %ACCESS_TOKEN%" + }, + "capture": { + "test1": { + "body": { + "access_token": "%ACCESS_TOKEN%" + } + } + } +} \ No newline at end of file diff --git a/tests.json b/tests.json index 0def9a2..05482e9 100644 --- a/tests.json +++ b/tests.json @@ -22,7 +22,7 @@ } }, "test2": { - "path": "/api/admin/user/register", + "path": "/api/admin/user/add", "method": "POST", "description": "register user account", "header": { @@ -136,7 +136,12 @@ "description": "store first new secret", "body": { "safeId": 1, - "deviceName": "avcp01.cdc.home", + "deviceName": { + "random": { + "isString": true, + "length": 12 + } + }, "deviceCategory": "appliance", "userName": "service@cdc.home", "secretValue": "TheWiggles" diff --git a/user-test.json b/user-test.json new file mode 100644 index 0000000..f58e8a4 --- /dev/null +++ b/user-test.json @@ -0,0 +1,66 @@ +{ + "name": "CBS normal user testing", + "testCases": { + "test1": { + "path": "/api/login", + "method": "POST", + "description": "Perform login", + "body": { + "username": "nathtest", + "password": "VMware1!" + }, + "expect": { + "header": { + "contains": { + "http_status": "200", + "Content-Type": "application/json" + } + }, + "body": { + "hasKeys": ["access_token"] + } + } + }, + "test20": { + "path": "/api/secret/list", + "disabled": false, + "method": "GET", + "description": "List secrets", + "header": { + "Authorization": "Bearer %ACCESS_TOKEN%" + }, + "expect": { + "header": { + "contains": { + "http_status": "200" + } + } + } + }, + "test22": { + "path": "/api/secret/update", + "method": "POST", + "description": "try update existing secret", + "body": { + "deviceName": "adcp01.cdc.home", + "deviceCategory": "windows-server", + "userName": "dummy@cdc.home", + "secretValue": "TheWiggles" + } + } + }, + "url": "https://10.63.39.130:8443", + "insecure": true, + "header": { + "Content-Type": "application/json", + "Authorization": "Bearer %ACCESS_TOKEN%" + }, + "capture": { + "test1": { + "body": { + "access_token": "%ACCESS_TOKEN%" + } + } + } +} + \ No newline at end of file