diff --git a/caddy.go b/caddy.go index 7dd989c9e1e..e7afc13b86e 100644 --- a/caddy.go +++ b/caddy.go @@ -20,6 +20,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "flag" "fmt" "io" "io/fs" @@ -778,7 +779,10 @@ func exitProcess(ctx context.Context, logger *zap.Logger) { } else { logger.Error("unclean shutdown") } - os.Exit(exitCode) + // check if we are in test environment, and dont call exit if we are + if flag.Lookup("test.v") == nil && !strings.Contains(os.Args[0], ".test") { + os.Exit(exitCode) + } }() if remoteAdminServer != nil { diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 05aa1e3f52f..c67b9cbb898 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -1,40 +1,29 @@ package caddytest import ( - "bytes" "context" "crypto/tls" "encoding/json" - "errors" "fmt" "io" - "io/fs" "log" "net" "net/http" "net/http/cookiejar" "os" - "path" - "reflect" - "regexp" - "runtime" + "strconv" "strings" - "testing" + "sync/atomic" "time" - "github.com/aryann/difflib" - caddycmd "github.com/caddyserver/caddy/v2/cmd" - "github.com/caddyserver/caddy/v2/caddyconfig" // plug in Caddy modules here _ "github.com/caddyserver/caddy/v2/modules/standard" ) // 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 Certificates []string // TestRequestTimeout is the time to wait for a http request to @@ -45,29 +34,31 @@ type Defaults struct { // Default testing values var Default = Defaults{ - AdminPort: 2999, // different from what a real server also running on a developer's machine might be Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"}, TestRequestTimeout: 5 * time.Second, 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 + + adminPort int + + portOne int + portTwo int + + started atomic.Bool + configLoaded bool + configFileName string + envFileName 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,8 +68,7 @@ func NewTester(t testing.TB) *Tester { Timeout: Default.TestRequestTimeout, }, configLoaded: false, - t: t, - } + }, nil } type configLoadError struct { @@ -92,53 +82,73 @@ func timeElapsed(start time.Time, name string) { log.Printf("%s took %s", name, elapsed) } -// InitServer this will configure the server with a configurion of a specific -// type. The configType must be either "json" or the adapter type. -func (tc *Tester) InitServer(rawConfig string, configType string) { - if err := tc.initServer(rawConfig, configType); err != nil { - tc.t.Logf("failed to load config: %s", err) - tc.t.Fail() +// launch caddy will start the server +func (tc *Tester) LaunchCaddy() error { + if !tc.started.CompareAndSwap(false, true) { + return fmt.Errorf("already launched caddy with this tester") } - if err := tc.ensureConfigRunning(rawConfig, configType); err != nil { - tc.t.Logf("failed ensuring config is running: %s", err) - tc.t.Fail() + if err := tc.startServer(); err != nil { + return fmt.Errorf("failed to start server: %w", err) } + return nil } -// InitServer this will configure the server with a configurion of a specific -// type. The configType must be either "json" or the adapter type. -func (tc *Tester) initServer(rawConfig string, configType string) error { - if testing.Short() { - tc.t.SkipNow() - return nil - } - - err := validateTestPrerequisites(tc.t) +func (tc *Tester) CleanupCaddy() error { + // now shutdown the server, since the test is done. + defer func() { + // try to remove pthe tmp config file we created + if tc.configFileName != "" { + os.Remove(tc.configFileName) + } + if tc.envFileName != "" { + os.Remove(tc.envFileName) + } + }() + resp, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", tc.adminPort), "", nil) 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()) + return fmt.Errorf("couldn't stop caddytest server: %w", err) + } + resp.Body.Close() + for retries := 0; retries < 10; retries++ { + if tc.isCaddyAdminRunning() != nil { + return nil } - }) + time.Sleep(100 * time.Millisecond) + } + + return fmt.Errorf("timed out waiting for caddytest server to stop") +} - rawConfig = prependCaddyFilePath(rawConfig) +func (tc *Tester) AdminPort() int { + return tc.adminPort +} + +func (tc *Tester) PortOne() int { + return tc.portOne +} + +func (tc *Tester) PortTwo() int { + return tc.portTwo +} + +func (tc *Tester) ReplaceTestingPlaceholders(x string) string { + x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_BIND}", fmt.Sprintf("localhost:%d", tc.adminPort)) + x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_PORT}", fmt.Sprintf("%d", tc.adminPort)) + x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_ONE}", fmt.Sprintf("%d", tc.portOne)) + x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_TWO}", fmt.Sprintf("%d", tc.portTwo)) + return x +} + +// LoadConfig loads the config to the tester server and also ensures that the config was loaded +// it should not be run +func (tc *Tester) LoadConfig(rawConfig string, configType string) error { + if tc.adminPort == 0 { + return fmt.Errorf("load config called where startServer didnt succeed") + } + rawConfig = tc.ReplaceTestingPlaceholders(rawConfig) + // replace special testing placeholders so we can have our admin api be on a random port // 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 @@ -148,16 +158,14 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { return err } rawConfig = string(c) - tc.t.Logf("After: %s", rawConfig) } client := &http.Client{ Timeout: Default.LoadRequestTimeout, } 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", tc.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" { @@ -168,16 +176,14 @@ func (tc *Tester) initServer(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 { @@ -185,133 +191,115 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { } tc.configLoaded = true + + // if the config is not loaded at this point, it is a bug in caddy's config.Load + // the contract for config.Load states that the config must be loaded before it returns, and that it will + // error if the config fails to apply return nil } -func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error { - expectedBytes := []byte(prependCaddyFilePath(rawConfig)) - if configType != "json" { - adapter := caddyconfig.GetAdapter(configType) - if adapter == nil { - return fmt.Errorf("adapter of config type is missing: %s", configType) - } - expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil) +func (tc *Tester) GetCurrentConfig(receiver any) error { + client := &http.Client{ + Timeout: Default.LoadRequestTimeout, } - var expected any - err := json.Unmarshal(expectedBytes, &expected) + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort)) if err != nil { return err } - - client := &http.Client{ - Timeout: Default.LoadRequestTimeout, + defer resp.Body.Close() + actualBytes, err := io.ReadAll(resp.Body) + if err != nil { + return err } - - fetchConfig := func(client *http.Client) any { - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) - if err != nil { - return nil - } - defer resp.Body.Close() - actualBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil - } - var actual any - err = json.Unmarshal(actualBytes, &actual) - if err != nil { - return nil - } - return actual + err = json.Unmarshal(actualBytes, receiver) + if err != nil { + return err } + return nil +} - for retries := 10; retries > 0; retries-- { - if reflect.DeepEqual(expected, fetchConfig(client)) { - return nil - } - time.Sleep(1 * time.Second) +func getFreePort() (int, error) { + lr, err := net.Listen("tcp", "localhost:0") + if err != nil { + return 0, err + } + port := strings.Split(lr.Addr().String(), ":") + if len(port) < 2 { + return 0, fmt.Errorf("no port available") + } + i, err := strconv.Atoi(port[1]) + if err != nil { + return 0, err + } + err = lr.Close() + if err != nil { + return 0, fmt.Errorf("failed to close listener: %w", err) } - tc.t.Errorf("POSTed configuration isn't active") - return errors.New("EnsureConfigRunning: POSTed configuration isn't active") + return i, nil } -const initConfig = `{ - admin localhost:2999 -} -` - -// validateTestPrerequisites ensures the certificates are available in the -// designated path and Caddy sub-process is running. -func validateTestPrerequisites(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 tc.isCaddyAdminRunning() == nil { + return fmt.Errorf("caddy test admin port still in use") } - - if isCaddyAdminRunning() != nil { - // setup the init config file, and set the cleanup afterwards + a, err := getFreePort() + if err != nil { + return fmt.Errorf("could not find a open port to listen on: %w", err) + } + tc.adminPort = a + tc.portOne, err = getFreePort() + if err != nil { + return fmt.Errorf("could not find a open portOne: %w", err) + } + tc.portTwo, err = getFreePort() + if err != nil { + return fmt.Errorf("could not find a open portOne: %w", err) + } + // 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()) - }) + tc.configFileName = f.Name() + + initConfig := fmt.Sprintf(`{ + admin localhost:%d +}`, a) 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 to start serving the initial config - for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { - time.Sleep(1 * time.Second) - } + // start inprocess caddy server + go func() { + _ = caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile") + }() + // wait for caddy admin api to start. it should happen quickly. + for retries := 10; retries > 0 && tc.isCaddyAdminRunning() != nil; retries-- { + time.Sleep(100 * time.Millisecond) } // one more time to return the error - return isCaddyAdminRunning() + return tc.isCaddyAdminRunning() } -func isCaddyAdminRunning() error { +func (tc *Tester) isCaddyAdminRunning() error { // assert that caddy is running client := &http.Client{ Timeout: Default.LoadRequestTimeout, } - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort)) if err != nil { - return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort) + return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.adminPort) } resp.Body.Close() 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 -} - // CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally func CreateTestingTransport() *http.Transport { dialer := net.Dialer{ @@ -338,231 +326,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) - - err := tc.initServer(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 00000000000..23054424d87 --- /dev/null +++ b/caddytest/caddytest_assert.go @@ -0,0 +1,116 @@ +package caddytest + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/aryann/difflib" + "github.com/stretchr/testify/require" + + "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, err := NewTester() + require.NoError(t, err) + err = tc.LaunchCaddy() + require.NoError(t, err) + + err = tc.LoadConfig(rawConfig, configType) + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) + } + _ = tc.CleanupCaddy() +} + +// 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 937537faa7c..d788a810b5e 100644 --- a/caddytest/caddytest_test.go +++ b/caddytest/caddytest_test.go @@ -1,21 +1,22 @@ package caddytest import ( + "fmt" "net/http" "strings" "testing" ) func TestReplaceCertificatePaths(t *testing.T) { - rawConfig := `a.caddy.localhost:9443 { + 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) @@ -34,8 +35,8 @@ func TestReplaceCertificatePaths(t *testing.T) { } func TestLoadUnorderedJSON(t *testing.T) { - tester := NewTester(t) - tester.InitServer(` + harness := StartHarness(t) + harness.LoadConfig(` { "logging": { "logs": { @@ -68,7 +69,7 @@ func TestLoadUnorderedJSON(t *testing.T) { } }, "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "pki": { @@ -79,13 +80,13 @@ func TestLoadUnorderedJSON(t *testing.T) { } }, "http": { - "http_port": 9080, - "https_port": 9443, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, "servers": { "s_server": { "listen": [ - ":9443", - ":9080" + ":{$TESTING_CADDY_PORT_ONE}", + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -120,10 +121,10 @@ func TestLoadUnorderedJSON(t *testing.T) { } } `, "json") - req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), nil) if err != nil { t.Fail() return } - tester.AssertResponseCode(req, 200) + harness.AssertResponseCode(req, 200) } diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index ceacd1db0fa..63cd1f81d3c 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -24,19 +24,13 @@ const acmeChallengePort = 9081 // Test the basic functionality of Caddy's ACME server func TestACMEServerWithDefaults(t *testing.T) { ctx := context.Background() - logger, err := zap.NewDevelopment() - if err != nil { - t.Error(err) - return - } - - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} local_certs } acme.localhost { @@ -44,10 +38,11 @@ func TestACMEServerWithDefaults(t *testing.T) { } `, "caddyfile") + logger := caddy.Log().Named("acmeserver") client := acmez.Client{ Client: &acme.Client{ - Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()), + HTTPClient: harness.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ @@ -97,13 +92,13 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) { ctx := context.Background() logger := caddy.Log().Named("acmez") - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} local_certs } acme.localhost { @@ -115,8 +110,8 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) { client := acmez.Client{ Client: &acme.Client{ - Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()), + HTTPClient: harness.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index 22b716f84db..4c7946ac6fd 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -5,50 +5,51 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "fmt" "strings" "testing" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddytest" "github.com/mholt/acmez/v2" "github.com/mholt/acmez/v2/acme" - "go.uber.org/zap" ) func TestACMEServerDirectory(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust local_certs - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} pki { ca local { name "Caddy Local Authority" } } } - acme.localhost:9443 { + acme.localhost:{$TESTING_CADDY_PORT_TWO} { acme_server } `, "caddyfile") - tester.AssertGetResponse( - "https://acme.localhost:9443/acme/local/directory", + harness.AssertGetResponse( + fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()), 200, - `{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"} -`) + fmt.Sprintf(`{"newNonce":"https://acme.localhost:%[1]d/acme/local/new-nonce","newAccount":"https://acme.localhost:%[1]d/acme/local/new-account","newOrder":"https://acme.localhost:%[1]d/acme/local/new-order","revokeCert":"https://acme.localhost:%[1]d/acme/local/revoke-cert","keyChange":"https://acme.localhost:%[1]d/acme/local/key-change"} +`, harness.Tester().PortTwo())) } func TestACMEServerAllowPolicy(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust local_certs - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} pki { ca local { name "Caddy Local Authority" @@ -66,16 +67,12 @@ func TestACMEServerAllowPolicy(t *testing.T) { `, "caddyfile") ctx := context.Background() - logger, err := zap.NewDevelopment() - if err != nil { - t.Error(err) - return - } + logger := caddy.Log().Named("acmez") client := acmez.Client{ Client: &acme.Client{ - Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()), + HTTPClient: harness.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ @@ -131,14 +128,14 @@ func TestACMEServerAllowPolicy(t *testing.T) { } func TestACMEServerDenyPolicy(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust local_certs - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} pki { ca local { name "Caddy Local Authority" @@ -155,16 +152,12 @@ func TestACMEServerDenyPolicy(t *testing.T) { `, "caddyfile") ctx := context.Background() - logger, err := zap.NewDevelopment() - if err != nil { - t.Error(err) - return - } + logger := caddy.Log().Named("acmez") client := acmez.Client{ Client: &acme.Client{ - Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()), + HTTPClient: harness.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ @@ -197,7 +190,7 @@ func TestACMEServerDenyPolicy(t *testing.T) { _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"}) if err == nil { t.Errorf("obtaining certificate for 'deny.localhost' domain") - } else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") { + } else if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") { t.Logf("unexpected error: %v", err) } } diff --git a/caddytest/integration/autohttps_test.go b/caddytest/integration/autohttps_test.go index 1dbdbcee209..ec296400394 100644 --- a/caddytest/integration/autohttps_test.go +++ b/caddytest/integration/autohttps_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "net/http" "testing" @@ -8,69 +9,69 @@ import ( ) func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_CADDY_ADMIN_BIND} skip_install_trust - http_port 9080 - https_port 9443 + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} } localhost respond "Yahaha! You found me!" `, "caddyfile") - tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect) + harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost/", http.StatusPermanentRedirect) } func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} } - localhost:9443 + localhost:{$TESTING_CADDY_PORT_TWO} respond "Yahaha! You found me!" `, "caddyfile") - tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect) + harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost/", http.StatusPermanentRedirect) } func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} } localhost:1234 respond "Yahaha! You found me!" `, "caddyfile") - tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect) + harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost:1234/", http.StatusPermanentRedirect) } func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "http": { - "http_port": 9080, - "https_port": 9443, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, "servers": { "ingress_server": { "listen": [ - ":9080", - ":9443" + ":{$TESTING_CADDY_PORT_ONE}", + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -94,52 +95,52 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) { } } `, "json") - tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect) + harness.AssertRedirect(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), "https://localhost/", http.StatusPermanentRedirect) } func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} local_certs } - http://:9080 { + http://:{$TESTING_CADDY_PORT_ONE} { respond "Foo" } - http://baz.localhost:9080 { + http://baz.localhost:{$TESTING_CADDY_PORT_ONE} { respond "Baz" } bar.localhost { respond "Bar" } `, "caddyfile") - tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect) - tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo") - tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz") + harness.AssertRedirect(fmt.Sprintf("http://bar.localhost:%d/", harness.Tester().PortOne()), "https://bar.localhost/", http.StatusPermanentRedirect) + harness.AssertGetResponse(fmt.Sprintf("http://foo.localhost:%d/", harness.Tester().PortOne()), 200, "Foo") + harness.AssertGetResponse(fmt.Sprintf("http://baz.localhost:%d/", harness.Tester().PortOne()), 200, "Baz") } func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} local_certs } - http://:9080 { + http://:{$TESTING_CADDY_PORT_ONE} { respond "Foo" } bar.localhost { respond "Bar" } `, "caddyfile") - tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect) - tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo") - tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo") + harness.AssertRedirect(fmt.Sprintf("http://bar.localhost:%d/", harness.Tester().PortOne()), "https://bar.localhost/", http.StatusPermanentRedirect) + harness.AssertGetResponse(fmt.Sprintf("http://foo.localhost:%d/", harness.Tester().PortOne()), 200, "Foo") + harness.AssertGetResponse(fmt.Sprintf("http://baz.localhost:%d/", harness.Tester().PortOne()), 200, "Foo") } diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go index 11ffc08aeb1..dab5a7d1db1 100644 --- a/caddytest/integration/caddyfile_test.go +++ b/caddytest/integration/caddyfile_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "net/http" "net/url" "testing" @@ -10,62 +11,63 @@ import ( func TestRespond(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - - localhost:9080 { + + localhost:{$TESTING_CADDY_PORT_ONE} { respond /version 200 { body "hello from localhost" - } + } } `, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost") + harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), 200, "hello from localhost") } func TestRedirect(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - - localhost:9080 { - - redir / http://localhost:9080/hello 301 - + + localhost:{$TESTING_CADDY_PORT_ONE} { + + redir / http://localhost:{$TESTING_CADDY_PORT_ONE}/hello 301 + respond /hello 200 { body "hello from localhost" - } + } } `, "caddyfile") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) // act and assert - tester.AssertRedirect("http://localhost:9080/", "http://localhost:9080/hello", 301) + harness.AssertRedirect(target, target+"hello", 301) // follow redirect - tester.AssertGetResponse("http://localhost:9080/", 200, "hello from localhost") + harness.AssertGetResponse(target, 200, "hello from localhost") } func TestDuplicateHosts(t *testing.T) { // act and assert caddytest.AssertLoadError(t, ` - localhost:9080 { + localhost:{$TESTING_CADDY_PORT_ONE} { } - - localhost:9080 { + + localhost:{$TESTING_CADDY_PORT_ONE} { } `, "caddyfile", @@ -80,18 +82,18 @@ func TestReadCookie(t *testing.T) { } // arrange - tester := caddytest.NewTester(t) - tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie}) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.Client().Jar.SetCookies(localhost, []*http.Cookie{&cookie}) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - - localhost:9080 { + + localhost:{$TESTING_CADDY_PORT_ONE} { templates { root testdata } @@ -102,21 +104,22 @@ func TestReadCookie(t *testing.T) { `, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/cookie.html", 200, "

