summaryrefslogtreecommitdiff
path: root/caddytest
diff options
context:
space:
mode:
Diffstat (limited to 'caddytest')
-rw-r--r--caddytest/caddy.localhost.crt23
-rw-r--r--caddytest/caddy.localhost.key27
-rw-r--r--caddytest/caddytest.go272
-rw-r--r--caddytest/caddytest_test.go33
-rw-r--r--caddytest/integration/caddyfile_test.go91
5 files changed, 446 insertions, 0 deletions
diff --git a/caddytest/caddy.localhost.crt b/caddytest/caddy.localhost.crt
new file mode 100644
index 0000000..a3a2f4c
--- /dev/null
+++ b/caddytest/caddy.localhost.crt
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID5zCCAs8CFFmAAFKV79uhzxc5qXbUw3oBNsYXMA0GCSqGSIb3DQEBCwUAMIGv
+MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZBgNVBAoMEkxvY2FsIERldmVs
+b3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxvcGVtZW50MRowGAYDVQQDDBEq
+LmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9jYWwgRGV2ZWxvcGVtZW50MSAw
+HgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2NhbDAeFw0yMDAzMDIwODAxMTZa
+Fw0zMDAyMjgwODAxMTZaMIGvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxGzAZ
+BgNVBAoMEkxvY2FsIERldmVsb3BlbWVudDEbMBkGA1UEBwwSTG9jYWwgRGV2ZWxv
+cGVtZW50MRowGAYDVQQDDBEqLmNhZGR5LmxvY2FsaG9zdDEbMBkGA1UECwwSTG9j
+YWwgRGV2ZWxvcGVtZW50MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBjYWRkeS5sb2Nh
+bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJngfeirQkWaU8ihgIC5
+SKpRQX/3koRjljDK/oCbhLs+wg592kIwVv06l7+mn7NSaNBloabjuA1GqyLRsNLL
+ptrv0HvXa5qLx28+icsb2Ny3dJnQaj9w9PwjxQ1qZqEJfWRH1D8Vz9AmB+QSV/Gu
+8e8alGFewlYZVfH1kbxoTT6QorF37TeA3bh1fgKFtzsGYKswcaZNdDBBHzLunCKZ
+HU6U6L45hm+yLADj3mmDLafUeiVOt6MRLLoSD1eLRVSXGrNo+brJ87zkZntI9+W1
+JxOBoXtZCwka7k2DlAtLihsrmBZA2ZC9yVeu/SQy3qb3iCNnTFTCyAnWeTCr6Tcq
+6w8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOWfXqpAmD4C3wGiMeZAeaaS4hDAR
++JmN+avPDA6F6Bq7DB4NJuIwVUlaDL2s07w5VJJtW52aZVKoBlgHR5yG/XUli6J7
+YUJRmdQJvHUSu26cmKvyoOaTrEYbmvtGICWtZc8uTlMf9wQZbJA4KyxTgEQJDXsZ
+B2XFe+wVdhAgEpobYDROi+l/p8TL5z3U24LpwVTcJy5sEZVv7Wfs886IyxU8ORt8
+VZNcDiH6V53OIGeiufIhia/mPe6jbLntfGZfIFxtCcow4IA/lTy1ned7K5fmvNNb
+ZilxOQUk+wVK8genjdrZVAnAxsYLHJIb5yf9O7rr6fWciVMF3a0k5uNK1w==
+-----END CERTIFICATE-----
diff --git a/caddytest/caddy.localhost.key b/caddytest/caddy.localhost.key
new file mode 100644
index 0000000..d85cd40
--- /dev/null
+++ b/caddytest/caddy.localhost.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAmeB96KtCRZpTyKGAgLlIqlFBf/eShGOWMMr+gJuEuz7CDn3a
+QjBW/TqXv6afs1Jo0GWhpuO4DUarItGw0sum2u/Qe9drmovHbz6JyxvY3Ld0mdBq
+P3D0/CPFDWpmoQl9ZEfUPxXP0CYH5BJX8a7x7xqUYV7CVhlV8fWRvGhNPpCisXft
+N4DduHV+AoW3OwZgqzBxpk10MEEfMu6cIpkdTpTovjmGb7IsAOPeaYMtp9R6JU63
+oxEsuhIPV4tFVJcas2j5usnzvORme0j35bUnE4Ghe1kLCRruTYOUC0uKGyuYFkDZ
+kL3JV679JDLepveII2dMVMLICdZ5MKvpNyrrDwIDAQABAoIBAFcPK01zb6hfm12c
++k5aBiHOnUdgc/YRPg1XHEz5MEycQkDetZjTLrRQ7UBSbnKPgpu9lIsOtbhVLkgh
+6XAqJroiCou2oruqr+hhsqZGmBiwdvj7cNF6ADGTr05az7v22YneFdinZ481pStF
+sZocx+bm2+KHMV5zMSwXKyA0xtdJLxs2yklniDBxSZRppgppq1pDPprP5DkgKPfe
+3ekUmbQd5bHmivhW8ItbJLuf82XSsMBZ9ZhKiKIlWlbKAgiSV3SqnUQb5fi7l8hG
+yYZxbuCUIGFwKmEpUBBt/nyxrOlMiNtDh9JhrPmijTV3slq70pCLwLL/Ai2aeear
+EVA5VhkCgYEAyAmxfPqc2P7BsDAp67/sA7OEPso9qM4WyuWiVdlX2gb9TLNLYbPX
+Kk/UmpAIVzpoTAGY5Zp3wkvdD/ou8uUQsE8ioNn4S1a4G9XURH1wVhcEbUiAKI1S
+QVBH9B/Pj3eIp5OTKwob0Wj7DNdxoH7ed/Eok0EaTWzOA8pCWADKv/MCgYEAxOzY
+YsX7Nl+eyZr2+9unKyeAK/D1DCT/o99UUAHx72/xaBVP/06cfzpvKBNcF9iYc+fq
+R1yIUIrDRoSmYKBq+Kb3+nOg1nrqih/NBTokbTiI4Q+/30OQt0Al1e7y9iNKqV8H
+jYZItzluGNrWKedZbATwBwbVCY2jnNl6RMDnS3UCgYBxj3cwQUHLuoyQjjcuO80r
+qLzZvIxWiXDNDKIk5HcIMlGYOmz/8U2kGp/SgxQJGQJeq8V2C0QTjGfaCyieAcaA
+oNxCvptDgd6RBsoze5bLeNOtiqwe2WOp6n5+q5R0mOJ+Z7vzghCayGNFPgWmnH+F
+TeW/+wSIkc0+v5L8TK7NWwKBgBrlWlyLO9deUfqpHqihhICBYaEexOlGuF+yZfqT
+eW7BdFBJ8OYm33sFCR+JHV/oZlIWT8o1Wizd9vPPtEWoQ1P4wg/D8Si6GwSIeWEI
+YudD/HX4x7T/rmlI6qIAg9CYW18sqoRq3c2gm2fro6qPfYgiWIItLbWjUcBfd7Ki
+QjTtAoGARKdRv3jMWL84rlEx1nBRgL3pe9Dt+Uxzde2xT3ZeF+5Hp9NfU01qE6M6
+1I6H64smqpetlsXmCEVKwBemP3pJa6avLKgIYiQvHAD/v4rs9mqgy1RTqtYyGNhR
+1A/6dKkbiZ6wzePLLPasXVZxSKEviXf5gJooqumQVSVhCswyCZ0=
+-----END RSA PRIVATE KEY-----
diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go
new file mode 100644
index 0000000..04b65ba
--- /dev/null
+++ b/caddytest/caddytest.go
@@ -0,0 +1,272 @@
+package caddytest
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "path"
+ "regexp"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+)
+
+// Defaults store any configuration required to make the tests run
+type Defaults struct {
+ // Port we expect caddy to listening on
+ AdminPort int
+ // Certificates we expect to be loaded before attempting to run the tests
+ Certifcates []string
+}
+
+// Default testing values
+var Default = Defaults{
+ AdminPort: 2019,
+ Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
+}
+
+var (
+ matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
+ matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
+)
+
+type configLoadError struct {
+ Response string
+}
+
+func (e configLoadError) Error() string { return e.Response }
+
+// InitServer this will configure the server with a configurion of a specific
+// type. The configType must be either "json" or the adapter type.
+func InitServer(t *testing.T, rawConfig string, configType string) {
+ if err := initServer(t, rawConfig, configType); errors.Is(err, &configLoadError{}) {
+ t.Logf("failed to load config: %s", err)
+ t.Fail()
+ }
+}
+
+// InitServer this will configure the server with a configurion of a specific
+// type. The configType must be either "json" or the adapter type.
+func initServer(t *testing.T, rawConfig string, configType string) error {
+
+ err := validateTestPrerequisites()
+ if err != nil {
+ t.Skipf("skipping tests as failed integration prerequisites. %s", err)
+ return nil
+ }
+
+ t.Cleanup(func() {
+ if t.Failed() {
+ res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
+ if err != nil {
+ t.Log("unable to read the current config")
+ }
+ defer res.Body.Close()
+ body, err := ioutil.ReadAll(res.Body)
+
+ var out bytes.Buffer
+ json.Indent(&out, body, "", " ")
+ t.Logf("----------- failed with config -----------\n%s", out.String())
+ }
+ })
+
+ rawConfig = prependCaddyFilePath(rawConfig)
+ client := &http.Client{
+ Timeout: time.Second * 2,
+ }
+ req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig))
+ if err != nil {
+ t.Errorf("failed to create request. %s", err)
+ return err
+ }
+
+ if configType == "json" {
+ req.Header.Add("Content-Type", "application/json")
+ } else {
+ req.Header.Add("Content-Type", "text/"+configType)
+ }
+
+ res, err := client.Do(req)
+ if err != nil {
+ t.Errorf("unable to contact caddy server. %s", err)
+ return err
+ }
+ defer res.Body.Close()
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Errorf("unable to read response. %s", err)
+ return err
+ }
+
+ if res.StatusCode != 200 {
+ return configLoadError{Response: string(body)}
+ }
+
+ return nil
+}
+
+var hasValidated bool
+var arePrerequisitesValid bool
+
+func validateTestPrerequisites() error {
+
+ if hasValidated {
+ if !arePrerequisitesValid {
+ return errors.New("caddy integration prerequisites failed. see first error")
+ }
+ return nil
+ }
+
+ hasValidated = true
+ arePrerequisitesValid = false
+
+ // check certificates are found
+ for _, certName := range Default.Certifcates {
+ if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
+ return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
+ }
+ }
+
+ // assert that caddy is running
+ client := &http.Client{
+ Timeout: time.Second * 2,
+ }
+ _, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
+ if err != nil {
+ return errors.New("caddy integration test caddy server not running. Expected to be listening on localhost:2019")
+ }
+
+ arePrerequisitesValid = true
+ return nil
+}
+
+func getIntegrationDir() string {
+
+ _, filename, _, ok := runtime.Caller(1)
+ if !ok {
+ panic("unable to determine the current file path")
+ }
+
+ return path.Dir(filename)
+}
+
+// use the convention to replace /[certificatename].[crt|key] with the full path
+// this helps reduce the noise in test configurations and also allow this
+// to run in any path
+func prependCaddyFilePath(rawConfig string) string {
+ r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
+ r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
+ return r
+}
+
+// creates a testing transport that forces call dialing connections to happen locally
+func createTestingTransport() *http.Transport {
+
+ dialer := net.Dialer{
+ Timeout: 5 * time.Second,
+ KeepAlive: 5 * time.Second,
+ DualStack: true,
+ }
+
+ dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
+ parts := strings.Split(addr, ":")
+ destAddr := fmt.Sprintf("127.0.0.1:%s", parts[1])
+ log.Printf("caddytest: redirecting the dialer from %s to %s", addr, destAddr)
+ return dialer.DialContext(ctx, network, destAddr)
+ }
+
+ return &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: dialContext,
+ ForceAttemptHTTP2: true,
+ MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 5 * time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ }
+}
+
+// AssertLoadError will load a config and expect an error
+func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
+ err := initServer(t, rawConfig, configType)
+ if !strings.Contains(err.Error(), expectedError) {
+ 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("expected response body \"%s\" but got \"%s\"", expectedBody, body)
+ }
+ return resp, string(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("expected status code: %d but got %d", 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
+func AssertRedirect(t *testing.T, requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
+
+ redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
+ return http.ErrUseLastResponse
+ }
+
+ client := &http.Client{
+ CheckRedirect: redirectPolicyFunc,
+ Transport: createTestingTransport(),
+ }
+
+ resp, err := client.Get(requestURI)
+ if err != nil {
+ t.Errorf("failed to call server %s", err)
+ return nil
+ }
+
+ if expectedStatusCode != resp.StatusCode {
+ t.Errorf("expected status code: %d but got %d", expectedStatusCode, resp.StatusCode)
+ }
+
+ loc, err := resp.Location()
+ if expectedToLocation != loc.String() {
+ t.Errorf("expected location: \"%s\" but got \"%s\"", expectedToLocation, loc.String())
+ }
+
+ return resp
+}
diff --git a/caddytest/caddytest_test.go b/caddytest/caddytest_test.go
new file mode 100644
index 0000000..a46867c
--- /dev/null
+++ b/caddytest/caddytest_test.go
@@ -0,0 +1,33 @@
+package caddytest
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestReplaceCertificatePaths(t *testing.T) {
+ rawConfig := `a.caddy.localhost:9443 {
+ tls /caddy.localhost.crt /caddy.localhost.key {
+ }
+
+ redir / https://b.caddy.localhost:9443/version 301
+
+ respond /version 200 {
+ body "hello from a.caddy.localhost"
+ }
+ }`
+
+ r := prependCaddyFilePath(rawConfig)
+
+ if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.crt") {
+ t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
+ }
+
+ if !strings.Contains(r, getIntegrationDir()+"/caddy.localhost.key") {
+ t.Error("expected the /caddy.localhost.crt to be expanded to include the full path")
+ }
+
+ if !strings.Contains(r, "https://b.caddy.localhost:9443/version") {
+ t.Error("expected redirect uri to be unchanged")
+ }
+}
diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go
new file mode 100644
index 0000000..33b4b2d
--- /dev/null
+++ b/caddytest/integration/caddyfile_test.go
@@ -0,0 +1,91 @@
+package integration
+
+import (
+ "testing"
+
+ "github.com/caddyserver/caddy/v2/caddytest"
+)
+
+func TestRespond(t *testing.T) {
+
+ // arrange
+ caddytest.InitServer(t, `
+ {
+ http_port 9080
+ https_port 9443
+ }
+
+ localhost:9080 {
+ respond /version 200 {
+ body "hello from a.caddy.localhost"
+ }
+ }
+ `, "caddyfile")
+
+ // act and assert
+ caddytest.AssertGetResponse(t, "http://localhost:9080/version", 200, "hello from a.caddy.localhost")
+}
+
+func TestRedirect(t *testing.T) {
+
+ // arrange
+ caddytest.InitServer(t, `
+ {
+ http_port 9080
+ https_port 9443
+ }
+
+ localhost:9080 {
+
+ redir / http://localhost:9080/hello 301
+
+ respond /hello 200 {
+ body "hello from b"
+ }
+ }
+ `, "caddyfile")
+
+ // act and assert
+ caddytest.AssertRedirect(t, "http://localhost:9080/", "http://localhost:9080/hello", 301)
+
+ // follow redirect
+ caddytest.AssertGetResponse(t, "http://localhost:9080/", 200, "hello from b")
+}
+
+func TestDuplicateHosts(t *testing.T) {
+
+ // act and assert
+ caddytest.AssertLoadError(t,
+ `
+ localhost:9080 {
+ }
+
+ localhost:9080 {
+ }
+ `,
+ "caddyfile",
+ "duplicate site address not allowed")
+}
+
+func TestDefaultSNI(t *testing.T) {
+
+ // arrange
+ caddytest.InitServer(t, `
+ {
+ http_port 9080
+ https_port 9443
+ default_sni *.caddy.localhost
+ }
+
+ 127.0.0.1:9443 {
+ tls /caddy.localhost.crt /caddy.localhost.key {
+ }
+ respond /version 200 {
+ body "hello from a.caddy.localhost"
+ }
+ }
+ `, "caddyfile")
+
+ // act and assert
+ caddytest.AssertGetResponse(t, "https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost")
+}