Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instead of using http.CanonicalHeaderKey to check if a header exists,, you can directly use the response.Header.Get(header) method. #62

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 75 additions & 66 deletions internal/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,34 @@ import (
"go.uber.org/zap"
)

// TestResult stores results of a single test
const (
schemeHTTP = "http"
schemeHTTPS = "https"

methodGET = "GET"
methodPOST = "POST"
methodPUT = "PUT"
methodPATCH = "PATCH"
methodDELETE = "DELETE"
methodHEAD = "HEAD"
methodOPTIONS = "OPTIONS"
methodPURGE = "PURGE"
methodPROPFIND = "PROPFIND"
)

type TestResult struct {
Skipped bool
Errors []error
}

// RunTest runs a single test
func RunTest(test *Test, defaultHost string, maxRetries int) *TestResult {
result := &TestResult{}

// Validate test and assign default values
if err := preProcessTest(test, defaultHost); err != nil {
result.Errors = append(result.Errors, err)
return result
}

// Check test conditions and skip if not met
conditionsMet, err := validateConditions(test)
if err != nil {
result.Errors = append(result.Errors, err)
Expand All @@ -63,7 +74,7 @@ func RunTest(test *Test, defaultHost string, maxRetries int) *TestResult {

retryCallback := func(ctx context.Context, resp *http.Response, inErr error) (bool, error) {
if inErr != nil {
// retry is there is an error with the request
// retry if there is an error with the request
return true, nil
}

Expand Down Expand Up @@ -110,30 +121,30 @@ func RunTest(test *Test, defaultHost string, maxRetries int) *TestResult {
return result
}

// preProcessTest validates test and assigns default values
func preProcessTest(test *Test, defaultHost string) error {
// Scheme
scheme := stringValue(test.Request.Scheme, "https")
if scheme != "http" && scheme != "https" {
scheme := stringValue(test.Request.Scheme, schemeHTTPS)
switch scheme {
case schemeHTTP, schemeHTTPS:
test.Request.Scheme = scheme
default:
return fmt.Errorf("invalid scheme %s. only http and https are supported", scheme)
}
test.Request.Scheme = scheme

// Host
host := stringValue(test.Request.Host, defaultHost)
if len(host) == 0 {
return fmt.Errorf("no host specified for this test and no default host set")
}
test.Request.Host = host

// Method
method := stringValue(test.Request.Method, "GET")
if method != "GET" && method != "POST" && method != "PUT" && method != "PATCH" && method != "DELETE" && method != "HEAD" && method != "OPTIONS" && method != "PURGE" && method != "PROPFIND" {
method := stringValue(test.Request.Method, methodGET)
switch method {
case methodGET, methodPOST, methodPUT, methodPATCH, methodDELETE, methodHEAD, methodOPTIONS, methodPURGE, methodPROPFIND:
test.Request.Method = method
default:
return fmt.Errorf("invalid method %s. only GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, PURGE, PROPFIND are supported", method)
}
test.Request.Method = method

// Path
if len(test.Request.Path) < 1 {
return fmt.Errorf("request path is required")
}
Expand All @@ -142,14 +153,13 @@ func preProcessTest(test *Test, defaultHost string) error {
return fmt.Errorf("request.path must start with /")
}

// Process the dynamic headers
if err := ProcessDynamicHeaders(test.Request.DynamicHeaders, test.Request.Headers); err != nil {
return err
}

// Convert header fields to lowercase
// https://tools.ietf.org/html/rfc7540#section-8.1.2
headers := map[string]string{}
headers := make(map[string]string, len(test.Request.Headers))
for k, v := range test.Request.Headers {
headers[strings.ToLower(k)] = v
}
Expand All @@ -168,9 +178,9 @@ func stringValue(val, defaultVal string) string {
func validateConditions(test *Test) (bool, error) {
// Environment variable
for key, pattern := range test.Conditions.Env {
re, err := regexp.Compile("(?i)" + pattern)
re, err := compilePattern(pattern)
if err != nil {
return false, fmt.Errorf("%s", err.Error())
return false, err
}

if !re.MatchString(os.Getenv(key)) {
Expand All @@ -181,14 +191,11 @@ func validateConditions(test *Test) (bool, error) {
return true, nil
}

func validateResponse(test *Test, response *http.Response, body []byte) []error {
errors := []error{}

errors = append(errors, validateResponseStatus(test, response)...)
errors = append(errors, validateResponseHeaders(test, response)...)
errors = append(errors, validateResponseBody(test, response, body)...)

return errors
func validateResponse(test *Test, response *http.Response, body []byte) (errs []error) {
errs = append(errs, validateResponseStatus(test, response)...)
errs = append(errs, validateResponseHeaders(test, response)...)
errs = append(errs, validateResponseBody(test, response, body)...)
return errs
}

func validateResponseStatus(test *Test, response *http.Response) []error {
Expand All @@ -199,6 +206,7 @@ func validateResponseStatus(test *Test, response *http.Response) []error {
for _, code := range expected.StatusCodes {
if code == response.StatusCode {
matched = true
break
}
}

Expand All @@ -213,48 +221,30 @@ func validateResponseHeaders(test *Test, response *http.Response) []error {
errors := []error{}
expectedResponse := test.Response

// Patterns (matching assertions)
errors = append(errors, validateResponseHeaderPatterns(response, expectedResponse.Headers.Patterns, true)...)
errors = append(errors, validateHeaderPatterns(response, expectedResponse.Headers.Patterns, true)...)

// NotMatching assertions
errors = append(errors, validateResponseHeaderPatterns(response, expectedResponse.Headers.NotMatching, false)...)
errors = append(errors, validateHeaderPatterns(response, expectedResponse.Headers.NotMatching, false)...)

// NotPresent assertions
npAssertions := expectedResponse.Headers.NotPresent
for _, header := range npAssertions {
if len(response.Header.Get(header)) > 0 {
errors = append(errors, fmt.Errorf("found unexpected response header \"%s\"", header))
}
}
errors = append(errors, validateHeadersNotPresent(response, expectedResponse.Headers.NotPresent)...)

// IfPresentNotMatching assertions come in the form of key/value pairs where the key is the header name and the value is
// the pattern that we want to confirm does not exist within that header. Here we test that in two steps:
// 1. If the header doesn't exists, the test automatically passes.
// 2. If the header does exist, validate against the not matching assertions.
ipnmHeaders := expectedResponse.Headers.IfPresentNotMatching
for header := range ipnmHeaders {
if len(response.Header.Get(header)) > 0 {
errors = append(errors, validateResponseHeaderPatterns(response, ipnmHeaders, false)...)
}
}
errors = append(errors, validateHeadersIfPresentNotMatching(response, expectedResponse.Headers.IfPresentNotMatching)...)

return errors
}

func validateResponseHeaderPatterns(response *http.Response, patterns map[string]string, expectedToMatch bool) []error {
func validateHeaderPatterns(response *http.Response, patterns map[string]string, expectedToMatch bool) []error {
errors := []error{}

// Patterns
for header, pattern := range patterns {
re, err := regexp.Compile("(?i)" + pattern)
re, err := compilePattern(pattern)
if err != nil {
errors = append(errors, fmt.Errorf("invalid test pattern `%s`: %s", pattern, err.Error()))
continue
}

// Get all instances of the response header
values, ok := response.Header[http.CanonicalHeaderKey(header)]
if !ok {
value := response.Header.Get(header)
if value == "" {
if expectedToMatch {
errors = append(errors, fmt.Errorf("response header \"%s\" not found, expected to match pattern \"%s\"", header, pattern))
} else {
Expand All @@ -263,21 +253,36 @@ func validateResponseHeaderPatterns(response *http.Response, patterns map[string
continue
}

// Try to match pattern from one of the instances
matched := false
for _, value := range values {
value = strings.ToLower(value)
if re.MatchString(value) {
matched = true
}
if expectedToMatch && !re.MatchString(value) {
errors = append(errors, fmt.Errorf("response header \"%s\" has value \"%s\", which does not match pattern \"%s\"", header, value, pattern))
}

if !expectedToMatch && re.MatchString(value) {
errors = append(errors, fmt.Errorf("response header \"%s\" has value \"%s\", which matches pattern \"%s\"", header, value, pattern))
}
}

return errors
}

if expectedToMatch && !matched {
errors = append(errors, fmt.Errorf("response header \"%s\" has value(s) \"%s\", none of which match pattern \"%s\"", header, strings.Join(values[:], "\", \""), pattern))
func validateHeadersNotPresent(response *http.Response, notPresentAssertions []string) []error {
errors := []error{}

for _, header := range notPresentAssertions {
if len(response.Header.Get(header)) > 0 {
errors = append(errors, fmt.Errorf("found unexpected response header \"%s\"", header))
}
}

return errors
}

if !expectedToMatch && matched {
errors = append(errors, fmt.Errorf("response header \"%s\" has value(s) \"%s\", at least one of which matches pattern \"%s\"", header, strings.Join(values[:], "\", \""), pattern))
func validateHeadersIfPresentNotMatching(response *http.Response, ifPresentNotMatchingAssertions map[string]string) []error {
errors := []error{}

for header := range ifPresentNotMatchingAssertions {
if response.Header.Get(header) != "" {
errors = append(errors, validateHeaderPatterns(response, ifPresentNotMatchingAssertions, false)...)
}
}

Expand All @@ -289,9 +294,9 @@ func validateResponseBody(test *Test, response *http.Response, body []byte) []er

patterns := test.Response.Body.Patterns
for _, pattern := range patterns {
re, err := regexp.Compile("(?i)" + pattern)
re, err := compilePattern(pattern)
if err != nil {
errors = append(errors, fmt.Errorf("%s", err.Error()))
errors = append(errors, err)
continue
}

Expand All @@ -302,3 +307,7 @@ func validateResponseBody(test *Test, response *http.Response, body []byte) []er

return errors
}

func compilePattern(pattern string) (*regexp.Regexp, error) {
return regexp.Compile("(?i)" + pattern)
}