Cookie.ClientName caddytest

") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"cookie.html", 200, "

Cookie.ClientName caddytest

") } func TestReplIndex(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - localhost:9080 { + localhost:{$TESTING_CADDY_PORT_ONE} { templates { root testdata } @@ -128,7 +131,8 @@ func TestReplIndex(t *testing.T) { `, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/", 200, "") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target, 200, "") } func TestInvalidPrefix(t *testing.T) { @@ -481,40 +485,42 @@ func TestValidPrefix(t *testing.T) { } func TestUriReplace(t *testing.T) { - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) - tester.InitServer(` + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$TESTING_CADDY_PORT_ONE} uri replace "\}" %7D uri replace "\{" %7B - + respond "{query}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D") } func TestUriOps(t *testing.T) { - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) - tester.InitServer(` + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$TESTING_CADDY_PORT_ONE} uri query +foo bar uri query -baz 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") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?foo=bar0&baz=buz&taz=nottest&changethis=val", 200, "changed=val&foo=bar0&foo=bar&key%3Dvalue=example&taz=test") } // Tests the `http.request.local.port` placeholder. @@ -523,204 +529,215 @@ 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) + harness := caddytest.StartHarness(t) - tester.InitServer(` + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$TESTING_CADDY_PORT_ONE} respond "{http.request.local.port}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/", 200, "9080") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target, 200, fmt.Sprintf("%d", harness.Tester().PortOne())) } func TestSetThenAddQueryParams(t *testing.T) { - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) - tester.InitServer(` + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$TESTING_CADDY_PORT_ONE} uri query foo bar uri query +foo baz - + respond "{query}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/endpoint", 200, "foo=bar&foo=baz") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint", 200, "foo=bar&foo=baz") } func TestSetThenDeleteParams(t *testing.T) { - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) - tester.InitServer(` + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$TESTING_CADDY_PORT_ONE} uri query bar foo{query.foo} uri query -foo - + respond "{query}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=foobar") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "bar=foobar") } func TestRenameAndOtherOps(t *testing.T) { - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) - tester.InitServer(` + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$TESTING_CADDY_PORT_ONE} 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") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "bar=taz&bar=baz") } func TestReplaceOps(t *testing.T) { - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) - tester.InitServer(` + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 - uri query foo bar baz + :{$TESTING_CADDY_PORT_ONE} + uri query foo bar baz respond "{query}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=baz") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "foo=baz") } func TestReplaceWithReplacementPlaceholder(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 - uri query foo bar {query.placeholder} + :{$TESTING_CADDY_PORT_ONE} + uri query foo bar {query.placeholder} respond "{query}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=baz&foo=bar", 200, "foo=baz&placeholder=baz") - + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?placeholder=baz&foo=bar", 200, "foo=baz&placeholder=baz") } func TestReplaceWithKeyPlaceholder(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 - uri query {query.placeholder} bar baz + :{$TESTING_CADDY_PORT_ONE} + uri query {query.placeholder} bar baz respond "{query}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=foo&foo=bar", 200, "foo=baz&placeholder=foo") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?placeholder=foo&foo=bar", 200, "foo=baz&placeholder=foo") } func TestPartialReplacement(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 - uri query foo ar az + :{$TESTING_CADDY_PORT_ONE} + uri query foo ar az respond "{query}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=baz") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "foo=baz") } func TestNonExistingSearch(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 - uri query foo var baz + :{$TESTING_CADDY_PORT_ONE} + uri query foo var baz respond "{query}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=bar") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?foo=bar", 200, "foo=bar") } func TestReplaceAllOps(t *testing.T) { - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) - tester.InitServer(` + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 - uri query * bar baz + :{$TESTING_CADDY_PORT_ONE} + uri query * bar baz respond "{query}"`, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar&baz=bar", 200, "baz=baz&foo=baz") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"endpoint?foo=bar&baz=bar", 200, "baz=baz&foo=baz") } func TestUriOpsBlock(t *testing.T) { - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) - tester.InitServer(` + harness.LoadConfig(` { - admin localhost:2999 - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$TESTING_CADDY_PORT_ONE} uri query { +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") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"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(`{ - admin localhost:2999 - http_port 9080 + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - localhost:9080 { + localhost:{$TESTING_CADDY_PORT_ONE} { root * /srv error /private* "Unauthorized" 410 error /hidden* "Not found" 404 - + handle_errors 404 410 { respond "404 or 410 error" } }`, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/private", 410, "404 or 410 error") - tester.AssertGetResponse("http://localhost:9080/hidden", 404, "404 or 410 error") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"private", 410, "404 or 410 error") + harness.AssertGetResponse(target+"hidden", 404, "404 or 410 error") } func TestHandleErrorRange(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ - admin localhost:2999 - http_port 9080 + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - localhost:9080 { + localhost:{$TESTING_CADDY_PORT_ONE} { root * /srv error /private* "Unauthorized" 410 error /hidden* "Not found" 404 @@ -730,17 +747,18 @@ func TestHandleErrorRange(t *testing.T) { } }`, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range") - tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"private", 410, "Error in the [400 .. 499] range") + harness.AssertGetResponse(target+"hidden", 404, "Error in the [400 .. 499] range") } func TestHandleErrorSort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ - admin localhost:2999 - http_port 9080 + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - localhost:9080 { + localhost:{$TESTING_CADDY_PORT_ONE} { root * /srv error /private* "Unauthorized" 410 error /hidden* "Not found" 404 @@ -754,17 +772,18 @@ func TestHandleErrorSort(t *testing.T) { } }`, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Fallback route: code outside the [400..499] range") - tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"internalerr", 500, "Fallback route: code outside the [400..499] range") + harness.AssertGetResponse(target+"hidden", 404, "Error in the [400 .. 499] range") } func TestHandleErrorRangeAndCodes(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ - admin localhost:2999 - http_port 9080 + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - localhost:9080 { + localhost:{$TESTING_CADDY_PORT_ONE} { root * /srv error /private* "Unauthorized" 410 error /threehundred* "Moved Permanently" 301 @@ -778,9 +797,10 @@ func TestHandleErrorRangeAndCodes(t *testing.T) { } }`, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Error code is equal to 500 or in the [300..399] range") - tester.AssertGetResponse("http://localhost:9080/threehundred", 301, "Error code is equal to 500 or in the [300..399] range") - tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"internalerr", 500, "Error code is equal to 500 or in the [300..399] range") + harness.AssertGetResponse(target+"threehundred", 301, "Error code is equal to 500 or in the [300..399] range") + harness.AssertGetResponse(target+"private", 410, "Error in the [400 .. 499] range") } func TestInvalidSiteAddressesAsDirectives(t *testing.T) { diff --git a/caddytest/integration/handler_test.go b/caddytest/integration/handler_test.go index afc700b02bd..500c0e448c0 100644 --- a/caddytest/integration/handler_test.go +++ b/caddytest/integration/handler_test.go @@ -2,6 +2,7 @@ package integration import ( "bytes" + "fmt" "net/http" "testing" @@ -9,36 +10,36 @@ import ( ) func TestBrowse(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - http://localhost:9080 { + http://localhost:{$TESTING_CADDY_PORT_ONE} { file_server browse } `, "caddyfile") - req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), nil) if err != nil { t.Fail() return } - tester.AssertResponseCode(req, 200) + harness.AssertResponseCode(req, 200) } func TestRespondWithJSON(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } localhost { @@ -46,7 +47,7 @@ func TestRespondWithJSON(t *testing.T) { } `, "caddyfile") - res, _ := tester.AssertPostResponseBody("https://localhost:9443/", + res, _ := harness.AssertPostResponseBody(fmt.Sprintf("https://localhost:%d/", harness.Tester().PortTwo()), nil, bytes.NewBufferString(`{ "greeting": "Hello, world!" diff --git a/caddytest/integration/intercept_test.go b/caddytest/integration/intercept_test.go index 81db6a7d639..f52ac5ffac5 100644 --- a/caddytest/integration/intercept_test.go +++ b/caddytest/integration/intercept_test.go @@ -1,22 +1,23 @@ package integration import ( + "fmt" "testing" "github.com/caddyserver/caddy/v2/caddytest" ) func TestIntercept(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - - localhost:9080 { + + localhost:{$TESTING_CADDY_PORT_ONE} { respond /intercept "I'm a teapot" 408 respond /no-intercept "I'm not a teapot" @@ -25,10 +26,10 @@ func TestIntercept(t *testing.T) { handle_response @teapot { respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503 } - } + } } `, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") - tester.AssertGetResponse("http://localhost:9080/no-intercept", 200, "I'm not a teapot") + harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/intercept", harness.Tester().PortOne()), 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") + harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/no-intercept", harness.Tester().PortOne()), 200, "I'm not a teapot") } diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go index 4399902eaee..502674edbc2 100644 --- a/caddytest/integration/leafcertloaders_test.go +++ b/caddytest/integration/leafcertloaders_test.go @@ -7,21 +7,21 @@ import ( ) func TestLeafCertLoaders(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "http": { - "http_port": 9080, - "https_port": 9443, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, "grace_period": 1, "servers": { "srv0": { "listen": [ - ":9443" + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { diff --git a/caddytest/integration/listener_test.go b/caddytest/integration/listener_test.go index 30642b1aed9..f22f5133d8f 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,15 +28,15 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy _ = srv.Close() _ = l.Close() }) - tester := caddytest.NewTester(t) - tester.InitServer(fmt.Sprintf(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(fmt.Sprintf(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} local_certs - servers :9443 { + servers :{$TESTING_CADDY_PORT_TWO} { listener_wrappers { http_redirect tls @@ -47,7 +47,7 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy reverse_proxy %s } `, l.Addr().String()), "caddyfile") - return tester + return harness } func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) { @@ -56,7 +56,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) { body := make([]byte, uploadSize) rand.New(rand.NewSource(0)).Read(body) - tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) { + harness := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) { buf := new(bytes.Buffer) _, err := buf.ReadFrom(request.Body) if err != nil { @@ -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 := harness.Client().Post(fmt.Sprintf("https://localhost:%d", harness.Tester().PortTwo()), "application/octet-stream", bytes.NewReader(body)) if err != nil { t.Fatalf("failed to post: %s", err) } @@ -80,14 +80,14 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) { } func TestLargeHttpRequest(t *testing.T) { - tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) { + harness := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) { t.Fatal("not supposed to handle a request") }) // We never read the body in any way, set an extra long header instead. - req, _ := http.NewRequest("POST", "http://localhost:9443", nil) + req, _ := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d", harness.Tester().PortTwo()), nil) req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024)) - _, err := tester.Client.Do(req) + _, err := harness.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 eb338656469..88ceb9e3638 100644 --- a/caddytest/integration/map_test.go +++ b/caddytest/integration/map_test.go @@ -2,6 +2,7 @@ package integration import ( "bytes" + "fmt" "testing" "github.com/caddyserver/caddy/v2/caddytest" @@ -9,16 +10,16 @@ import ( func TestMap(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(`{ + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - localhost:9080 { + localhost:{$TESTING_CADDY_PORT_ONE} { map {http.request.method} {dest-1} {dest-2} { default unknown1 unknown2 @@ -28,50 +29,50 @@ func TestMap(t *testing.T) { respond /version 200 { body "hello from localhost {dest-1} {dest-2}" - } + } } `, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost GET-called unknown2") - tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called foobar") + harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), 200, "hello from localhost GET-called unknown2") + harness.AssertPostResponseBody(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called foobar") } func TestMapRespondWithDefault(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(`{ + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} } - - localhost:9080 { - + + localhost:{$TESTING_CADDY_PORT_ONE} { + map {http.request.method} {dest-name} { default unknown GET get-called } - + respond /version 200 { body "hello from localhost {dest-name}" - } + } } `, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called") - tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown") + harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), 200, "hello from localhost get-called") + harness.AssertPostResponseBody(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown") } func TestMapAsJSON(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "pki": { @@ -82,12 +83,12 @@ func TestMapAsJSON(t *testing.T) { } }, "http": { - "http_port": 9080, - "https_port": 9443, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, "servers": { "srv0": { "listen": [ - ":9080" + ":{$TESTING_CADDY_PORT_ONE}" ], "routes": [ { @@ -145,7 +146,7 @@ func TestMapAsJSON(t *testing.T) { } } }`, "json") - - tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called") - tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called") + target := fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()) + harness.AssertGetResponse(target, 200, "hello from localhost get-called") + harness.AssertPostResponseBody(target, []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called") } diff --git a/caddytest/integration/reverseproxy_test.go b/caddytest/integration/reverseproxy_test.go index cbfe8433bc9..f141f6130f0 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -14,11 +14,11 @@ import ( ) func TestSRVReverseProxy(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "pki": { @@ -87,11 +87,11 @@ func TestDialWithPlaceholderUnix(t *testing.T) { }) runtime.Gosched() // Allow other goroutines to run - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "pki": { @@ -135,15 +135,15 @@ func TestDialWithPlaceholderUnix(t *testing.T) { return } req.Header.Set("X-Caddy-Upstream-Dial", socketName) - tester.AssertResponse(req, 200, "Hello, World!") + harness.AssertResponse(req, 200, "Hello, World!") } func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "pki": { @@ -186,7 +186,7 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) { }, "srv1": { "listen": [ - ":9080" + ":{$TESTING_CADDY_PORT_ONE}" ], "routes": [ { @@ -199,7 +199,7 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) { ], "handle": [ { - + "handler": "reverse_proxy", "upstreams": [ { @@ -223,21 +223,21 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) { } `, "json") - req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d", harness.Tester().PortOne()), nil) if err != nil { t.Fail() return } req.Header.Set("X-Caddy-Upstream-Dial", "localhost:18080") - tester.AssertResponse(req, 200, "Hello, World!") + harness.AssertResponse(req, 200, "Hello, World!") } func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "pki": { @@ -280,7 +280,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { }, "srv1": { "listen": [ - ":9080" + ":{$TESTING_CADDY_PORT_ONE}" ], "routes": [ { @@ -293,7 +293,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { ], "handle": [ { - + "handler": "reverse_proxy", "upstreams": [ { @@ -317,23 +317,23 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { } `, "json") - req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d", harness.Tester().PortOne()), nil) if err != nil { t.Fail() return } req.Header.Set("X-Caddy-Upstream-Dial", "localhost") - tester.AssertResponse(req, 200, "Hello, World!") + harness.AssertResponse(req, 200, "Hello, World!") } func TestReverseProxyHealthCheck(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } http://localhost:2020 { @@ -342,10 +342,10 @@ func TestReverseProxyHealthCheck(t *testing.T) { http://localhost:2021 { respond "ok" } - http://localhost:9080 { + http://localhost:{$TESTING_CADDY_PORT_ONE} { reverse_proxy { to localhost:2020 - + health_uri /health health_port 2021 health_interval 10ms @@ -357,14 +357,15 @@ func TestReverseProxyHealthCheck(t *testing.T) { `, "caddyfile") time.Sleep(100 * time.Millisecond) // TODO: for some reason this test seems particularly flaky, getting 503 when it should be 200, unless we wait - tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target, 200, "Hello, World!") } func TestReverseProxyHealthCheckUnixSocket(t *testing.T) { if runtime.GOOS == "windows" { t.SkipNow() } - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) f, err := os.CreateTemp("", "*.sock") if err != nil { t.Errorf("failed to create TempFile: %s", err) @@ -395,18 +396,18 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) { }) runtime.Gosched() // Allow other goroutines to run - tester.InitServer(fmt.Sprintf(` + harness.LoadConfig(fmt.Sprintf(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - http://localhost:9080 { + http://localhost:{$TESTING_CADDY_PORT_ONE} { reverse_proxy { to unix/%s - + health_uri /health health_port 2021 health_interval 2s @@ -415,14 +416,15 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) { } `, socketName), "caddyfile") - tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target, 200, "Hello, World!") } func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) { if runtime.GOOS == "windows" { t.SkipNow() } - tester := caddytest.NewTester(t) + harness := caddytest.StartHarness(t) f, err := os.CreateTemp("", "*.sock") if err != nil { t.Errorf("failed to create TempFile: %s", err) @@ -453,18 +455,18 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) { }) runtime.Gosched() // Allow other goroutines to run - tester.InitServer(fmt.Sprintf(` + harness.LoadConfig(fmt.Sprintf(` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - http://localhost:9080 { + http://localhost:{$TESTING_CADDY_PORT_ONE} { reverse_proxy { to unix/%s - + health_uri /health health_interval 2s health_timeout 5s @@ -472,5 +474,5 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) { } `, socketName), "caddyfile") - tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") + harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), 200, "Hello, World!") } diff --git a/caddytest/integration/sni_test.go b/caddytest/integration/sni_test.go index 188f9354135..8a5cfb73628 100644 --- a/caddytest/integration/sni_test.go +++ b/caddytest/integration/sni_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "testing" "github.com/caddyserver/caddy/v2/caddytest" @@ -8,20 +9,20 @@ import ( func TestDefaultSNI(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(`{ + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "http": { - "http_port": 9080, - "https_port": 9443, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, "grace_period": 1, "servers": { "srv0": { "listen": [ - ":9443" + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -102,26 +103,27 @@ func TestDefaultSNI(t *testing.T) { // act and assert // makes a request with no sni - tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost") + target := fmt.Sprintf("https://127.0.0.1:%d/", harness.Tester().PortTwo()) + harness.AssertGetResponse(target+"version", 200, "hello from a.caddy.localhost") } func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "http": { - "http_port": 9080, - "https_port": 9443, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, "grace_period": 1, "servers": { "srv0": { "listen": [ - ":9443" + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -206,26 +208,27 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { // act and assert // makes a request with no sni - tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a") + target := fmt.Sprintf("https://127.0.0.1:%d/", harness.Tester().PortTwo()) + harness.AssertGetResponse(target+"version", 200, "hello from a") } func TestDefaultSNIWithPortMappingOnly(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "http": { - "http_port": 9080, - "https_port": 9443, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, "grace_period": 1, "servers": { "srv0": { "listen": [ - ":9443" + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -282,7 +285,8 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) { // act and assert // makes a request with no sni - tester.AssertGetResponse("https://127.0.0.1:9443/version", 200, "hello from a.caddy.localhost") + target := fmt.Sprintf("https://127.0.0.1:%d/", harness.Tester().PortTwo()) + harness.AssertGetResponse(target+"version", 200, "hello from a.caddy.localhost") } func TestHttpOnlyOnDomainWithSNI(t *testing.T) { diff --git a/caddytest/integration/stream_test.go b/caddytest/integration/stream_test.go index d2f2fd79b95..e23c4f3d8ec 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -20,21 +20,21 @@ import ( // (see https://github.com/caddyserver/caddy/issues/3556 for use case) func TestH2ToH2CStream(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "http": { - "http_port": 9080, - "https_port": 9443, - "grace_period": 1, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, + "grace_period": 1, "servers": { "srv0": { "listen": [ - ":9443" + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -102,7 +102,7 @@ func TestH2ToH2CStream(t *testing.T) { expectedBody := "some data to be echoed" // start the server - server := testH2ToH2CStreamServeH2C(t) + server := testH2ToH2CStreamServeH2C(harness, t) go server.ListenAndServe() defer func() { ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) @@ -116,7 +116,7 @@ func TestH2ToH2CStream(t *testing.T) { Body: io.NopCloser(r), URL: &url.URL{ Scheme: "https", - Host: "127.0.0.1:9443", + Host: fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()), Path: "/tov2ray", }, Proto: "HTTP/2", @@ -127,7 +127,7 @@ func TestH2ToH2CStream(t *testing.T) { // Disable any compression method from server. req.Header.Set("Accept-Encoding", "identity") - resp := tester.AssertResponseCode(req, http.StatusOK) + resp := harness.AssertResponseCode(req, http.StatusOK) if resp.StatusCode != http.StatusOK { return } @@ -149,7 +149,7 @@ func TestH2ToH2CStream(t *testing.T) { } } -func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { +func testH2ToH2CStreamServeH2C(harness *caddytest.TestHarness, t *testing.T) *http.Server { h2s := &http2.Server{} handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rstring, err := httputil.DumpRequest(r, false) @@ -163,7 +163,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { return } - if r.Host != "127.0.0.1:9443" { + if r.Host != fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()) { t.Errorf("r.Host doesn't match, %v!", r.Host) w.WriteHeader(http.StatusNotFound) return @@ -204,28 +204,21 @@ 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(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, - "logging": { - "logs": { - "default": { - "level": "DEBUG" - } - } - }, "apps": { "http": { - "http_port": 9080, - "https_port": 9443, - "grace_period": 1, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, + "grace_period": 1, "servers": { "srv0": { "listen": [ - ":9443" + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -312,7 +305,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { } // start the server - server := testH2ToH1ChunkedResponseServeH1(t) + server := testH2ToH1ChunkedResponseServeH1(harness, t) go server.ListenAndServe() defer func() { ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) @@ -326,7 +319,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { Body: io.NopCloser(r), URL: &url.URL{ Scheme: "https", - Host: "127.0.0.1:9443", + Host: fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()), Path: "/tov2ray", }, Proto: "HTTP/2", @@ -340,7 +333,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { fmt.Fprint(w, expectedBody) w.Close() }() - resp := tester.AssertResponseCode(req, http.StatusOK) + resp := harness.AssertResponseCode(req, http.StatusOK) if resp.StatusCode != http.StatusOK { return } @@ -358,9 +351,9 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { } } -func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server { +func testH2ToH1ChunkedResponseServeH1(harness *caddytest.TestHarness, t *testing.T) *http.Server { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Host != "127.0.0.1:9443" { + if r.Host != fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()) { t.Errorf("r.Host doesn't match, %v!", r.Host) w.WriteHeader(http.StatusNotFound) return diff --git a/caddytest/testing_harness.go b/caddytest/testing_harness.go new file mode 100644 index 00000000000..c9d7a70f462 --- /dev/null +++ b/caddytest/testing_harness.go @@ -0,0 +1,241 @@ +package caddytest + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "path" + "regexp" + "runtime" + "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 +} + +func getIntegrationDir() string { + _, filename, _, ok := runtime.Caller(1) + if !ok { + panic("unable to determine the current file path") + } + + return path.Dir(filename) +} + +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) Tester() *Tester { + return tc.tester +} + +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 server: %s", err) + tc.t.FailNow() + return + } + // cleanup + tc.t.Cleanup(func() { + func() { + if tc.t.Failed() { + res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.tester.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) + tc.t.FailNow() + } + }) +} + +// 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) +} diff --git a/cmd/cobra.go b/cmd/cobra.go index 1a2509206a8..5323d2ec57e 100644 --- a/cmd/cobra.go +++ b/cmd/cobra.go @@ -8,9 +8,10 @@ import ( "github.com/caddyserver/caddy/v2" ) -var rootCmd = &cobra.Command{ - Use: "caddy", - Long: `Caddy is an extensible server platform written in Go. +var defaultFactory = NewRootCommandFactory(func() *cobra.Command { + return &cobra.Command{ + Use: "caddy", + Long: `Caddy is an extensible server platform written in Go. At its core, Caddy merely manages configuration. Modules are plugged in statically at compile-time to provide useful functionality. Caddy's @@ -91,23 +92,26 @@ package installers: https://caddyserver.com/docs/install Instructions for running Caddy in production are also available: https://caddyserver.com/docs/running `, - Example: ` $ caddy run + Example: ` $ caddy run $ caddy run --config caddy.json $ caddy reload --config caddy.json $ caddy stop`, - // kind of annoying to have all the help text printed out if - // caddy has an error provisioning its modules, for instance... - SilenceUsage: true, - Version: onlyVersionText(), -} + // kind of annoying to have all the help text printed out if + // caddy has an error provisioning its modules, for instance... + SilenceUsage: true, + Version: onlyVersionText(), + } +}) const fullDocsFooter = `Full documentation is available at: https://caddyserver.com/docs/command-line` func init() { - rootCmd.SetVersionTemplate("{{.Version}}\n") - rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") + defaultFactory.Use(func(cmd *cobra.Command) { + cmd.SetVersionTemplate("{{.Version}}\n") + cmd.SetHelpTemplate(cmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") + }) } func onlyVersionText() string { diff --git a/cmd/commandfactory.go b/cmd/commandfactory.go new file mode 100644 index 00000000000..49a38a4e194 --- /dev/null +++ b/cmd/commandfactory.go @@ -0,0 +1,28 @@ +package caddycmd + +import ( + "github.com/spf13/cobra" +) + +type RootCommandFactory struct { + constructor func() *cobra.Command + options []func(*cobra.Command) +} + +func NewRootCommandFactory(fn func() *cobra.Command) *RootCommandFactory { + return &RootCommandFactory{ + constructor: fn, + } +} + +func (f *RootCommandFactory) Use(fn func(cmd *cobra.Command)) { + f.options = append(f.options, fn) +} + +func (f *RootCommandFactory) Build() *cobra.Command { + o := f.constructor() + for _, v := range f.options { + v(o) + } + return o +} diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 746cf3da6b6..8de079bfe4b 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -20,6 +20,7 @@ import ( "crypto/rand" "encoding/json" "errors" + "flag" "fmt" "io" "io/fs" @@ -257,6 +258,7 @@ func cmdRun(fl Flags) (int, error) { // if enabled, reload config file automatically on changes // (this better only be used in dev!) + // do not enable this during tests, it will cause leaks if watchFlag { go watchConfigFile(configFile, configAdapterFlag) } @@ -280,7 +282,11 @@ func cmdRun(fl Flags) (int, error) { } } - select {} + if flag.Lookup("test.v") == nil || !strings.Contains(os.Args[0], ".test") { + select {} + } else { + return caddy.ExitCodeSuccess, nil + } } func cmdStop(fl Flags) (int, error) { diff --git a/cmd/commands.go b/cmd/commands.go index e5e1265e441..7e7af1c77d0 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -459,7 +459,8 @@ argument of --directory. If the directory does not exist, it will be created. if err := os.MkdirAll(dir, 0o755); err != nil { return caddy.ExitCodeFailedQuit, err } - if err := doc.GenManTree(rootCmd, &doc.GenManHeader{ + ccmd := defaultFactory.Build() + if err := doc.GenManTree(ccmd, &doc.GenManHeader{ Title: "Caddy", Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections }, dir); err != nil { @@ -471,10 +472,11 @@ argument of --directory. If the directory does not exist, it will be created. }) // source: https://github.com/spf13/cobra/blob/main/shell_completions.md - rootCmd.AddCommand(&cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Long: fmt.Sprintf(`To load completions: + defaultFactory.Use(func(ccmd *cobra.Command) { + ccmd.AddCommand(&cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate completion script", + Long: fmt.Sprintf(`To load completions: Bash: @@ -512,24 +514,25 @@ argument of --directory. If the directory does not exist, it will be created. # To load completions for every new session, run: PS> %[1]s completion powershell > %[1]s.ps1 # and source this file from your PowerShell profile. - `, rootCmd.Root().Name()), - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), - RunE: func(cmd *cobra.Command, args []string) error { - switch args[0] { - case "bash": - return cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - return cmd.Root().GenZshCompletion(os.Stdout) - case "fish": - return cmd.Root().GenFishCompletion(os.Stdout, true) - case "powershell": - return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - default: - return fmt.Errorf("unrecognized shell: %s", args[0]) - } - }, + `, defaultFactory.constructor().Name()), + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: func(cmd *cobra.Command, args []string) error { + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + return cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + return cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + default: + return fmt.Errorf("unrecognized shell: %s", args[0]) + } + }, + }) }) } @@ -563,7 +566,9 @@ func RegisterCommand(cmd Command) { if !commandNameRegex.MatchString(cmd.Name) { panic("invalid command name") } - rootCmd.AddCommand(caddyCmdToCobra(cmd)) + defaultFactory.Use(func(ccmd *cobra.Command) { + ccmd.AddCommand(caddyCmdToCobra(cmd)) + }) } var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`) diff --git a/cmd/main.go b/cmd/main.go index 3c3ae627087..6defac7568b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -71,7 +71,7 @@ func Main() { if err != nil { caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err)) } - + rootCmd := defaultFactory.Build() if err := rootCmd.Execute(); err != nil { var exitError *exitError if errors.As(err, &exitError) { @@ -81,6 +81,18 @@ func Main() { } } +// MainForTesting implements the main function of the caddy command, used internally for testing +func MainForTesting(args ...string) error { + // create a root command for testing which will not pollute the global namespace, and does not + // call os.Exit(). + rootCmd := defaultFactory.Build() + rootCmd.SetArgs(args) + if err := rootCmd.Execute(); err != nil { + return err + } + return nil +} + // handlePingbackConn reads from conn and ensures it matches // the bytes in expect, or returns an error if it doesn't. func handlePingbackConn(conn net.Conn, expect []byte) error { diff --git a/logging.go b/logging.go index ca10beeeddc..09bf8f846bc 100644 --- a/logging.go +++ b/logging.go @@ -16,6 +16,7 @@ package caddy import ( "encoding/json" + "flag" "fmt" "io" "log" @@ -699,7 +700,13 @@ type defaultCustomLog struct { // and enables INFO-level logs and higher. func newDefaultProductionLog() (*defaultCustomLog, error) { cl := new(CustomLog) - cl.writerOpener = StderrWriter{} + f := flag.Lookup("test.v") + if (f != nil && f.Value.String() != "true") || strings.Contains(os.Args[0], ".test") { + cl.writerOpener = &DiscardWriter{} + } else { + cl.writerOpener = StderrWriter{} + } + var err error cl.writer, err = cl.writerOpener.OpenWriter() if err != nil { diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 3d703280403..5d54742ddb7 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -15,7 +15,6 @@ package fileserver import ( - "bytes" "errors" "fmt" "io" @@ -691,10 +690,6 @@ func (fsrv *FileServer) getEtagFromFile(fileSystem fs.FS, filename string) (stri if err != nil { return "", fmt.Errorf("cannot read etag from file %s: %v", etagFilename, err) } - - // Etags should not contain newline characters - etag = bytes.ReplaceAll(etag, []byte("\n"), []byte{}) - return string(etag), nil } return "", nil