progress to actually making requests

This commit is contained in:
2024-01-02 15:20:50 +11:00
parent ebcdbedacc
commit 035ccf1cbe
2 changed files with 276 additions and 95 deletions

317
main.go
View File

@@ -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
}

View File

@@ -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"
}
}
}
}