diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index d4a0a6c87..f22a09ef2 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -1,14 +1,12 @@ package caddytest import ( - "bytes" "context" "crypto/tls" "encoding/json" "errors" "fmt" "io" - "io/fs" "log" "net" "net/http" @@ -16,14 +14,10 @@ import ( "os" "path" "reflect" - "regexp" "runtime" "strings" - "testing" "time" - "github.com/aryann/difflib" - caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -51,23 +45,18 @@ var Default = Defaults{ LoadRequestTimeout: 5 * time.Second, } -var ( - matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`) - matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`) -) - // Tester represents an instance of a test client. type Tester struct { - Client *http.Client - configLoaded bool - t testing.TB + Client *http.Client + configLoaded bool + configFileName string } // NewTester will create a new testing client with an attached cookie jar -func NewTester(t testing.TB) *Tester { +func NewTester() (*Tester, error) { jar, err := cookiejar.New(nil) if err != nil { - t.Fatalf("failed to create cookiejar: %s", err) + return nil, fmt.Errorf("failed to create cookiejar: %w", err) } return &Tester{ @@ -77,12 +66,7 @@ func NewTester(t testing.TB) *Tester { Timeout: Default.TestRequestTimeout, }, configLoaded: false, - t: t, - } -} - -func (t *Tester) T() testing.TB { - return t.t + }, nil } type configLoadError struct { @@ -97,83 +81,40 @@ func timeElapsed(start time.Time, name string) { } // launch caddy will start the server -func (tc *Tester) LaunchCaddy() { +func (tc *Tester) LaunchCaddy() error { if err := tc.startServer(); err != nil { - tc.t.Logf("failed to start server: %s", err) - tc.t.Fail() + return fmt.Errorf("failed to start server: %w", err) } -} - -// DEPRECATED: InitServer -// Initserver is shorthand for LaunchCaddy() and LoadConfig(rawConfig, configType string) -func (tc *Tester) InitServer(rawConfig string, configType string) { - if err := tc.startServer(); err != nil { - tc.t.Logf("failed to start server: %s", err) - tc.t.Fail() - } - if err := tc.LoadConfig(rawConfig, configType); err != nil { - tc.t.Logf("failed ensuring config is running: %s", err) - tc.t.Fail() - } -} - -func (tc *Tester) startServer() error { - if testing.Short() { - tc.t.SkipNow() - return nil - } - err := validateAndStartServer(tc.t) - if err != nil { - tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err) - return nil - } - tc.t.Cleanup(func() { - if tc.t.Failed() && tc.configLoaded { - res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) - if err != nil { - tc.t.Log("unable to read the current config") - return - } - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - var out bytes.Buffer - _ = json.Indent(&out, body, "", " ") - tc.t.Logf("----------- failed with config -----------\n%s", out.String()) - } - // now shutdown the server, since the test is done. - - _, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", Default.AdminPort), "", nil) - if err != nil { - tc.t.Log("couldn't stop admin server") - } - time.Sleep(1 * time.Millisecond) - // try ensure the admin api is stopped three times. - for retries := 0; retries < 3; retries++ { - if isCaddyAdminRunning() != nil { - return - } - time.Sleep(10 * time.Millisecond) - tc.t.Log("timed out waiting for admin server to stop") - } - }) return nil } -func (tc *Tester) MustLoadConfig(rawConfig string, configType string) { - if err := tc.LoadConfig(rawConfig, configType); err != nil { - tc.t.Logf("failed ensuring config is running: %s", err) - tc.t.Fail() +func (tc *Tester) CleanupCaddy() error { + // now shutdown the server, since the test is done. + defer func() { + // try to remove the tmp config file we created + os.Remove(tc.configFileName) + }() + _, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", Default.AdminPort), "", nil) + if err != nil { + return fmt.Errorf("couldn't stop caddytest server") } + time.Sleep(200 * time.Millisecond) + for retries := 0; retries < 10; retries++ { + if isCaddyAdminRunning() != nil { + return nil + } + time.Sleep(100 * time.Millisecond) + } + + return fmt.Errorf("timed out waiting for caddytest server to stop") + } // LoadConfig loads the config to the tester server and also ensures that the config was loaded func (tc *Tester) LoadConfig(rawConfig string, configType string) error { originalRawConfig := rawConfig - rawConfig = prependCaddyFilePath(rawConfig) // normalize JSON config if configType == "json" { - tc.t.Logf("Before: %s", rawConfig) var conf any if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil { return err @@ -183,7 +124,6 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { return err } rawConfig = string(c) - tc.t.Logf("After: %s", rawConfig) } client := &http.Client{ Timeout: Default.LoadRequestTimeout, @@ -191,8 +131,7 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { start := time.Now() req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig)) if err != nil { - tc.t.Errorf("failed to create request. %s", err) - return err + return fmt.Errorf("failed to create request. %w", err) } if configType == "json" { @@ -203,16 +142,14 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { res, err := client.Do(req) if err != nil { - tc.t.Errorf("unable to contact caddy server. %s", err) - return err + return fmt.Errorf("unable to contact caddy server. %w", err) } timeElapsed(start, "caddytest: config load time") defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { - tc.t.Errorf("unable to read response. %s", err) - return err + return fmt.Errorf("unable to read response. %w", err) } if res.StatusCode != 200 { @@ -224,7 +161,7 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { } func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error { - expectedBytes := []byte(prependCaddyFilePath(rawConfig)) + expectedBytes := []byte(rawConfig) if configType != "json" { adapter := caddyconfig.GetAdapter(configType) if adapter == nil { @@ -269,7 +206,6 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error } time.Sleep(1 * time.Second) } - tc.t.Errorf("POSTed configuration isn't active") return errors.New("EnsureConfigRunning: POSTed configuration isn't active") } @@ -278,38 +214,30 @@ const initConfig = `{ } ` -// validateAndStartServer ensures the certificates are available in the -// designated path, launches caddy, and then ensures the Caddy sub-process is running. -func validateAndStartServer(t testing.TB) error { - // check certificates are found - for _, certName := range Default.Certificates { - if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("caddy integration test certificates (%s) not found", certName) - } +// launches caddy, and then ensures the Caddy sub-process is running. +func (tc *Tester) startServer() error { + if isCaddyAdminRunning() == nil { + return fmt.Errorf("caddy test admin port still in use") + } + // setup the init config file, and set the cleanup afterwards + f, err := os.CreateTemp("", "") + if err != nil { + return err + } + tc.configFileName = f.Name() + + if _, err := f.WriteString(initConfig); err != nil { + return err } - if isCaddyAdminRunning() != nil { - // setup the init config file, and set the cleanup afterwards - f, err := os.CreateTemp("", "") - if err != nil { - return err - } - t.Cleanup(func() { - os.Remove(f.Name()) - }) - if _, err := f.WriteString(initConfig); err != nil { - return err - } - - // start inprocess caddy server - os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"} - go func() { - caddycmd.Main() - }() - // wait for caddy admin api to start. it should happen quickly. - for retries := 3; retries > 0 && isCaddyAdminRunning() != nil; retries-- { - time.Sleep(1 * time.Second) - } + // start inprocess caddy server + os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"} + go func() { + caddycmd.Main() + }() + // wait for caddy admin api to start. it should happen quickly. + for retries := 3; retries > 0 && isCaddyAdminRunning() != nil; retries-- { + time.Sleep(1 * time.Second) } // one more time to return the error @@ -339,15 +267,6 @@ func getIntegrationDir() string { 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 -} - // CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally func CreateTestingTransport() *http.Transport { dialer := net.Dialer{ @@ -374,231 +293,3 @@ func CreateTestingTransport() *http.Transport { TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec } } - -// AssertLoadError will load a config and expect an error -func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { - tc := NewTester(t) - tc.LaunchCaddy() - err := tc.LoadConfig(rawConfig, configType) - if !strings.Contains(err.Error(), expectedError) { - t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) - } -} - -// AssertRedirect makes a request and asserts the redirection happens -func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { - redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - - // using the existing client, we override the check redirect policy for this test - old := tc.Client.CheckRedirect - tc.Client.CheckRedirect = redirectPolicyFunc - defer func() { tc.Client.CheckRedirect = old }() - - resp, err := tc.Client.Get(requestURI) - if err != nil { - tc.t.Errorf("failed to call server %s", err) - return nil - } - - if expectedStatusCode != resp.StatusCode { - tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode) - } - - loc, err := resp.Location() - if err != nil { - tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err) - } - if loc == nil && expectedToLocation != "" { - tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI) - } - if loc != nil { - if expectedToLocation != loc.String() { - tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String()) - } - } - - return resp -} - -// CompareAdapt adapts a config and then compares it against an expected result -func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool { - cfgAdapter := caddyconfig.GetAdapter(adapterName) - if cfgAdapter == nil { - t.Logf("unrecognized config adapter '%s'", adapterName) - return false - } - - options := make(map[string]any) - - result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options) - if err != nil { - t.Logf("adapting config using %s adapter: %v", adapterName, err) - return false - } - - // prettify results to keep tests human-manageable - var prettyBuf bytes.Buffer - err = json.Indent(&prettyBuf, result, "", "\t") - if err != nil { - return false - } - result = prettyBuf.Bytes() - - if len(warnings) > 0 { - for _, w := range warnings { - t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message) - } - } - - diff := difflib.Diff( - strings.Split(expectedResponse, "\n"), - strings.Split(string(result), "\n")) - - // scan for failure - failed := false - for _, d := range diff { - if d.Delta != difflib.Common { - failed = true - break - } - } - - if failed { - for _, d := range diff { - switch d.Delta { - case difflib.Common: - fmt.Printf(" %s\n", d.Payload) - case difflib.LeftOnly: - fmt.Printf(" - %s\n", d.Payload) - case difflib.RightOnly: - fmt.Printf(" + %s\n", d.Payload) - } - } - return false - } - return true -} - -// AssertAdapt adapts a config and then tests it against an expected result -func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) { - ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse) - if !ok { - t.Fail() - } -} - -// Generic request functions - -func applyHeaders(t testing.TB, 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.URL.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 := io.ReadAll(resp.Body) - if err != nil { - tc.t.Fatalf("unable to read the response body %s", err) - } - - body := string(bytes) - - if 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) -} diff --git a/caddytest/caddytest_assert.go b/caddytest/caddytest_assert.go new file mode 100644 index 000000000..d81764d66 --- /dev/null +++ b/caddytest/caddytest_assert.go @@ -0,0 +1,109 @@ +package caddytest + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/aryann/difflib" + "github.com/caddyserver/caddy/v2/caddyconfig" +) + +// AssertLoadError will load a config and expect an error +func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { + tc := StartHarness(t) + err := tc.tester.LoadConfig(rawConfig, configType) + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) + } +} + +// CompareAdapt adapts a config and then compares it against an expected result +func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool { + cfgAdapter := caddyconfig.GetAdapter(adapterName) + if cfgAdapter == nil { + t.Logf("unrecognized config adapter '%s'", adapterName) + return false + } + + options := make(map[string]any) + + result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options) + if err != nil { + t.Logf("adapting config using %s adapter: %v", adapterName, err) + return false + } + + // prettify results to keep tests human-manageable + var prettyBuf bytes.Buffer + err = json.Indent(&prettyBuf, result, "", "\t") + if err != nil { + return false + } + result = prettyBuf.Bytes() + + if len(warnings) > 0 { + for _, w := range warnings { + t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message) + } + } + + diff := difflib.Diff( + strings.Split(expectedResponse, "\n"), + strings.Split(string(result), "\n")) + + // scan for failure + failed := false + for _, d := range diff { + if d.Delta != difflib.Common { + failed = true + break + } + } + + if failed { + for _, d := range diff { + switch d.Delta { + case difflib.Common: + fmt.Printf(" %s\n", d.Payload) + case difflib.LeftOnly: + fmt.Printf(" - %s\n", d.Payload) + case difflib.RightOnly: + fmt.Printf(" + %s\n", d.Payload) + } + } + return false + } + return true +} + +// AssertAdapt adapts a config and then tests it against an expected result +func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) { + ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse) + if !ok { + t.Fail() + } +} + +// Generic request functions + +func applyHeaders(t testing.TB, 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") + } +} diff --git a/caddytest/caddytest_test.go b/caddytest/caddytest_test.go index 937537faa..589c33ea6 100644 --- a/caddytest/caddytest_test.go +++ b/caddytest/caddytest_test.go @@ -12,10 +12,10 @@ func TestReplaceCertificatePaths(t *testing.T) { } redir / https://b.caddy.localhost:9443/version 301 - + respond /version 200 { body "hello from a.caddy.localhost" - } + } }` r := prependCaddyFilePath(rawConfig) @@ -34,8 +34,8 @@ func TestReplaceCertificatePaths(t *testing.T) { } func TestLoadUnorderedJSON(t *testing.T) { - tester := NewTester(t) - tester.InitServer(` + tester := StartHarness(t) + tester.LoadConfig(` { "logging": { "logs": { diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index 43f99b15e..9697cf955 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -19,27 +19,13 @@ import ( "go.uber.org/zap" ) -func curry2[A, B any](fn func(A, B)) func(a A) func(b B) { - return func(a A) func(B) { - return func(b B) { - fn(a, b) - } - } -} - const acmeChallengePort = 9081 -func TestAcmeServer(t *testing.T) { - tester := caddytest.NewTester(t) - tester.LaunchCaddy() - t.Run("WithDefaults", curry2(testACMEServerWithDefaults)(tester)) - t.Run("WithMismatchedChallenges", curry2(testACMEServerWithDefaults)(tester)) -} - // Test the basic functionality of Caddy's ACME server -func testACMEServerWithDefaults(tester *caddytest.Tester, t *testing.T) { +func TestACMEServerWithDefaults(t *testing.T) { ctx := context.Background() - tester.MustLoadConfig(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -56,7 +42,7 @@ func testACMEServerWithDefaults(tester *caddytest.Tester, t *testing.T) { client := acmez.Client{ Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + HTTPClient: tester.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ @@ -102,10 +88,20 @@ func testACMEServerWithDefaults(tester *caddytest.Tester, t *testing.T) { } } -func testACMEServerWithMismatchedChallenges(tester *caddytest.Tester, t *testing.T) { +func TestACMEServerWithMismatchedChallenges(t *testing.T) { ctx := context.Background() logger := caddy.Log().Named("acmez") - tester.MustLoadConfig(` + + tester := caddytest.StartHarness(t) + tester.LoadConfig(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + } + acme.localhost { acme_server { challenges tls-alpn-01 } @@ -115,7 +111,7 @@ func testACMEServerWithMismatchedChallenges(tester *caddytest.Tester, t *testing client := acmez.Client{ Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + HTTPClient: tester.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index f05fb836e..e3ce29ac0 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -15,8 +15,8 @@ import ( ) func TestACMEServerDirectory(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust local_certs @@ -41,8 +41,8 @@ func TestACMEServerDirectory(t *testing.T) { } func TestACMEServerAllowPolicy(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust local_certs @@ -71,7 +71,7 @@ func TestACMEServerAllowPolicy(t *testing.T) { client := acmez.Client{ Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + HTTPClient: tester.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ @@ -127,8 +127,8 @@ func TestACMEServerAllowPolicy(t *testing.T) { } func TestACMEServerDenyPolicy(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust local_certs @@ -156,7 +156,7 @@ func TestACMEServerDenyPolicy(t *testing.T) { client := acmez.Client{ Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + HTTPClient: tester.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ diff --git a/caddytest/integration/autohttps_test.go b/caddytest/integration/autohttps_test.go index 1dbdbcee2..993fa2b9a 100644 --- a/caddytest/integration/autohttps_test.go +++ b/caddytest/integration/autohttps_test.go @@ -8,8 +8,8 @@ import ( ) func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 skip_install_trust @@ -24,8 +24,8 @@ func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) { } func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -40,8 +40,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) { } func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -56,8 +56,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T } func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -98,8 +98,8 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) { } func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -123,8 +123,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) { } func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go index 11ffc08ae..44c28cd58 100644 --- a/caddytest/integration/caddyfile_test.go +++ b/caddytest/integration/caddyfile_test.go @@ -10,19 +10,19 @@ import ( func TestRespond(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 https_port 9443 grace_period 1ns } - + localhost:9080 { respond /version 200 { body "hello from localhost" - } + } } `, "caddyfile") @@ -32,22 +32,22 @@ func TestRespond(t *testing.T) { func TestRedirect(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 https_port 9443 grace_period 1ns } - + localhost:9080 { - + redir / http://localhost:9080/hello 301 - + respond /hello 200 { body "hello from localhost" - } + } } `, "caddyfile") @@ -64,8 +64,8 @@ func TestDuplicateHosts(t *testing.T) { ` localhost:9080 { } - - localhost:9080 { + + localhost:9080 { } `, "caddyfile", @@ -80,9 +80,9 @@ func TestReadCookie(t *testing.T) { } // arrange - tester := caddytest.NewTester(t) - tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie}) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.Client().Jar.SetCookies(localhost, []*http.Cookie{&cookie}) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -90,7 +90,7 @@ func TestReadCookie(t *testing.T) { https_port 9443 grace_period 1ns } - + localhost:9080 { templates { root testdata @@ -106,8 +106,8 @@ func TestReadCookie(t *testing.T) { } func TestReplIndex(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -481,9 +481,9 @@ func TestValidPrefix(t *testing.T) { } func TestUriReplace(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -491,16 +491,16 @@ func TestUriReplace(t *testing.T) { :9080 uri replace "\}" %7D uri replace "\{" %7B - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D") } func TestUriOps(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -511,7 +511,7 @@ func TestUriOps(t *testing.T) { uri query taz test uri query key=value example uri query changethis>changed - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest&changethis=val", 200, "changed=val&foo=bar0&foo=bar&key%3Dvalue=example&taz=test") @@ -523,9 +523,9 @@ func TestUriOps(t *testing.T) { // refer to 127.0.0.1 or ::1. // TODO: Test each http version separately (especially http/3) func TestHttpRequestLocalPortPlaceholder(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -537,9 +537,9 @@ func TestHttpRequestLocalPortPlaceholder(t *testing.T) { } func TestSetThenAddQueryParams(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -547,16 +547,16 @@ func TestSetThenAddQueryParams(t *testing.T) { :9080 uri query foo bar uri query +foo baz - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint", 200, "foo=bar&foo=baz") } func TestSetThenDeleteParams(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -564,16 +564,16 @@ func TestSetThenDeleteParams(t *testing.T) { :9080 uri query bar foo{query.foo} uri query -foo - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=foobar") } func TestRenameAndOtherOps(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -582,36 +582,36 @@ func TestRenameAndOtherOps(t *testing.T) { uri query foo>bar uri query bar taz uri query +bar baz - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=taz&bar=baz") } func TestReplaceOps(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query foo bar baz + uri query foo bar baz respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=baz") } func TestReplaceWithReplacementPlaceholder(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query foo bar {query.placeholder} + uri query foo bar {query.placeholder} respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=baz&foo=bar", 200, "foo=baz&placeholder=baz") @@ -619,66 +619,66 @@ func TestReplaceWithReplacementPlaceholder(t *testing.T) { } func TestReplaceWithKeyPlaceholder(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query {query.placeholder} bar baz + uri query {query.placeholder} bar baz respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=foo&foo=bar", 200, "foo=baz&placeholder=foo") } func TestPartialReplacement(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query foo ar az + uri query foo ar az respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=baz") } func TestNonExistingSearch(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query foo var baz + uri query foo var baz respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=bar") } func TestReplaceAllOps(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query * bar baz + uri query * bar baz respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar&baz=bar", 200, "baz=baz&foo=baz") } func TestUriOpsBlock(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -688,15 +688,15 @@ func TestUriOpsBlock(t *testing.T) { +foo bar -baz taz test - } + } respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest", 200, "foo=bar0&foo=bar&taz=test") } func TestHandleErrorSimpleCodes(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ admin localhost:2999 http_port 9080 } @@ -704,7 +704,7 @@ func TestHandleErrorSimpleCodes(t *testing.T) { root * /srv error /private* "Unauthorized" 410 error /hidden* "Not found" 404 - + handle_errors 404 410 { respond "404 or 410 error" } @@ -715,8 +715,8 @@ func TestHandleErrorSimpleCodes(t *testing.T) { } func TestHandleErrorRange(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ admin localhost:2999 http_port 9080 } @@ -735,8 +735,8 @@ func TestHandleErrorRange(t *testing.T) { } func TestHandleErrorSort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ admin localhost:2999 http_port 9080 } @@ -759,8 +759,8 @@ func TestHandleErrorSort(t *testing.T) { } func TestHandleErrorRangeAndCodes(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ admin localhost:2999 http_port 9080 } diff --git a/caddytest/integration/handler_test.go b/caddytest/integration/handler_test.go index afc700b02..62b37939f 100644 --- a/caddytest/integration/handler_test.go +++ b/caddytest/integration/handler_test.go @@ -9,8 +9,8 @@ import ( ) func TestBrowse(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -32,8 +32,8 @@ func TestBrowse(t *testing.T) { } func TestRespondWithJSON(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 diff --git a/caddytest/integration/intercept_test.go b/caddytest/integration/intercept_test.go index 81db6a7d6..ef33f1cd8 100644 --- a/caddytest/integration/intercept_test.go +++ b/caddytest/integration/intercept_test.go @@ -7,8 +7,8 @@ import ( ) func TestIntercept(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ skip_install_trust admin localhost:2999 http_port 9080 diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go index 4399902ea..39501fe5b 100644 --- a/caddytest/integration/leafcertloaders_test.go +++ b/caddytest/integration/leafcertloaders_test.go @@ -7,8 +7,8 @@ import ( ) func TestLeafCertLoaders(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" diff --git a/caddytest/integration/listener_test.go b/caddytest/integration/listener_test.go index 30642b1ae..b2e327b04 100644 --- a/caddytest/integration/listener_test.go +++ b/caddytest/integration/listener_test.go @@ -12,7 +12,7 @@ import ( "github.com/caddyserver/caddy/v2/caddytest" ) -func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.Tester { +func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.TestHarness { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("failed to listen: %s", err) @@ -28,8 +28,8 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy _ = srv.Close() _ = l.Close() }) - tester := caddytest.NewTester(t) - tester.InitServer(fmt.Sprintf(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(fmt.Sprintf(` { skip_install_trust admin localhost:2999 @@ -69,7 +69,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) { writer.WriteHeader(http.StatusNoContent) }) - resp, err := tester.Client.Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body)) + resp, err := tester.Client().Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body)) if err != nil { t.Fatalf("failed to post: %s", err) } @@ -87,7 +87,7 @@ func TestLargeHttpRequest(t *testing.T) { // We never read the body in any way, set an extra long header instead. req, _ := http.NewRequest("POST", "http://localhost:9443", nil) req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024)) - _, err := tester.Client.Do(req) + _, err := tester.Client().Do(req) if err == nil { t.Fatal("not supposed to succeed") } diff --git a/caddytest/integration/map_test.go b/caddytest/integration/map_test.go index eb3386564..c5e7d7d3d 100644 --- a/caddytest/integration/map_test.go +++ b/caddytest/integration/map_test.go @@ -9,8 +9,8 @@ import ( func TestMap(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ skip_install_trust admin localhost:2999 http_port 9080 @@ -39,8 +39,8 @@ func TestMap(t *testing.T) { func TestMapRespondWithDefault(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ skip_install_trust admin localhost:2999 http_port 9080 @@ -67,8 +67,8 @@ func TestMapRespondWithDefault(t *testing.T) { func TestMapAsJSON(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" diff --git a/caddytest/integration/reverseproxy_test.go b/caddytest/integration/reverseproxy_test.go index cbfe8433b..f165c2e5c 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -14,8 +14,8 @@ import ( ) func TestSRVReverseProxy(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -87,8 +87,8 @@ func TestDialWithPlaceholderUnix(t *testing.T) { }) runtime.Gosched() // Allow other goroutines to run - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -139,8 +139,8 @@ func TestDialWithPlaceholderUnix(t *testing.T) { } func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -233,8 +233,8 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) { } func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -327,8 +327,8 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { } func TestReverseProxyHealthCheck(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -364,7 +364,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) { if runtime.GOOS == "windows" { t.SkipNow() } - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) f, err := os.CreateTemp("", "*.sock") if err != nil { t.Errorf("failed to create TempFile: %s", err) @@ -395,7 +395,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) { }) runtime.Gosched() // Allow other goroutines to run - tester.InitServer(fmt.Sprintf(` + tester.LoadConfig(fmt.Sprintf(` { skip_install_trust admin localhost:2999 @@ -422,7 +422,7 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) { if runtime.GOOS == "windows" { t.SkipNow() } - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) f, err := os.CreateTemp("", "*.sock") if err != nil { t.Errorf("failed to create TempFile: %s", err) @@ -453,7 +453,7 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) { }) runtime.Gosched() // Allow other goroutines to run - tester.InitServer(fmt.Sprintf(` + tester.LoadConfig(fmt.Sprintf(` { skip_install_trust admin localhost:2999 diff --git a/caddytest/integration/sni_test.go b/caddytest/integration/sni_test.go index 188f93541..7830bf725 100644 --- a/caddytest/integration/sni_test.go +++ b/caddytest/integration/sni_test.go @@ -8,8 +8,8 @@ import ( func TestDefaultSNI(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ "admin": { "listen": "localhost:2999" }, @@ -107,8 +107,8 @@ func TestDefaultSNI(t *testing.T) { func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -211,8 +211,8 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { func TestDefaultSNIWithPortMappingOnly(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" diff --git a/caddytest/integration/stream_test.go b/caddytest/integration/stream_test.go index d82882d69..77c9bdfce 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -20,8 +20,8 @@ import ( // (see https://github.com/caddyserver/caddy/issues/3556 for use case) func TestH2ToH2CStream(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -204,8 +204,8 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { // (see https://github.com/caddyserver/caddy/issues/3606 for use case) func TestH2ToH1ChunkedResponse(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" diff --git a/caddytest/testing_harness.go b/caddytest/testing_harness.go new file mode 100644 index 000000000..581d2f7ed --- /dev/null +++ b/caddytest/testing_harness.go @@ -0,0 +1,223 @@ +package caddytest + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +// 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 +} + +var ( + matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`) + matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`) +) + +type TestHarness struct { + t testing.TB + + tester *Tester +} + +// StartHarness creates and starts a test harness environment which spans the lifetime a single caddy instance +// This is used for the integration tests +func StartHarness(t *testing.T) *TestHarness { + if testing.Short() { + t.SkipNow() + return nil + } + o := &TestHarness{t: t} + o.init() + return o +} + +func (tc *TestHarness) Client() *http.Client { + return tc.tester.Client +} + +func (tc *TestHarness) LoadConfig(rawConfig, configType string) { + rawConfig = prependCaddyFilePath(rawConfig) + err := tc.tester.LoadConfig(rawConfig, configType) + require.NoError(tc.t, err) +} + +func (tc *TestHarness) init() { + // start the server + tester, err := NewTester() + if err != nil { + tc.t.Errorf("Failed to create caddy tester: %s", err) + return + } + tc.tester = tester + err = tc.tester.LaunchCaddy() + if err != nil { + tc.t.Errorf("Failed to launch caddy tester: %s", err) + } + // cleanup + tc.t.Cleanup(func() { + func() { + if tc.t.Failed() { + res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + if err != nil { + tc.t.Log("unable to read the current config") + return + } + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + var out bytes.Buffer + _ = json.Indent(&out, body, "", " ") + tc.t.Logf("----------- failed with config -----------\n%s", out.String()) + } + }() + // shutdown server after extracing the config + err = tc.tester.CleanupCaddy() + if err != nil { + tc.t.Errorf("failed to clean up caddy instance: %s", err) + } + }) +} + +// AssertRedirect makes a request and asserts the redirection happens +func (tc *TestHarness) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { + redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // using the existing client, we override the check redirect policy for this test + old := tc.tester.Client.CheckRedirect + tc.tester.Client.CheckRedirect = redirectPolicyFunc + defer func() { tc.tester.Client.CheckRedirect = old }() + + resp, err := tc.tester.Client.Get(requestURI) + if err != nil { + tc.t.Errorf("failed to call server %s", err) + return nil + } + + if expectedStatusCode != resp.StatusCode { + tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode) + } + + loc, err := resp.Location() + if err != nil { + tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err) + } + if loc == nil && expectedToLocation != "" { + tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI) + } + if loc != nil { + if expectedToLocation != loc.String() { + tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String()) + } + } + + return resp +} + +// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions +func (tc *TestHarness) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response { + resp, err := tc.tester.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.URL.RequestURI(), expectedStatusCode, resp.StatusCode) + } + + return resp +} + +// AssertResponse request a URI and assert the status code and the body contains a string +func (tc *TestHarness) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) { + resp := tc.AssertResponseCode(req, expectedStatusCode) + + defer resp.Body.Close() + bytes, err := io.ReadAll(resp.Body) + if err != nil { + tc.t.Fatalf("unable to read the response body %s", err) + } + + body := string(bytes) + + if 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 *TestHarness) 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 *TestHarness) 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 *TestHarness) 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 *TestHarness) 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 *TestHarness) 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) +}