pkg/network/check.go
package network
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
"time"
"github.com/zephinzer/dev/internal/constants"
"github.com/zephinzer/dev/pkg/utils"
)
// Check represents a network check
type Check struct {
Method string `json:"method" yaml:"method,omitempty"`
URL string `json:"url" yaml:"url"`
// StatusCode should contain the expected http status code, if not defined,
// status codes starting with 1xx, 2xx, and 3xx will be considered successful,
// and 4xx, and 5xx codes will be considered failures
StatusCode int `json:"statusCode" yaml:"statusCode,omitempty"`
// Headers contains headers key-value pairs that should be present in the
// HTTP headers of the response
Headers map[string]string `json:"headers" yaml:"headers,omitempty"`
// ResponseBody is a regex-supported match with the response body
ResponseBody string `json:"responseBody" yaml:"responseBody,omitempty"`
observed *http.Response
}
// GetObserved returns the response object from the check; if no check has
// been performed yet, the http.Response will be of zero value with an error
func (c *Check) GetObserved() (http.Response, error) {
if c.observed == nil {
return http.Response{}, fmt.Errorf("network check to '%s' has not been run yet", c.URL)
}
return *c.observed, nil
}
// Run executes the target network check and places the response in the
// .observed property (used by .Verify to check if the check succeeded); Run
// will return an error on any network issues that result in the .observed
// property not being populated
func (c *Check) Run() error {
var err error
client := http.Client{
Timeout: time.Second * 5,
}
method := c.Method
if len(method) == 0 {
method = constants.DefaultNetworkCheckMethod
}
request, err := http.NewRequest(method, c.URL, nil)
if err != nil {
return err
}
c.observed, err = client.Do(request)
if err != nil {
return err
}
return nil
}
// Verify compares the expected values and the observed outcome, returning
// an error if the verification failed.
func (c Check) Verify() error {
defer func() {
if c.observed != nil {
if c.observed.Body != nil {
c.observed.Body.Close()
}
}
}()
switch true {
case c.observed == nil:
return fmt.Errorf("Run() has not be executed")
case c.StatusCode == *new(int) && c.observed.StatusCode > 399:
return fmt.Errorf("observed status code, %v, was a non-success response (if this is expected, set StatusCode to this value)", c.observed.StatusCode)
case c.StatusCode != *new(int) && c.StatusCode != c.observed.StatusCode:
return fmt.Errorf("expected status code %v but response had status code %v", c.StatusCode, c.observed.StatusCode)
case c.observed.Body != nil && !c.bodiesMatch():
observedBody, _ := ioutil.ReadAll(c.observed.Body)
return fmt.Errorf("expected body response '%s' to match '%s' but it did not", c.ResponseBody, string(observedBody))
case utils.GetNKeyValuePairsStringMap(c.Headers) > 0 && !c.headersMatch():
expectedHeaders, _ := json.Marshal(c.Headers)
responseHeaders := map[string]string{}
for key, value := range c.observed.Header {
responseHeaders[key] = strings.Join(value, ",")
}
observedHeaders, _ := json.Marshal(responseHeaders)
return fmt.Errorf("expected headers '%s' but got '%s'", string(expectedHeaders), string(observedHeaders))
}
return nil
}
// bodiesMatch is a utilities function to check if the expected response body
// matches the observed response body
func (c Check) bodiesMatch() bool {
body, _ := ioutil.ReadAll(c.observed.Body)
regex := regexp.MustCompile(c.ResponseBody)
return regex.Match(body)
}
// headersMatch is a utilities function to check if the expected headers
// match the observed headers
func (c Check) headersMatch() bool {
for key, value := range c.Headers {
if c.observed.Header.Get(key) != value {
return false
}
}
return true
}