diff --git a/main.go b/main.go index c1b9657..7c78d16 100644 --- a/main.go +++ b/main.go @@ -2,11 +2,14 @@ package main import ( "bytes" + "crypto/tls" "encoding/json" + "errors" "flag" "fmt" "io" "net/http" + "net/url" "os" "strconv" @@ -14,11 +17,12 @@ import ( ) type TestDefinitions struct { - Name string `json:"name"` - TestCases []TestCase `json:"testCases"` - BaseUrl string `json:"url"` - Headers map[string]string // Need to initialise with make - //Header HeaderOptions `json:"header"` + Name string `json:"name"` + TestCases []TestCase `json:"testCases"` + BaseUrl string `json:"url"` + CaptureCases []CaptureCase `json:"capture"` + Headers map[string]string + Insecure bool `json:"insecure"` } type TestCase struct { @@ -27,48 +31,54 @@ type TestCase struct { Method string `json:"method"` Description string `json:"description"` Expect ExpectOptions `json:"expect"` - Header map[string]string - Body map[string]string + + Header map[string]string + Body map[string]string + + // Something to store results in + ResultStatusCode int + ResultHeaders map[string]string + ResultBody string +} + +type CaptureCase struct { + TestCaseName string + CaptureData CaptureCaseData +} + +type CaptureCaseData struct { + Header CaptureHeader `json:"header"` + Body CaptureBody `json:"body"` +} + +type CaptureHeader struct { + Data map[string]string +} + +type CaptureBody struct { + Data map[string]string } type ExpectOptions struct { - Header HeaderOptions `json:"header"` - Body BodyOptions `json:"body"` -} - -type HeaderOptions struct { - Contains ContainsTest `json:"contains"` - Equals EqualsTest `json:"equals"` -} - -type BodyOptions struct { - Contains ContainsTest `json:"contains"` - Equals EqualsTest `json:"equals"` - HasKeys HasKeysTest `json:"hasKeys"` - PathEq PathEqTest `json:"pathEq"` - PathContains PathContainsTest `json:"pathContains"` + Header HeaderTests `json:"header"` + Body BodyTests `json:"body"` } // Test types -type ContainsTest struct { - // dynamically specified key value pairs +type HeaderTests struct { + Contains map[string]string + Equals map[string]string } -type EqualsTest struct { - // dynamically specified key value pairs +type BodyTests struct { + Contains map[string]string + Equals map[string]string + HasKeys map[string]string + PathEquals string + PathContains map[string]string } -type HasKeysTest struct { - // dynamically specified key value pairs -} - -type PathEqTest struct { - // dynamically specified key value pairs -} - -type PathContainsTest struct { - // dynamically specified key value pairs -} +var testDefinitions *TestDefinitions // fileExists returns true if the specified file exists and is not a directory func fileExists(filename string) bool { @@ -86,7 +96,7 @@ func PerformGet() { data := []byte(`{"name": "Test User", "email": "test@example.com"}`) client := &http.Client{} - req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + req, err := http.NewRequest("GET", url, bytes.NewBuffer(data)) if err != nil { fmt.Println(err) return @@ -101,6 +111,9 @@ func PerformGet() { } defer resp.Body.Close() + fmt.Printf("%+v\n", resp.Header) + fmt.Printf("%+v\n", resp.StatusCode) + body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println(err) @@ -118,14 +131,86 @@ func TestOutput() { } +// RunTest Executes each individual test case +func RunTest(testCase *TestCase) error { + var err error + var requestUrl string + var requestType string + //var requestHeaders map[string]string + //var requestBody map[string]string + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: testDefinitions.Insecure}, + } + client := &http.Client{Transport: tr} + + fmt.Printf("Running test '%s'\n", testCase.Name) + + // Determine URL + if len(testDefinitions.BaseUrl) > 0 { + 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) + } + } else { + requestUrl = testCase.Path + } + + // Populate request body + + 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 + } + + // Create request + 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 { + fmt.Printf("Add Header %s = %s\n", k, v) + req.Header.Add(k, v) + } + } + + // Then any test case specific headers + for i, key := range testCase.Header { + req.Header.Add(key, testDefinitions.Headers[i]) + } + + // Perform request + resp, err := client.Do(req) + if err != nil { + fmt.Println(err) + return err + } + defer resp.Body.Close() + + // Store response + + // No errors if we reached this point + return nil +} + func main() { var s []byte var err error - var ok bool + //var ok bool // Prepare struct to store settings common to all tests - testDefinitions := new(TestDefinitions) - testDefinitions.Headers = make(map[string]string) + testDefinitions = new(TestDefinitions) //testCaseDefinition := new(InputData) @@ -146,9 +231,22 @@ func main() { os.Exit(1) } + ReadInput(s, testDefinitions) + + // For debugging, just dump the output + fmt.Printf("%+v\n", testDefinitions) + + for i := range testDefinitions.TestCases { + RunTest(&testDefinitions.TestCases[i]) + } +} + +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(s), &o) + err = json.Unmarshal([]byte(data), &o) if err != nil { error := fmt.Sprintf("JSON Unmarshal error %s\n", err) panic(error) @@ -156,11 +254,6 @@ func main() { topLevel := o.Keys() fmt.Printf("Found %d top-level keys in json data\n", len(topLevel)) - /* - for i, key := range topLevel { - fmt.Printf("[%d] : %s\n", i, key) - } - */ // TODO : Check required top level keys are present if len(topLevel) <= 1 { @@ -174,6 +267,22 @@ func main() { 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 { @@ -204,6 +313,21 @@ func main() { return */ + // TODO : capture + // each capture node has a name that links it to the test case that captures that data + if capturecase, ok := o.Get("capture"); ok { + captureCasesMap := capturecase.(orderedmap.OrderedMap) + captureCasesKeys := captureCasesMap.Keys() + for i, outerKey := range captureCasesKeys { + fmt.Printf("[%d] : Capture from %s\n", i, outerKey) + + thisCaptureCase := new(CaptureCase) + thisCaptureCase.TestCaseName = outerKey + + 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() @@ -239,26 +363,36 @@ func main() { // 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)) - - /* - thisTestCase.Body = make(map[string]string) - bodyMap := val.(orderedmap.OrderedMap) - bodyMapKeys := bodyMap.Keys() - for _, bodyKey := range bodyMapKeys { - if bodyVal, ok := bodyMap.Get(bodyKey); ok { - switch bodyVal.(type) { - case string: - thisTestCase.Body[bodyKey] = bodyVal.(string) - case bool: - thisTestCase.Body[bodyKey] = strconv.FormatBool(bodyVal.(bool)) - } - - } - } - */ } + // 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 + } /* for j, key := range thisTestCaseMap.Keys() { @@ -268,12 +402,8 @@ func main() { } */ } - testDefinitions.TestCases = append(testDefinitions.TestCases, *thisTestCase) } - - // For debugging, just dump the output - fmt.Printf("%+v\n", testDefinitions) } func OrderedToStringMap(input orderedmap.OrderedMap) map[string]string { @@ -282,15 +412,62 @@ func OrderedToStringMap(input orderedmap.OrderedMap) map[string]string { mapKeys := input.Keys() for _, bodyKey := range mapKeys { if bodyVal, ok := input.Get(bodyKey); ok { - switch bodyVal.(type) { + switch vType := bodyVal.(type) { case string: result[bodyKey] = bodyVal.(string) case bool: result[bodyKey] = strconv.FormatBool(bodyVal.(bool)) + default: + fmt.Printf("OrderedToStringMap received unexpected value type, %T\n", vType) } - } } return result } + +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) + result.HasKeys = 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 { + 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 { + equalsMap := val.(orderedmap.OrderedMap) + result.HasKeys = OrderedToStringMap(equalsMap) + } + + // TODO : remaining tests +} diff --git a/tests.json b/tests.json index 4d95c21..d11c96e 100644 --- a/tests.json +++ b/tests.json @@ -2,43 +2,39 @@ "name": "CBS test cases", "testCases": { "test1": { - "path": "/hosts", - "method": "GET", - "description": "Check database query is working", + "path": "/api/login", + "method": "POST", + "description": "Perform login", + "body": { + "username": "Administrator", + "password": "Password123" + }, "expect": { "header": { - "contains": { + "equals": { "http_status": "200" } }, "body": { "contains": { - "Cluster":"Cluster2" + "access_token":"*" } } } - }, + }, "test2": { - "path": "/protected/hosts?key=e36689911ed0e9cba50c436b66d664cf9ee4f555e182a48021c21d9f7c640868", - "method": "POST", - "description": "Create new host", + "path": "/api/secret/list", + "method": "GET", + "description": "List secrets", + "header": { + "Authorization": "Bearer %access_token%" + }, "body": { "Hostname":"host999.cdc.home", - "IP":"10.63.39.5", - "Subnet":"255.255.255.0", - "Gateway":"10.63.39.1", - "Vcenter":"avcp06.cdc.home", - "Datacenter":"CDC", - "Cluster":"Cluster", - "MAC":"b8:2a:72:cf:84:99", - "PerformReinstall":false, - "BootWWN":"naa.70000970000297600333533030314130", - "Vlan":"1000", - "DNS":"10.63.39.1", - "DNS2":"10.45.39.1", - "NTP1":"10.63.39.1", - "NTP2":"10.45.39.1", - "BootTypeUefi":true + "BootTypeUefi":true, + "parent": { + "childKey": "childValue" + } }, "expect": { "header": { @@ -49,8 +45,16 @@ } } }, - "url": "https://10.63.39.130:443", + "url": "https://10.63.39.130:8443", + "insecure": true, "header": { "Content-Type": "application/json" + }, + "capture": { + "test1": { + "body": { + "access_token": "access_token" + } + } } } \ No newline at end of file