diff --git a/caddy.go b/caddy.go index 5f71d8e8b..5088e9350 100644 --- a/caddy.go +++ b/caddy.go @@ -20,6 +20,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "flag" "fmt" "io" "io/fs" @@ -749,9 +750,12 @@ func Validate(cfg *Config) error { // Errors are logged along the way, and an appropriate exit // code is emitted. func exitProcess(ctx context.Context, logger *zap.Logger) { + notRunningInTest := flag.Lookup("test.v") == nil && !strings.Contains(os.Args[0], ".test") // let the rest of the program know we're quitting; only do it once - if !atomic.CompareAndSwapInt32(exiting, 0, 1) { - return + if notRunningInTest { + if !atomic.CompareAndSwapInt32(exiting, 0, 1) { + return + } } // give the OS or service/process manager our 2 weeks' notice: we quit @@ -809,7 +813,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 notRunningInTest { + os.Exit(exitCode) + } }() if remoteAdminServer != nil { diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 7b56bb281..d501b90cc 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -5,28 +5,21 @@ import ( "context" "crypto/tls" "encoding/json" - "errors" "fmt" "io" - "io/fs" "log" "net" "net/http" "net/http/cookiejar" "os" - "path" - "reflect" - "regexp" - "runtime" + "strconv" "strings" + "sync/atomic" "testing" "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" ) @@ -45,30 +38,32 @@ type Config struct { // Default testing values var Default = Config{ - 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 - config Config + Client *http.Client + + portOne int + portTwo int + + started atomic.Bool + configLoaded bool + configFileName string + envFileName string + + t testing.TB + config Config } // NewTester will create a new testing client with an attached cookie jar -func NewTester(t testing.TB) *Tester { +func NewTester(t testing.TB) (*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{ @@ -80,7 +75,7 @@ func NewTester(t testing.TB) *Tester { configLoaded: false, t: t, config: Default, - } + }, nil } // WithDefaultOverrides this will override the default test configuration with the provided values. @@ -113,34 +108,28 @@ 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 - } +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) + } - err := validateTestPrerequisites(tc) - 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/", tc.config.AdminPort)) if err != nil { @@ -154,9 +143,52 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { _ = json.Indent(&out, body, "", " ") tc.t.Logf("----------- failed with config -----------\n%s", out.String()) } - }) + }() - rawConfig = prependCaddyFilePath(rawConfig) + resp, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", tc.config.AdminPort), "", nil) + if err != nil { + return fmt.Errorf("couldn't stop caddytest server: %w", err) + } + resp.Body.Close() + for range 10 { + if tc.isCaddyAdminRunning() != nil { + return nil + } + time.Sleep(100 * time.Millisecond) + } + + return fmt.Errorf("timed out waiting for caddytest server to stop") +} + +func (tc *Tester) AdminPort() int { + return tc.config.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.config.AdminPort)) + x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_PORT}", fmt.Sprintf("%d", tc.config.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.config.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) @@ -177,8 +209,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { start := time.Now() req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.config.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" { @@ -189,16 +220,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 { @@ -206,105 +235,107 @@ 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) - } - - var expected any - err := json.Unmarshal(expectedBytes, &expected) - if err != nil { - return err - } - +func (tc *Tester) GetCurrentConfig(receiver any) error { client := &http.Client{ Timeout: tc.config.LoadRequestTimeout, } - - fetchConfig := func(client *http.Client) any { - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.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 + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort)) + if err != nil { + return nil } - - for retries := 10; retries > 0; retries-- { - if reflect.DeepEqual(expected, fetchConfig(client)) { - return nil - } - time.Sleep(1 * time.Second) + defer resp.Body.Close() + actualBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil } - tc.t.Errorf("POSTed configuration isn't active") - return errors.New("EnsureConfigRunning: POSTed configuration isn't active") + err = json.Unmarshal(actualBytes, &receiver) + if err != nil { + return nil + } + return nil } -const initConfig = `{ - admin localhost:%d -} -` - -// validateTestPrerequisites ensures the certificates are available in the -// designated path and Caddy sub-process is running. -func validateTestPrerequisites(tc *Tester) error { - // check certificates are found - for _, certName := range tc.config.Certificates { - if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("caddy integration test certificates (%s) not found", certName) - } +func getFreePort() (int, error) { + lr, err := net.Listen("tcp", "localhost:0") + if err != nil { + return 0, err } - if isCaddyAdminRunning(tc) != nil { - // setup the init config file, and set the cleanup afterwards + 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) + } + return i, nil +} + +// 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") + } + a, err := getFreePort() + if err != nil { + return fmt.Errorf("could not find a open port to listen on: %w", err) + } + tc.config.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 } - tc.t.Cleanup(func() { - os.Remove(f.Name()) - }) + tc.configFileName = f.Name() + + initConfig := `{ + admin localhost:%d +}` if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); 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(tc) != nil; retries-- { - time.Sleep(1 * time.Second) + if err := f.Close(); err != nil { + return err } } - // one more time to return the error - return isCaddyAdminRunning(tc) -} + // 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) + } -func isCaddyAdminRunning(tc *Tester) error { + // one more time to return the error + return tc.isCaddyAdminRunning() +} +func (tc *Tester) isCaddyAdminRunning() error { // assert that caddy is running client := &http.Client{ - Timeout: tc.config.LoadRequestTimeout, + Timeout: Default.LoadRequestTimeout, } resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort)) if err != nil { @@ -315,24 +346,6 @@ func isCaddyAdminRunning(tc *Tester) error { 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{ @@ -359,231 +372,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 000000000..89f5d9284 --- /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(t) + 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 a9d5da936..d788a810b 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,12 +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": [ - ":9080" + ":{$TESTING_CADDY_PORT_ONE}", + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -119,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 f10aef6a8..a94c88f1e 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -27,19 +27,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 { @@ -47,10 +41,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: slog.New(zapslog.NewHandler(logger.Core())), }, ChallengeSolvers: map[string]acmez.Solver{ @@ -100,13 +95,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 { @@ -118,8 +113,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: slog.New(zapslog.NewHandler(logger.Core())), }, ChallengeSolvers: map[string]acmez.Solver{ diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index d6a9ba005..2eb65cabd 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "fmt" "log/slog" "strings" "testing" @@ -18,40 +19,40 @@ import ( ) 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 { + https://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" @@ -77,8 +78,8 @@ func TestACMEServerAllowPolicy(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: slog.New(zapslog.NewHandler(logger.Core())), }, ChallengeSolvers: map[string]acmez.Solver{ @@ -134,14 +135,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" @@ -166,8 +167,8 @@ func TestACMEServerDenyPolicy(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: slog.New(zapslog.NewHandler(logger.Core())), }, ChallengeSolvers: map[string]acmez.Solver{ @@ -200,7 +201,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 1dbdbcee2..ec2964003 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 d45d5a5e9..fde2be6f4 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,203 +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 @@ -729,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 @@ -753,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 @@ -777,14 +797,15 @@ 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 TestHandleErrorSubHandlers(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ admin localhost:2999 http_port 9080 } @@ -815,11 +836,11 @@ func TestHandleErrorSubHandlers(t *testing.T) { } `, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/en/notfound", 404, "not found") - tester.AssertGetResponse("http://localhost:9080/es/notfound", 404, "no encontrado") - tester.AssertGetResponse("http://localhost:9080/notfound", 404, "default not found") - tester.AssertGetResponse("http://localhost:9080/es/internalerr", 500, "Default error") - tester.AssertGetResponse("http://localhost:9080/en/internalerr", 500, "English error") + harness.AssertGetResponse("http://localhost:9080/en/notfound", 404, "not found") + harness.AssertGetResponse("http://localhost:9080/es/notfound", 404, "no encontrado") + harness.AssertGetResponse("http://localhost:9080/notfound", 404, "default not found") + harness.AssertGetResponse("http://localhost:9080/es/internalerr", 500, "Default error") + harness.AssertGetResponse("http://localhost:9080/en/internalerr", 500, "English error") } func TestInvalidSiteAddressesAsDirectives(t *testing.T) { diff --git a/caddytest/integration/h2listener_test.go b/caddytest/integration/h2listener_test.go index 451c925ba..b841abfb2 100644 --- a/caddytest/integration/h2listener_test.go +++ b/caddytest/integration/h2listener_test.go @@ -10,14 +10,15 @@ import ( "github.com/caddyserver/caddy/v2/caddytest" ) -func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester { +func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.TestHarness { const baseConfig = ` { skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 - servers :9443 { + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} + + servers :{$TESTING_CADDY_PORT_TWO} { protocols %s } } @@ -25,10 +26,10 @@ func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []strin respond "{http.request.tls.proto} {http.request.proto}" } ` - tester := caddytest.NewTester(t) - tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile") + harness := caddytest.StartHarness(t) + harness.LoadConfig(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile") - tr := tester.Client.Transport.(*http.Transport) + tr := harness.Client().Transport.(*http.Transport) tr.TLSClientConfig.NextProtos = clientVersions tr.Protocols = new(http.Protocols) if slices.Contains(clientVersions, "h2") { @@ -39,10 +40,10 @@ func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []strin tr.Protocols.SetHTTP1(false) } - return tester + return harness } -func TestH2ListenerWithTLS(t *testing.T) { +func TestH2ListenerWithTLS(pt *testing.T) { tests := []struct { serverVersions []string clientVersions []string @@ -57,26 +58,28 @@ func TestH2ListenerWithTLS(t *testing.T) { {[]string{"h2", "h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false}, } for _, tc := range tests { - tester := newH2ListenerWithVersionsWithTLSTester(t, tc.serverVersions, tc.clientVersions) - t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions) - if tc.failed { - resp, err := tester.Client.Get("https://localhost:9443") - if err == nil { - t.Errorf("unexpected response: %d", resp.StatusCode) + pt.Run(fmt.Sprintf("serverVersions=%v,clientVersions=%v", tc.serverVersions, tc.clientVersions), func(t *testing.T) { + harness := newH2ListenerWithVersionsWithTLSTester(t, tc.serverVersions, tc.clientVersions) + t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions) + if tc.failed { + resp, err := harness.Client().Get(fmt.Sprintf("http://localhost:%d", harness.Tester().PortTwo())) + if err == nil { + t.Errorf("unexpected response: %d", resp.StatusCode) + } + } else { + harness.AssertGetResponse(fmt.Sprintf("https://localhost:%d", harness.Tester().PortTwo()), 200, tc.expectedBody) } - } else { - tester.AssertGetResponse("https://localhost:9443", 200, tc.expectedBody) - } + }) } } -func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester { +func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.TestHarness { const baseConfig = ` { skip_install_trust - admin localhost:2999 - http_port 9080 - servers :9080 { + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + servers :{$TESTING_CADDY_PORT_ONE} { protocols %s } } @@ -84,10 +87,10 @@ func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []st respond "{http.request.proto}" } ` - tester := caddytest.NewTester(t) - tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile") + tester := caddytest.StartHarness(t) + tester.LoadConfig(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile") - tr := tester.Client.Transport.(*http.Transport) + tr := tester.Client().Transport.(*http.Transport) tr.Protocols = new(http.Protocols) if slices.Contains(clientVersions, "h2c") { tr.Protocols.SetHTTP1(false) @@ -100,7 +103,7 @@ func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []st return tester } -func TestH2ListenerWithoutTLS(t *testing.T) { +func TestH2ListenerWithoutTLS(pt *testing.T) { tests := []struct { serverVersions []string clientVersions []string @@ -115,15 +118,17 @@ func TestH2ListenerWithoutTLS(t *testing.T) { {[]string{"h2c", "h1"}, []string{"http/1.1"}, "HTTP/1.1", false}, } for _, tc := range tests { - tester := newH2ListenerWithVersionsWithoutTLSTester(t, tc.serverVersions, tc.clientVersions) - t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions) - if tc.failed { - resp, err := tester.Client.Get("http://localhost:9080") - if err == nil { - t.Errorf("unexpected response: %d", resp.StatusCode) + pt.Run(fmt.Sprintf("serverVersions=%v,clientVersions=%v", tc.serverVersions, tc.clientVersions), func(t *testing.T) { + harness := newH2ListenerWithVersionsWithoutTLSTester(t, tc.serverVersions, tc.clientVersions) + t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions) + if tc.failed { + resp, err := harness.Client().Get(fmt.Sprintf("http://localhost:%d", harness.Tester().PortOne())) + if err == nil { + t.Errorf("unexpected response: %d", resp.StatusCode) + } + } else { + harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d", harness.Tester().PortOne()), 200, tc.expectedBody) } - } else { - tester.AssertGetResponse("http://localhost:9080", 200, tc.expectedBody) - } + }) } } diff --git a/caddytest/integration/handler_test.go b/caddytest/integration/handler_test.go index afc700b02..500c0e448 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 6f8ffc929..5b15221cd 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 header /intercept To-Intercept ok respond /no-intercept "I'm not a teapot" @@ -27,14 +28,13 @@ func TestIntercept(t *testing.T) { header /intercept intercepted {resp.header.To-Intercept} respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503 } - } + } } `, "caddyfile") - r, _ := tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") + r, _ := 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") if r.Header.Get("intercepted") != "ok" { t.Fatalf(`header "intercepted" value is not "ok": %s`, r.Header.Get("intercepted")) } - - tester.AssertGetResponse("http://localhost:9080/no-intercept", 200, "I'm not a teapot") + 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 4399902ea..502674edb 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 30642b1ae..f22f5133d 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 eb3386564..88ceb9e36 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 cbfe8433b..f141f6130 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 188f93541..8a5cfb736 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 57231a527..667dd2dcc 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -21,21 +21,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": [ { @@ -103,7 +103,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) @@ -117,7 +117,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", @@ -128,7 +128,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 } @@ -150,7 +150,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) @@ -164,7 +164,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 @@ -205,28 +205,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": [ { @@ -313,7 +306,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) @@ -327,7 +320,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", @@ -341,7 +334,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 } @@ -359,9 +352,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 000000000..742badae1 --- /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(tc.t) + 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 extracting 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/commandfuncs.go b/cmd/commandfuncs.go index 75d114992..9af592eb5 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -20,6 +20,7 @@ import ( "crypto/rand" "encoding/json" "errors" + "flag" "fmt" "io" "io/fs" @@ -308,6 +309,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, adapterUsed) } @@ -331,10 +333,11 @@ func cmdRun(fl Flags) (int, error) { } } - // release the last local logger reference - logger = nil //nolint:wastedassign,ineffassign - - 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 c9ea636b9..6e8164437 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -525,6 +525,7 @@ 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()), CobraFunc: func(cmd *cobra.Command) { cmd.DisableFlagsInUseLine = true diff --git a/cmd/main.go b/cmd/main.go index 411f4545d..5042cb342 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -78,6 +78,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 2734b5425..689c4113f 100644 --- a/logging.go +++ b/logging.go @@ -710,6 +710,7 @@ type defaultCustomLog struct { func newDefaultProductionLog() (*defaultCustomLog, error) { cl := new(CustomLog) 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 3daf8daef..5b3503295 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -763,7 +763,6 @@ func (fsrv *FileServer) getEtagFromFile(fileSystem fs.FS, filename string) (stri // Etags should not contain newline characters etag = bytes.ReplaceAll(etag, []byte("\n"), []byte{}) - return string(etag), nil } return "", nil