refactored caddytest helpers (#3285)

* refactored caddytest helpers
* added cookie jar support. Added support for more http verbs
This commit is contained in:
Mark Sargent 2020-04-27 13:23:46 +12:00 committed by GitHub
parent a6761153cb
commit 570d84f7d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 227 additions and 76 deletions

View file

@ -11,6 +11,7 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"net/http/cookiejar"
"os" "os"
"path" "path"
"regexp" "regexp"
@ -46,6 +47,30 @@ var (
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`) matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
) )
// Tester represents an instance of a test client.
type Tester struct {
Client *http.Client
t *testing.T
}
// NewTester will create a new testing client with an attached cookie jar
func NewTester(t *testing.T) *Tester {
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatalf("failed to create cookiejar: %s", err)
}
return &Tester{
Client: &http.Client{
Transport: CreateTestingTransport(),
Jar: jar,
Timeout: 5 * time.Second,
},
t: t,
}
}
type configLoadError struct { type configLoadError struct {
Response string Response string
} }
@ -59,41 +84,42 @@ func timeElapsed(start time.Time, name string) {
// InitServer this will configure the server with a configurion of a specific // InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type. // type. The configType must be either "json" or the adapter type.
func InitServer(t *testing.T, rawConfig string, configType string) { func (tc *Tester) InitServer(rawConfig string, configType string) {
if err := initServer(t, rawConfig, configType); err != nil { if err := tc.initServer(rawConfig, configType); err != nil {
t.Logf("failed to load config: %s", err) tc.t.Logf("failed to load config: %s", err)
t.Fail() tc.t.Fail()
} }
} }
// InitServer this will configure the server with a configurion of a specific // InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type. // type. The configType must be either "json" or the adapter type.
func initServer(t *testing.T, rawConfig string, configType string) error { func (tc *Tester) initServer(rawConfig string, configType string) error {
if testing.Short() { if testing.Short() {
t.SkipNow() tc.t.SkipNow()
return nil return nil
} }
err := validateTestPrerequisites() err := validateTestPrerequisites()
if err != nil { if err != nil {
t.Skipf("skipping tests as failed integration prerequisites. %s", err) tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil return nil
} }
t.Cleanup(func() { tc.t.Cleanup(func() {
if t.Failed() { if tc.t.Failed() {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil { if err != nil {
t.Log("unable to read the current config") tc.t.Log("unable to read the current config")
return
} }
defer res.Body.Close() defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body) body, err := ioutil.ReadAll(res.Body)
var out bytes.Buffer var out bytes.Buffer
json.Indent(&out, body, "", " ") json.Indent(&out, body, "", " ")
t.Logf("----------- failed with config -----------\n%s", out.String()) tc.t.Logf("----------- failed with config -----------\n%s", out.String())
} }
}) })
@ -101,11 +127,10 @@ func initServer(t *testing.T, rawConfig string, configType string) error {
client := &http.Client{ client := &http.Client{
Timeout: time.Second * 2, Timeout: time.Second * 2,
} }
start := time.Now() start := time.Now()
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig)) req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig))
if err != nil { if err != nil {
t.Errorf("failed to create request. %s", err) tc.t.Errorf("failed to create request. %s", err)
return err return err
} }
@ -117,7 +142,7 @@ func initServer(t *testing.T, rawConfig string, configType string) error {
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
t.Errorf("unable to contact caddy server. %s", err) tc.t.Errorf("unable to contact caddy server. %s", err)
return err return err
} }
timeElapsed(start, "caddytest: config load time") timeElapsed(start, "caddytest: config load time")
@ -125,7 +150,7 @@ func initServer(t *testing.T, rawConfig string, configType string) error {
defer res.Body.Close() defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body) body, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
t.Errorf("unable to read response. %s", err) tc.t.Errorf("unable to read response. %s", err)
return err return err
} }
@ -213,8 +238,8 @@ func prependCaddyFilePath(rawConfig string) string {
return r return r
} }
// creates a testing transport that forces call dialing connections to happen locally // CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func createTestingTransport() *http.Transport { func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{ dialer := net.Dialer{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@ -243,78 +268,44 @@ func createTestingTransport() *http.Transport {
// AssertLoadError will load a config and expect an error // AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
err := initServer(t, rawConfig, configType)
tc := NewTester(t)
err := tc.initServer(rawConfig, configType)
if !strings.Contains(err.Error(), expectedError) { if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
} }
} }
// AssertGetResponse request a URI and assert the status code and the body contains a string
func AssertGetResponse(t *testing.T, requestURI string, statusCode int, expectedBody string) (*http.Response, string) {
resp, body := AssertGetResponseBody(t, requestURI, statusCode)
if !strings.Contains(body, expectedBody) {
t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", requestURI, expectedBody, body)
}
return resp, body
}
// AssertGetResponseBody request a URI and assert the status code matches
func AssertGetResponseBody(t *testing.T, requestURI string, expectedStatusCode int) (*http.Response, string) {
client := &http.Client{
Transport: createTestingTransport(),
}
resp, err := client.Get(requestURI)
if err != nil {
t.Errorf("failed to call server %s", err)
return nil, ""
}
defer resp.Body.Close()
if expectedStatusCode != resp.StatusCode {
t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("unable to read the response body %s", err)
return nil, ""
}
return resp, string(body)
}
// AssertRedirect makes a request and asserts the redirection happens // AssertRedirect makes a request and asserts the redirection happens
func AssertRedirect(t *testing.T, requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
} }
client := &http.Client{ // using the existing client, we override the check redirect policy for this test
CheckRedirect: redirectPolicyFunc, old := tc.Client.CheckRedirect
Transport: createTestingTransport(), tc.Client.CheckRedirect = redirectPolicyFunc
} defer func() { tc.Client.CheckRedirect = old }()
resp, err := client.Get(requestURI) resp, err := tc.Client.Get(requestURI)
if err != nil { if err != nil {
t.Errorf("failed to call server %s", err) tc.t.Errorf("failed to call server %s", err)
return nil return nil
} }
if expectedStatusCode != resp.StatusCode { if expectedStatusCode != resp.StatusCode {
t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode) tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
} }
loc, err := resp.Location() loc, err := resp.Location()
if err != nil { if err != nil {
t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err) tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
} }
if expectedToLocation != loc.String() { if expectedToLocation != loc.String() {
t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String()) tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
} }
return resp return resp
@ -371,3 +362,124 @@ func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedRes
t.Fail() t.Fail()
} }
} }
// Generic request functions
func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
requestContentType := ""
for _, requestHeader := range requestHeaders {
arr := strings.SplitAfterN(requestHeader, ":", 2)
k := strings.TrimRight(arr[0], ":")
v := strings.TrimSpace(arr[1])
if k == "Content-Type" {
requestContentType = v
}
t.Logf("Request header: %s => %s", k, v)
req.Header.Set(k, v)
}
if requestContentType == "" {
t.Logf("Content-Type header not provided")
}
}
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.Client.Do(req)
if err != nil {
tc.t.Fatalf("failed to call server %s", err)
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
}
return resp
}
// AssertResponse request a URI and assert the status code and the body contains a string
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
tc.t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if !strings.Contains(body, expectedBody) {
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return resp, body
}
// Verb specific test functions
// AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}

View file

@ -1,6 +1,8 @@
package integration package integration
import ( import (
"net/http"
"net/url"
"testing" "testing"
"github.com/caddyserver/caddy/v2/caddytest" "github.com/caddyserver/caddy/v2/caddytest"
@ -9,7 +11,8 @@ import (
func TestRespond(t *testing.T) { func TestRespond(t *testing.T) {
// arrange // arrange
caddytest.InitServer(t, ` tester := caddytest.NewTester(t)
tester.InitServer(`
{ {
http_port 9080 http_port 9080
https_port 9443 https_port 9443
@ -23,13 +26,14 @@ func TestRespond(t *testing.T) {
`, "caddyfile") `, "caddyfile")
// act and assert // act and assert
caddytest.AssertGetResponse(t, "http://localhost:9080/version", 200, "hello from localhost") tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost")
} }
func TestRedirect(t *testing.T) { func TestRedirect(t *testing.T) {
// arrange // arrange
caddytest.InitServer(t, ` tester := caddytest.NewTester(t)
tester.InitServer(`
{ {
http_port 9080 http_port 9080
https_port 9443 https_port 9443
@ -46,10 +50,10 @@ func TestRedirect(t *testing.T) {
`, "caddyfile") `, "caddyfile")
// act and assert // act and assert
caddytest.AssertRedirect(t, "http://localhost:9080/", "http://localhost:9080/hello", 301) tester.AssertRedirect("http://localhost:9080/", "http://localhost:9080/hello", 301)
// follow redirect // follow redirect
caddytest.AssertGetResponse(t, "http://localhost:9080/", 200, "hello from localhost") tester.AssertGetResponse("http://localhost:9080/", 200, "hello from localhost")
} }
func TestDuplicateHosts(t *testing.T) { func TestDuplicateHosts(t *testing.T) {
@ -66,3 +70,34 @@ func TestDuplicateHosts(t *testing.T) {
"caddyfile", "caddyfile",
"duplicate site address not allowed") "duplicate site address not allowed")
} }
func TestReadCookie(t *testing.T) {
localhost, _ := url.Parse("http://localhost")
cookie := http.Cookie{
Name: "clientname",
Value: "caddytest",
}
// arrange
tester := caddytest.NewTester(t)
tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie})
tester.InitServer(`
{
http_port 9080
https_port 9443
}
localhost:9080 {
templates {
root testdata
}
file_server {
root testdata
}
}
`, "caddyfile")
// act and assert
tester.AssertGetResponse("http://localhost:9080/cookie.html", 200, "<h2>Cookie.ClientName caddytest</h2>")
}

View file

@ -9,7 +9,8 @@ import (
func TestDefaultSNI(t *testing.T) { func TestDefaultSNI(t *testing.T) {
// arrange // arrange
caddytest.InitServer(t, `{ tester := caddytest.NewTester(t)
tester.InitServer(`{
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": 9080,
@ -98,13 +99,14 @@ func TestDefaultSNI(t *testing.T) {
// act and assert // act and assert
// makes a request with no sni // makes a request with no sni
caddytest.AssertGetResponse(t, "https://127.0.0.1:9443/version", 200, "hello from a") tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
} }
func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange // arrange
caddytest.InitServer(t, ` tester := caddytest.NewTester(t)
tester.InitServer(`
{ {
"apps": { "apps": {
"http": { "http": {
@ -198,13 +200,14 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// act and assert // act and assert
// makes a request with no sni // makes a request with no sni
caddytest.AssertGetResponse(t, "https://127.0.0.1:9443/version", 200, "hello from a") tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
} }
func TestDefaultSNIWithPortMappingOnly(t *testing.T) { func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange // arrange
caddytest.InitServer(t, ` tester := caddytest.NewTester(t)
tester.InitServer(`
{ {
"apps": { "apps": {
"http": { "http": {
@ -270,7 +273,7 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// act and assert // act and assert
// makes a request with no sni // makes a request with no sni
caddytest.AssertGetResponse(t, "https://127.0.0.1:9443/version", 200, "hello from a") tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a")
} }
func TestHttpOnlyOnDomainWithSNI(t *testing.T) { func TestHttpOnlyOnDomainWithSNI(t *testing.T) {

View file

@ -0,0 +1 @@
<h2>Cookie.ClientName {{.Cookie "clientname"}}</h2>