package main import ( "bytes" "crypto/tls" "encoding/json" "errors" "flag" "fmt" "io" "net/http" "net/url" "os" "strconv" "github.com/iancoleman/orderedmap" ) type TestDefinitions struct { 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 { Name string Path string `json:"path"` Method string `json:"method"` Description string `json:"description"` Expect ExpectOptions `json:"expect"` Header map[string]string //Body map[string]string Body []byte // 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 HeaderTests `json:"header"` Body BodyTests `json:"body"` } // Test types type HeaderTests struct { Contains map[string]string Equals map[string]string } type BodyTests struct { Contains map[string]string Equals map[string]string HasKeys map[string]string PathEquals string PathContains map[string]string } var testDefinitions *TestDefinitions // fileExists returns true if the specified file exists and is not a directory func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return !info.IsDir() } func PerformGet() { // Sample code from https://golangnote.com/request/sending-post-request-in-golang-with-header url := "https://www.example.com/api/v1/create" contentType := "application/json" data := []byte(`{"name": "Test User", "email": "test@example.com"}`) client := &http.Client{} req, err := http.NewRequest("GET", url, bytes.NewBuffer(data)) if err != nil { fmt.Println(err) return } req.Header.Add("Content-Type", contentType) req.Header.Add("Authorization", "Bearer YOUR_ACCESS_TOKEN") resp, err := client.Do(req) if err != nil { fmt.Println(err) return } 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) return } fmt.Println(string(body)) } func PerformPost() { } func TestOutput() { } // 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 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 // 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 } // Create request if len(testCase.Body) > 0 { req, err = http.NewRequest(requestType, requestUrl, bytes.NewBuffer(testCase.Body)) } 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 { fmt.Printf("Add global header %s = %s\n", k, v) req.Header.Add(k, v) } } // Then any test case specific headers for k, v := range testCase.Header { fmt.Printf("Add Header %s = %s\n", k, v) req.Header.Add(k, v) } // Perform request resp, err := client.Do(req) if err != nil { fmt.Println(err) return err } defer resp.Body.Close() // Store response fmt.Printf("Header response:\n%+v\n", resp.Header) fmt.Printf("http_status:\n'%+v'\n", resp.StatusCode) body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println(err) return err } fmt.Printf("Body response:\n%s\n", string(body)) // No errors if we reached this point return nil } func main() { var s []byte var err error //var ok bool // Prepare struct to store settings common to all tests testDefinitions = new(TestDefinitions) //testCaseDefinition := new(InputData) // Command line arguments var inputJson string // Process command line arguments flag.StringVar(&inputJson, "input", "./tests.json", "Full path to input json test definition file") // Read the json input file if fileExists(inputJson) { s, err = os.ReadFile(inputJson) if err != nil { panic(err) } } else { fmt.Printf("Input JSON file '%s' does not exist.\n", inputJson) 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(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 { fmt.Printf("No key defining test cases found\n") } /* // Test Cases is an array, need to go one level down before we start looking at key/values vs := testsInterface.([]interface{}) fmt.Printf("Listing %d test definitions\n", len(vs)) for i, vInterface := range vs { v := vInterface.(orderedmap.OrderedMap) keys := v.Keys() fmt.Printf("Test %d\n", i) for j := range keys { fmt.Printf("[%d] : %s\n", j, keys[j]) } } */ /* vslice := testsInterface.([]interface{}) vmap := vslice[2].(orderedmap.OrderedMap) k := vmap.Keys() for i := range k { fmt.Printf("[%d] : %s\n", i, k[i]) } 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() // Parse json into our testCaseDefinition // Parse each key into our config struct fmt.Printf("Listing %d test cases\n", len(testCasesKeys)) for i, 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) } // 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)) bytes, err := json.Marshal(val) if err != nil { fmt.Printf("Error processing request body for test case : '%s'\n", err) } else { thisTestCase.Body = bytes //fmt.Printf("Body marshalled:\n%v\n", string(bytes)) } } // 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() { val, _ := thisTestCaseMap.Get(key) fmt.Printf("[%d] %s : %s\n", j, key, val) } */ } testDefinitions.TestCases = append(testDefinitions.TestCases, *thisTestCase) } } func OrderedToStringMap(input orderedmap.OrderedMap) map[string]string { result := make(map[string]string) mapKeys := input.Keys() for _, bodyKey := range mapKeys { if bodyVal, ok := input.Get(bodyKey); ok { 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 }