From 93a185302216c404602cb7635eac5cb31cbf6cd7 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 18:16:33 -0500 Subject: [PATCH 01/14] noot --- caddy.go | 6 +- caddytest/caddytest.go | 78 +++++++++++++++++------- caddytest/integration/acme_test.go | 41 ++++++------- caddytest/integration/acmeserver_test.go | 16 ++--- caddytest/integration/stream_test.go | 11 +--- logging.go | 9 ++- 6 files changed, 96 insertions(+), 65 deletions(-) diff --git a/caddy.go b/caddy.go index 7dd989c9e1e..9aba97fa415 100644 --- a/caddy.go +++ b/caddy.go @@ -20,6 +20,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "flag" "fmt" "io" "io/fs" @@ -778,7 +779,10 @@ func exitProcess(ctx context.Context, logger *zap.Logger) { } else { logger.Error("unclean shutdown") } - os.Exit(exitCode) + // check if we are in test environment, and dont call exit if we are + if flag.Lookup("test.v") == nil || strings.Contains(os.Args[0], ".test") { + os.Exit(exitCode) + } }() if remoteAdminServer != nil { diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 05aa1e3f52f..d4a0a6c8760 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -81,6 +81,10 @@ func NewTester(t testing.TB) *Tester { } } +func (t *Tester) T() testing.TB { + return t.t +} + type configLoadError struct { Response string } @@ -92,33 +96,37 @@ 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. +// launch caddy will start the server +func (tc *Tester) LaunchCaddy() { + if err := tc.startServer(); err != nil { + tc.t.Logf("failed to start server: %s", err) + tc.t.Fail() + } +} + +// DEPRECATED: InitServer +// Initserver is shorthand for LaunchCaddy() and LoadConfig(rawConfig, configType string) 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) + if err := tc.startServer(); err != nil { + tc.t.Logf("failed to start server: %s", err) tc.t.Fail() } - if err := tc.ensureConfigRunning(rawConfig, configType); err != nil { + if err := tc.LoadConfig(rawConfig, configType); err != nil { tc.t.Logf("failed ensuring config is running: %s", err) tc.t.Fail() } } -// InitServer this will configure the server with a configurion of a specific -// type. The configType must be either "json" or the adapter type. -func (tc *Tester) initServer(rawConfig string, configType string) error { +func (tc *Tester) startServer() error { if testing.Short() { tc.t.SkipNow() return nil } - - err := validateTestPrerequisites(tc.t) + err := validateAndStartServer(tc.t) if err != nil { tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err) return nil } - tc.t.Cleanup(func() { if tc.t.Failed() && tc.configLoaded { res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) @@ -133,8 +141,35 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { _ = json.Indent(&out, body, "", " ") tc.t.Logf("----------- failed with config -----------\n%s", out.String()) } + // now shutdown the server, since the test is done. + + _, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", Default.AdminPort), "", nil) + if err != nil { + tc.t.Log("couldn't stop admin server") + } + time.Sleep(1 * time.Millisecond) + // try ensure the admin api is stopped three times. + for retries := 0; retries < 3; retries++ { + if isCaddyAdminRunning() != nil { + return + } + time.Sleep(10 * time.Millisecond) + tc.t.Log("timed out waiting for admin server to stop") + } }) + return nil +} + +func (tc *Tester) MustLoadConfig(rawConfig string, configType string) { + if err := tc.LoadConfig(rawConfig, configType); err != nil { + tc.t.Logf("failed ensuring config is running: %s", err) + tc.t.Fail() + } +} +// LoadConfig loads the config to the tester server and also ensures that the config was loaded +func (tc *Tester) LoadConfig(rawConfig string, configType string) error { + originalRawConfig := rawConfig rawConfig = prependCaddyFilePath(rawConfig) // normalize JSON config if configType == "json" { @@ -185,7 +220,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { } tc.configLoaded = true - return nil + return tc.ensureConfigRunning(originalRawConfig, configType) } func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error { @@ -226,7 +261,9 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error return actual } - for retries := 10; retries > 0; retries-- { + // TODO: does this really need to be tried more than once? + // Caddy should block until the new config is loaded, which means needing to wait is a caddy bug + for retries := 3; retries > 0; retries-- { if reflect.DeepEqual(expected, fetchConfig(client)) { return nil } @@ -241,9 +278,9 @@ const initConfig = `{ } ` -// validateTestPrerequisites ensures the certificates are available in the -// designated path and Caddy sub-process is running. -func validateTestPrerequisites(t testing.TB) error { +// validateAndStartServer ensures the certificates are available in the +// designated path, launches caddy, and then ensures the Caddy sub-process is running. +func validateAndStartServer(t testing.TB) error { // check certificates are found for _, certName := range Default.Certificates { if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) { @@ -269,9 +306,8 @@ func validateTestPrerequisites(t testing.TB) error { go func() { caddycmd.Main() }() - - // wait for caddy to start serving the initial config - for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { + // wait for caddy admin api to start. it should happen quickly. + for retries := 3; retries > 0 && isCaddyAdminRunning() != nil; retries-- { time.Sleep(1 * time.Second) } } @@ -342,8 +378,8 @@ func CreateTestingTransport() *http.Transport { // 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) + tc.LaunchCaddy() + err := tc.LoadConfig(rawConfig, configType) if !strings.Contains(err.Error(), expectedError) { t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) } diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index ceacd1db0fa..43f99b15e7e 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -19,19 +19,27 @@ import ( "go.uber.org/zap" ) +func curry2[A, B any](fn func(A, B)) func(a A) func(b B) { + return func(a A) func(B) { + return func(b B) { + fn(a, b) + } + } +} + const acmeChallengePort = 9081 +func TestAcmeServer(t *testing.T) { + tester := caddytest.NewTester(t) + tester.LaunchCaddy() + t.Run("WithDefaults", curry2(testACMEServerWithDefaults)(tester)) + t.Run("WithMismatchedChallenges", curry2(testACMEServerWithDefaults)(tester)) +} + // Test the basic functionality of Caddy's ACME server -func TestACMEServerWithDefaults(t *testing.T) { +func testACMEServerWithDefaults(tester *caddytest.Tester, t *testing.T) { ctx := context.Background() - logger, err := zap.NewDevelopment() - if err != nil { - t.Error(err) - return - } - - tester := caddytest.NewTester(t) - tester.InitServer(` + tester.MustLoadConfig(` { skip_install_trust admin localhost:2999 @@ -44,6 +52,7 @@ 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", @@ -93,20 +102,10 @@ func TestACMEServerWithDefaults(t *testing.T) { } } -func TestACMEServerWithMismatchedChallenges(t *testing.T) { +func testACMEServerWithMismatchedChallenges(tester *caddytest.Tester, t *testing.T) { ctx := context.Background() logger := caddy.Log().Named("acmez") - - tester := caddytest.NewTester(t) - tester.InitServer(` - { - skip_install_trust - admin localhost:2999 - http_port 9080 - https_port 9443 - local_certs - } - acme.localhost { + tester.MustLoadConfig(` acme_server { challenges tls-alpn-01 } diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index 22b716f84db..f05fb836e15 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -8,10 +8,10 @@ import ( "strings" "testing" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddytest" "github.com/mholt/acmez/v2" "github.com/mholt/acmez/v2/acme" - "go.uber.org/zap" ) func TestACMEServerDirectory(t *testing.T) { @@ -66,11 +66,7 @@ func TestACMEServerAllowPolicy(t *testing.T) { `, "caddyfile") ctx := context.Background() - logger, err := zap.NewDevelopment() - if err != nil { - t.Error(err) - return - } + logger := caddy.Log().Named("acmez") client := acmez.Client{ Client: &acme.Client{ @@ -155,11 +151,7 @@ func TestACMEServerDenyPolicy(t *testing.T) { `, "caddyfile") ctx := context.Background() - logger, err := zap.NewDevelopment() - if err != nil { - t.Error(err) - return - } + logger := caddy.Log().Named("acmez") client := acmez.Client{ Client: &acme.Client{ @@ -197,7 +189,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/stream_test.go b/caddytest/integration/stream_test.go index d2f2fd79b95..d82882d6997 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -21,7 +21,7 @@ import ( // (see https://github.com/caddyserver/caddy/issues/3556 for use case) func TestH2ToH2CStream(t *testing.T) { tester := caddytest.NewTester(t) - tester.InitServer(` + tester.InitServer(` { "admin": { "listen": "localhost:2999" @@ -205,18 +205,11 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { // (see https://github.com/caddyserver/caddy/issues/3606 for use case) func TestH2ToH1ChunkedResponse(t *testing.T) { tester := caddytest.NewTester(t) - tester.InitServer(` + tester.InitServer(` { "admin": { "listen": "localhost:2999" }, - "logging": { - "logs": { - "default": { - "level": "DEBUG" - } - } - }, "apps": { "http": { "http_port": 9080, diff --git a/logging.go b/logging.go index ca10beeeddc..09bf8f846bc 100644 --- a/logging.go +++ b/logging.go @@ -16,6 +16,7 @@ package caddy import ( "encoding/json" + "flag" "fmt" "io" "log" @@ -699,7 +700,13 @@ type defaultCustomLog struct { // and enables INFO-level logs and higher. func newDefaultProductionLog() (*defaultCustomLog, error) { cl := new(CustomLog) - cl.writerOpener = StderrWriter{} + f := flag.Lookup("test.v") + if (f != nil && f.Value.String() != "true") || strings.Contains(os.Args[0], ".test") { + cl.writerOpener = &DiscardWriter{} + } else { + cl.writerOpener = StderrWriter{} + } + var err error cl.writer, err = cl.writerOpener.OpenWriter() if err != nil { From 2619271a5c7366215d3319b336bda47b99ad5bf1 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 19:44:05 -0500 Subject: [PATCH 02/14] noot --- caddytest/caddytest.go | 407 +++--------------- caddytest/caddytest_assert.go | 109 +++++ caddytest/caddytest_test.go | 8 +- caddytest/integration/acme_test.go | 38 +- caddytest/integration/acmeserver_test.go | 16 +- caddytest/integration/autohttps_test.go | 24 +- caddytest/integration/caddyfile_test.go | 130 +++--- caddytest/integration/handler_test.go | 8 +- caddytest/integration/intercept_test.go | 4 +- caddytest/integration/leafcertloaders_test.go | 4 +- caddytest/integration/listener_test.go | 10 +- caddytest/integration/map_test.go | 12 +- caddytest/integration/reverseproxy_test.go | 28 +- caddytest/integration/sni_test.go | 12 +- caddytest/integration/stream_test.go | 8 +- caddytest/testing_harness.go | 223 ++++++++++ 16 files changed, 530 insertions(+), 511 deletions(-) create mode 100644 caddytest/caddytest_assert.go create mode 100644 caddytest/testing_harness.go diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index d4a0a6c8760..f22a09ef25e 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -1,14 +1,12 @@ package caddytest import ( - "bytes" "context" "crypto/tls" "encoding/json" "errors" "fmt" "io" - "io/fs" "log" "net" "net/http" @@ -16,14 +14,10 @@ import ( "os" "path" "reflect" - "regexp" "runtime" "strings" - "testing" "time" - "github.com/aryann/difflib" - caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -51,23 +45,18 @@ var Default = Defaults{ LoadRequestTimeout: 5 * time.Second, } -var ( - matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`) - matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`) -) - // Tester represents an instance of a test client. type Tester struct { - Client *http.Client - configLoaded bool - t testing.TB + Client *http.Client + configLoaded bool + configFileName string } // NewTester will create a new testing client with an attached cookie jar -func NewTester(t testing.TB) *Tester { +func NewTester() (*Tester, error) { jar, err := cookiejar.New(nil) if err != nil { - t.Fatalf("failed to create cookiejar: %s", err) + return nil, fmt.Errorf("failed to create cookiejar: %w", err) } return &Tester{ @@ -77,12 +66,7 @@ func NewTester(t testing.TB) *Tester { Timeout: Default.TestRequestTimeout, }, configLoaded: false, - t: t, - } -} - -func (t *Tester) T() testing.TB { - return t.t + }, nil } type configLoadError struct { @@ -97,83 +81,40 @@ func timeElapsed(start time.Time, name string) { } // launch caddy will start the server -func (tc *Tester) LaunchCaddy() { +func (tc *Tester) LaunchCaddy() error { if err := tc.startServer(); err != nil { - tc.t.Logf("failed to start server: %s", err) - tc.t.Fail() - } -} - -// DEPRECATED: InitServer -// Initserver is shorthand for LaunchCaddy() and LoadConfig(rawConfig, configType string) -func (tc *Tester) InitServer(rawConfig string, configType string) { - if err := tc.startServer(); err != nil { - tc.t.Logf("failed to start server: %s", err) - tc.t.Fail() - } - if err := tc.LoadConfig(rawConfig, configType); err != nil { - tc.t.Logf("failed ensuring config is running: %s", err) - tc.t.Fail() + return fmt.Errorf("failed to start server: %w", err) } + return nil } -func (tc *Tester) startServer() error { - if testing.Short() { - tc.t.SkipNow() - return nil - } - err := validateAndStartServer(tc.t) +func (tc *Tester) CleanupCaddy() error { + // now shutdown the server, since the test is done. + defer func() { + // try to remove the tmp config file we created + os.Remove(tc.configFileName) + }() + _, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", Default.AdminPort), "", nil) if err != nil { - tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err) - return nil + return fmt.Errorf("couldn't stop caddytest server") } - tc.t.Cleanup(func() { - if tc.t.Failed() && tc.configLoaded { - res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) - if err != nil { - tc.t.Log("unable to read the current config") - return - } - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - var out bytes.Buffer - _ = json.Indent(&out, body, "", " ") - tc.t.Logf("----------- failed with config -----------\n%s", out.String()) + time.Sleep(200 * time.Millisecond) + for retries := 0; retries < 10; retries++ { + if isCaddyAdminRunning() != nil { + return nil } - // now shutdown the server, since the test is done. + time.Sleep(100 * time.Millisecond) + } - _, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", Default.AdminPort), "", nil) - if err != nil { - tc.t.Log("couldn't stop admin server") - } - time.Sleep(1 * time.Millisecond) - // try ensure the admin api is stopped three times. - for retries := 0; retries < 3; retries++ { - if isCaddyAdminRunning() != nil { - return - } - time.Sleep(10 * time.Millisecond) - tc.t.Log("timed out waiting for admin server to stop") - } - }) - return nil -} + return fmt.Errorf("timed out waiting for caddytest server to stop") -func (tc *Tester) MustLoadConfig(rawConfig string, configType string) { - if err := tc.LoadConfig(rawConfig, configType); err != nil { - tc.t.Logf("failed ensuring config is running: %s", err) - tc.t.Fail() - } } // LoadConfig loads the config to the tester server and also ensures that the config was loaded func (tc *Tester) LoadConfig(rawConfig string, configType string) error { originalRawConfig := rawConfig - rawConfig = prependCaddyFilePath(rawConfig) // normalize JSON config if configType == "json" { - tc.t.Logf("Before: %s", rawConfig) var conf any if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil { return err @@ -183,7 +124,6 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { return err } rawConfig = string(c) - tc.t.Logf("After: %s", rawConfig) } client := &http.Client{ Timeout: Default.LoadRequestTimeout, @@ -191,8 +131,7 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { start := time.Now() req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig)) if err != nil { - tc.t.Errorf("failed to create request. %s", err) - return err + return fmt.Errorf("failed to create request. %w", err) } if configType == "json" { @@ -203,16 +142,14 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { res, err := client.Do(req) if err != nil { - tc.t.Errorf("unable to contact caddy server. %s", err) - return err + return fmt.Errorf("unable to contact caddy server. %w", err) } timeElapsed(start, "caddytest: config load time") defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { - tc.t.Errorf("unable to read response. %s", err) - return err + return fmt.Errorf("unable to read response. %w", err) } if res.StatusCode != 200 { @@ -224,7 +161,7 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { } func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error { - expectedBytes := []byte(prependCaddyFilePath(rawConfig)) + expectedBytes := []byte(rawConfig) if configType != "json" { adapter := caddyconfig.GetAdapter(configType) if adapter == nil { @@ -269,7 +206,6 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error } time.Sleep(1 * time.Second) } - tc.t.Errorf("POSTed configuration isn't active") return errors.New("EnsureConfigRunning: POSTed configuration isn't active") } @@ -278,38 +214,30 @@ const initConfig = `{ } ` -// validateAndStartServer ensures the certificates are available in the -// designated path, launches caddy, and then ensures the Caddy sub-process is running. -func validateAndStartServer(t testing.TB) error { - // check certificates are found - for _, certName := range Default.Certificates { - if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("caddy integration test certificates (%s) not found", certName) - } +// launches caddy, and then ensures the Caddy sub-process is running. +func (tc *Tester) startServer() error { + if isCaddyAdminRunning() == nil { + return fmt.Errorf("caddy test admin port still in use") + } + // setup the init config file, and set the cleanup afterwards + f, err := os.CreateTemp("", "") + if err != nil { + return err } + tc.configFileName = f.Name() - if isCaddyAdminRunning() != nil { - // setup the init config file, and set the cleanup afterwards - f, err := os.CreateTemp("", "") - if err != nil { - return err - } - t.Cleanup(func() { - os.Remove(f.Name()) - }) - if _, err := f.WriteString(initConfig); err != nil { - return err - } + if _, err := f.WriteString(initConfig); err != nil { + return err + } - // start inprocess caddy server - os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"} - go func() { - caddycmd.Main() - }() - // wait for caddy admin api to start. it should happen quickly. - for retries := 3; retries > 0 && isCaddyAdminRunning() != nil; retries-- { - time.Sleep(1 * time.Second) - } + // start inprocess caddy server + os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"} + go func() { + caddycmd.Main() + }() + // wait for caddy admin api to start. it should happen quickly. + for retries := 3; retries > 0 && isCaddyAdminRunning() != nil; retries-- { + time.Sleep(1 * time.Second) } // one more time to return the error @@ -339,15 +267,6 @@ func getIntegrationDir() string { return path.Dir(filename) } -// use the convention to replace /[certificatename].[crt|key] with the full path -// this helps reduce the noise in test configurations and also allow this -// to run in any path -func prependCaddyFilePath(rawConfig string) string { - r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1") - r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1") - return r -} - // CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally func CreateTestingTransport() *http.Transport { dialer := net.Dialer{ @@ -374,231 +293,3 @@ func CreateTestingTransport() *http.Transport { TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec } } - -// AssertLoadError will load a config and expect an error -func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { - tc := NewTester(t) - tc.LaunchCaddy() - err := tc.LoadConfig(rawConfig, configType) - if !strings.Contains(err.Error(), expectedError) { - t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) - } -} - -// AssertRedirect makes a request and asserts the redirection happens -func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { - redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - - // using the existing client, we override the check redirect policy for this test - old := tc.Client.CheckRedirect - tc.Client.CheckRedirect = redirectPolicyFunc - defer func() { tc.Client.CheckRedirect = old }() - - resp, err := tc.Client.Get(requestURI) - if err != nil { - tc.t.Errorf("failed to call server %s", err) - return nil - } - - if expectedStatusCode != resp.StatusCode { - tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode) - } - - loc, err := resp.Location() - if err != nil { - tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err) - } - if loc == nil && expectedToLocation != "" { - tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI) - } - if loc != nil { - if expectedToLocation != loc.String() { - tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String()) - } - } - - return resp -} - -// CompareAdapt adapts a config and then compares it against an expected result -func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool { - cfgAdapter := caddyconfig.GetAdapter(adapterName) - if cfgAdapter == nil { - t.Logf("unrecognized config adapter '%s'", adapterName) - return false - } - - options := make(map[string]any) - - result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options) - if err != nil { - t.Logf("adapting config using %s adapter: %v", adapterName, err) - return false - } - - // prettify results to keep tests human-manageable - var prettyBuf bytes.Buffer - err = json.Indent(&prettyBuf, result, "", "\t") - if err != nil { - return false - } - result = prettyBuf.Bytes() - - if len(warnings) > 0 { - for _, w := range warnings { - t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message) - } - } - - diff := difflib.Diff( - strings.Split(expectedResponse, "\n"), - strings.Split(string(result), "\n")) - - // scan for failure - failed := false - for _, d := range diff { - if d.Delta != difflib.Common { - failed = true - break - } - } - - if failed { - for _, d := range diff { - switch d.Delta { - case difflib.Common: - fmt.Printf(" %s\n", d.Payload) - case difflib.LeftOnly: - fmt.Printf(" - %s\n", d.Payload) - case difflib.RightOnly: - fmt.Printf(" + %s\n", d.Payload) - } - } - return false - } - return true -} - -// AssertAdapt adapts a config and then tests it against an expected result -func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) { - ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse) - if !ok { - t.Fail() - } -} - -// Generic request functions - -func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) { - requestContentType := "" - for _, requestHeader := range requestHeaders { - arr := strings.SplitAfterN(requestHeader, ":", 2) - k := strings.TrimRight(arr[0], ":") - v := strings.TrimSpace(arr[1]) - if k == "Content-Type" { - requestContentType = v - } - t.Logf("Request header: %s => %s", k, v) - req.Header.Set(k, v) - } - - if requestContentType == "" { - t.Logf("Content-Type header not provided") - } -} - -// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions -func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response { - resp, err := tc.Client.Do(req) - if err != nil { - tc.t.Fatalf("failed to call server %s", err) - } - - if expectedStatusCode != resp.StatusCode { - tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode) - } - - return resp -} - -// AssertResponse request a URI and assert the status code and the body contains a string -func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) { - resp := tc.AssertResponseCode(req, expectedStatusCode) - - defer resp.Body.Close() - bytes, err := io.ReadAll(resp.Body) - if err != nil { - tc.t.Fatalf("unable to read the response body %s", err) - } - - body := string(bytes) - - if body != expectedBody { - tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body) - } - - return resp, body -} - -// Verb specific test functions - -// AssertGetResponse GET a URI and expect a statusCode and body text -func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { - req, err := http.NewRequest("GET", requestURI, nil) - if err != nil { - tc.t.Fatalf("unable to create request %s", err) - } - - return tc.AssertResponse(req, expectedStatusCode, expectedBody) -} - -// AssertDeleteResponse request a URI and expect a statusCode and body text -func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { - req, err := http.NewRequest("DELETE", requestURI, nil) - if err != nil { - tc.t.Fatalf("unable to create request %s", err) - } - - return tc.AssertResponse(req, expectedStatusCode, expectedBody) -} - -// AssertPostResponseBody POST to a URI and assert the response code and body -func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { - req, err := http.NewRequest("POST", requestURI, requestBody) - if err != nil { - tc.t.Errorf("failed to create request %s", err) - return nil, "" - } - - applyHeaders(tc.t, req, requestHeaders) - - return tc.AssertResponse(req, expectedStatusCode, expectedBody) -} - -// AssertPutResponseBody PUT to a URI and assert the response code and body -func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { - req, err := http.NewRequest("PUT", requestURI, requestBody) - if err != nil { - tc.t.Errorf("failed to create request %s", err) - return nil, "" - } - - applyHeaders(tc.t, req, requestHeaders) - - return tc.AssertResponse(req, expectedStatusCode, expectedBody) -} - -// AssertPatchResponseBody PATCH to a URI and assert the response code and body -func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { - req, err := http.NewRequest("PATCH", requestURI, requestBody) - if err != nil { - tc.t.Errorf("failed to create request %s", err) - return nil, "" - } - - applyHeaders(tc.t, req, requestHeaders) - - return tc.AssertResponse(req, expectedStatusCode, expectedBody) -} diff --git a/caddytest/caddytest_assert.go b/caddytest/caddytest_assert.go new file mode 100644 index 00000000000..d81764d667e --- /dev/null +++ b/caddytest/caddytest_assert.go @@ -0,0 +1,109 @@ +package caddytest + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/aryann/difflib" + "github.com/caddyserver/caddy/v2/caddyconfig" +) + +// AssertLoadError will load a config and expect an error +func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { + tc := StartHarness(t) + err := tc.tester.LoadConfig(rawConfig, configType) + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) + } +} + +// CompareAdapt adapts a config and then compares it against an expected result +func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool { + cfgAdapter := caddyconfig.GetAdapter(adapterName) + if cfgAdapter == nil { + t.Logf("unrecognized config adapter '%s'", adapterName) + return false + } + + options := make(map[string]any) + + result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options) + if err != nil { + t.Logf("adapting config using %s adapter: %v", adapterName, err) + return false + } + + // prettify results to keep tests human-manageable + var prettyBuf bytes.Buffer + err = json.Indent(&prettyBuf, result, "", "\t") + if err != nil { + return false + } + result = prettyBuf.Bytes() + + if len(warnings) > 0 { + for _, w := range warnings { + t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message) + } + } + + diff := difflib.Diff( + strings.Split(expectedResponse, "\n"), + strings.Split(string(result), "\n")) + + // scan for failure + failed := false + for _, d := range diff { + if d.Delta != difflib.Common { + failed = true + break + } + } + + if failed { + for _, d := range diff { + switch d.Delta { + case difflib.Common: + fmt.Printf(" %s\n", d.Payload) + case difflib.LeftOnly: + fmt.Printf(" - %s\n", d.Payload) + case difflib.RightOnly: + fmt.Printf(" + %s\n", d.Payload) + } + } + return false + } + return true +} + +// AssertAdapt adapts a config and then tests it against an expected result +func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) { + ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse) + if !ok { + t.Fail() + } +} + +// Generic request functions + +func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) { + requestContentType := "" + for _, requestHeader := range requestHeaders { + arr := strings.SplitAfterN(requestHeader, ":", 2) + k := strings.TrimRight(arr[0], ":") + v := strings.TrimSpace(arr[1]) + if k == "Content-Type" { + requestContentType = v + } + t.Logf("Request header: %s => %s", k, v) + req.Header.Set(k, v) + } + + if requestContentType == "" { + t.Logf("Content-Type header not provided") + } +} diff --git a/caddytest/caddytest_test.go b/caddytest/caddytest_test.go index 937537faa7c..589c33ea6dc 100644 --- a/caddytest/caddytest_test.go +++ b/caddytest/caddytest_test.go @@ -12,10 +12,10 @@ func TestReplaceCertificatePaths(t *testing.T) { } redir / https://b.caddy.localhost:9443/version 301 - + respond /version 200 { body "hello from a.caddy.localhost" - } + } }` r := prependCaddyFilePath(rawConfig) @@ -34,8 +34,8 @@ func TestReplaceCertificatePaths(t *testing.T) { } func TestLoadUnorderedJSON(t *testing.T) { - tester := NewTester(t) - tester.InitServer(` + tester := StartHarness(t) + tester.LoadConfig(` { "logging": { "logs": { diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index 43f99b15e7e..9697cf9551b 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -19,27 +19,13 @@ import ( "go.uber.org/zap" ) -func curry2[A, B any](fn func(A, B)) func(a A) func(b B) { - return func(a A) func(B) { - return func(b B) { - fn(a, b) - } - } -} - const acmeChallengePort = 9081 -func TestAcmeServer(t *testing.T) { - tester := caddytest.NewTester(t) - tester.LaunchCaddy() - t.Run("WithDefaults", curry2(testACMEServerWithDefaults)(tester)) - t.Run("WithMismatchedChallenges", curry2(testACMEServerWithDefaults)(tester)) -} - // Test the basic functionality of Caddy's ACME server -func testACMEServerWithDefaults(tester *caddytest.Tester, t *testing.T) { +func TestACMEServerWithDefaults(t *testing.T) { ctx := context.Background() - tester.MustLoadConfig(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -56,7 +42,7 @@ func testACMEServerWithDefaults(tester *caddytest.Tester, t *testing.T) { client := acmez.Client{ Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + HTTPClient: tester.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ @@ -102,10 +88,20 @@ func testACMEServerWithDefaults(tester *caddytest.Tester, t *testing.T) { } } -func testACMEServerWithMismatchedChallenges(tester *caddytest.Tester, t *testing.T) { +func TestACMEServerWithMismatchedChallenges(t *testing.T) { ctx := context.Background() logger := caddy.Log().Named("acmez") - tester.MustLoadConfig(` + + tester := caddytest.StartHarness(t) + tester.LoadConfig(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + } + acme.localhost { acme_server { challenges tls-alpn-01 } @@ -115,7 +111,7 @@ func testACMEServerWithMismatchedChallenges(tester *caddytest.Tester, t *testing client := acmez.Client{ Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + HTTPClient: tester.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index f05fb836e15..e3ce29ac094 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -15,8 +15,8 @@ import ( ) func TestACMEServerDirectory(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust local_certs @@ -41,8 +41,8 @@ func TestACMEServerDirectory(t *testing.T) { } func TestACMEServerAllowPolicy(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust local_certs @@ -71,7 +71,7 @@ func TestACMEServerAllowPolicy(t *testing.T) { client := acmez.Client{ Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + HTTPClient: tester.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ @@ -127,8 +127,8 @@ func TestACMEServerAllowPolicy(t *testing.T) { } func TestACMEServerDenyPolicy(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust local_certs @@ -156,7 +156,7 @@ func TestACMEServerDenyPolicy(t *testing.T) { client := acmez.Client{ Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client, + HTTPClient: tester.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ diff --git a/caddytest/integration/autohttps_test.go b/caddytest/integration/autohttps_test.go index 1dbdbcee209..993fa2b9aeb 100644 --- a/caddytest/integration/autohttps_test.go +++ b/caddytest/integration/autohttps_test.go @@ -8,8 +8,8 @@ import ( ) func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 skip_install_trust @@ -24,8 +24,8 @@ func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) { } func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -40,8 +40,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) { } func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -56,8 +56,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T } func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -98,8 +98,8 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) { } func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -123,8 +123,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) { } func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go index 11ffc08aeb1..44c28cd5850 100644 --- a/caddytest/integration/caddyfile_test.go +++ b/caddytest/integration/caddyfile_test.go @@ -10,19 +10,19 @@ import ( func TestRespond(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 https_port 9443 grace_period 1ns } - + localhost:9080 { respond /version 200 { body "hello from localhost" - } + } } `, "caddyfile") @@ -32,22 +32,22 @@ func TestRespond(t *testing.T) { func TestRedirect(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 https_port 9443 grace_period 1ns } - + localhost:9080 { - + redir / http://localhost:9080/hello 301 - + respond /hello 200 { body "hello from localhost" - } + } } `, "caddyfile") @@ -64,8 +64,8 @@ func TestDuplicateHosts(t *testing.T) { ` localhost:9080 { } - - localhost:9080 { + + localhost:9080 { } `, "caddyfile", @@ -80,9 +80,9 @@ func TestReadCookie(t *testing.T) { } // arrange - tester := caddytest.NewTester(t) - tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie}) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.Client().Jar.SetCookies(localhost, []*http.Cookie{&cookie}) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -90,7 +90,7 @@ func TestReadCookie(t *testing.T) { https_port 9443 grace_period 1ns } - + localhost:9080 { templates { root testdata @@ -106,8 +106,8 @@ func TestReadCookie(t *testing.T) { } func TestReplIndex(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -481,9 +481,9 @@ func TestValidPrefix(t *testing.T) { } func TestUriReplace(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -491,16 +491,16 @@ func TestUriReplace(t *testing.T) { :9080 uri replace "\}" %7D uri replace "\{" %7B - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D") } func TestUriOps(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -511,7 +511,7 @@ func TestUriOps(t *testing.T) { uri query taz test uri query key=value example uri query changethis>changed - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest&changethis=val", 200, "changed=val&foo=bar0&foo=bar&key%3Dvalue=example&taz=test") @@ -523,9 +523,9 @@ func TestUriOps(t *testing.T) { // refer to 127.0.0.1 or ::1. // TODO: Test each http version separately (especially http/3) func TestHttpRequestLocalPortPlaceholder(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -537,9 +537,9 @@ func TestHttpRequestLocalPortPlaceholder(t *testing.T) { } func TestSetThenAddQueryParams(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -547,16 +547,16 @@ func TestSetThenAddQueryParams(t *testing.T) { :9080 uri query foo bar uri query +foo baz - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint", 200, "foo=bar&foo=baz") } func TestSetThenDeleteParams(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -564,16 +564,16 @@ func TestSetThenDeleteParams(t *testing.T) { :9080 uri query bar foo{query.foo} uri query -foo - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=foobar") } func TestRenameAndOtherOps(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -582,36 +582,36 @@ func TestRenameAndOtherOps(t *testing.T) { uri query foo>bar uri query bar taz uri query +bar baz - + respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=taz&bar=baz") } func TestReplaceOps(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query foo bar baz + uri query foo bar baz respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=baz") } func TestReplaceWithReplacementPlaceholder(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query foo bar {query.placeholder} + uri query foo bar {query.placeholder} respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=baz&foo=bar", 200, "foo=baz&placeholder=baz") @@ -619,66 +619,66 @@ func TestReplaceWithReplacementPlaceholder(t *testing.T) { } func TestReplaceWithKeyPlaceholder(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query {query.placeholder} bar baz + uri query {query.placeholder} bar baz respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=foo&foo=bar", 200, "foo=baz&placeholder=foo") } func TestPartialReplacement(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query foo ar az + uri query foo ar az respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=baz") } func TestNonExistingSearch(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query foo var baz + uri query foo var baz respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "foo=bar") } func TestReplaceAllOps(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 } :9080 - uri query * bar baz + uri query * bar baz respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar&baz=bar", 200, "baz=baz&foo=baz") } func TestUriOpsBlock(t *testing.T) { - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) - tester.InitServer(` + tester.LoadConfig(` { admin localhost:2999 http_port 9080 @@ -688,15 +688,15 @@ func TestUriOpsBlock(t *testing.T) { +foo bar -baz taz test - } + } respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest", 200, "foo=bar0&foo=bar&taz=test") } func TestHandleErrorSimpleCodes(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ admin localhost:2999 http_port 9080 } @@ -704,7 +704,7 @@ func TestHandleErrorSimpleCodes(t *testing.T) { root * /srv error /private* "Unauthorized" 410 error /hidden* "Not found" 404 - + handle_errors 404 410 { respond "404 or 410 error" } @@ -715,8 +715,8 @@ func TestHandleErrorSimpleCodes(t *testing.T) { } func TestHandleErrorRange(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ admin localhost:2999 http_port 9080 } @@ -735,8 +735,8 @@ func TestHandleErrorRange(t *testing.T) { } func TestHandleErrorSort(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ admin localhost:2999 http_port 9080 } @@ -759,8 +759,8 @@ func TestHandleErrorSort(t *testing.T) { } func TestHandleErrorRangeAndCodes(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ admin localhost:2999 http_port 9080 } diff --git a/caddytest/integration/handler_test.go b/caddytest/integration/handler_test.go index afc700b02bd..62b37939f7d 100644 --- a/caddytest/integration/handler_test.go +++ b/caddytest/integration/handler_test.go @@ -9,8 +9,8 @@ import ( ) func TestBrowse(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -32,8 +32,8 @@ func TestBrowse(t *testing.T) { } func TestRespondWithJSON(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 diff --git a/caddytest/integration/intercept_test.go b/caddytest/integration/intercept_test.go index 81db6a7d639..ef33f1cd8f3 100644 --- a/caddytest/integration/intercept_test.go +++ b/caddytest/integration/intercept_test.go @@ -7,8 +7,8 @@ import ( ) func TestIntercept(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ skip_install_trust admin localhost:2999 http_port 9080 diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go index 4399902eaee..39501fe5b57 100644 --- a/caddytest/integration/leafcertloaders_test.go +++ b/caddytest/integration/leafcertloaders_test.go @@ -7,8 +7,8 @@ import ( ) func TestLeafCertLoaders(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" diff --git a/caddytest/integration/listener_test.go b/caddytest/integration/listener_test.go index 30642b1aed9..b2e327b04c6 100644 --- a/caddytest/integration/listener_test.go +++ b/caddytest/integration/listener_test.go @@ -12,7 +12,7 @@ import ( "github.com/caddyserver/caddy/v2/caddytest" ) -func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.Tester { +func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.TestHarness { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("failed to listen: %s", err) @@ -28,8 +28,8 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy _ = srv.Close() _ = l.Close() }) - tester := caddytest.NewTester(t) - tester.InitServer(fmt.Sprintf(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(fmt.Sprintf(` { skip_install_trust admin localhost:2999 @@ -69,7 +69,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) { writer.WriteHeader(http.StatusNoContent) }) - resp, err := tester.Client.Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body)) + resp, err := tester.Client().Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body)) if err != nil { t.Fatalf("failed to post: %s", err) } @@ -87,7 +87,7 @@ func TestLargeHttpRequest(t *testing.T) { // We never read the body in any way, set an extra long header instead. req, _ := http.NewRequest("POST", "http://localhost:9443", nil) req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024)) - _, err := tester.Client.Do(req) + _, err := tester.Client().Do(req) if err == nil { t.Fatal("not supposed to succeed") } diff --git a/caddytest/integration/map_test.go b/caddytest/integration/map_test.go index eb338656469..c5e7d7d3d58 100644 --- a/caddytest/integration/map_test.go +++ b/caddytest/integration/map_test.go @@ -9,8 +9,8 @@ import ( func TestMap(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ skip_install_trust admin localhost:2999 http_port 9080 @@ -39,8 +39,8 @@ func TestMap(t *testing.T) { func TestMapRespondWithDefault(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ skip_install_trust admin localhost:2999 http_port 9080 @@ -67,8 +67,8 @@ func TestMapRespondWithDefault(t *testing.T) { func TestMapAsJSON(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" diff --git a/caddytest/integration/reverseproxy_test.go b/caddytest/integration/reverseproxy_test.go index cbfe8433bc9..f165c2e5cdc 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -14,8 +14,8 @@ import ( ) func TestSRVReverseProxy(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -87,8 +87,8 @@ func TestDialWithPlaceholderUnix(t *testing.T) { }) runtime.Gosched() // Allow other goroutines to run - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -139,8 +139,8 @@ func TestDialWithPlaceholderUnix(t *testing.T) { } func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -233,8 +233,8 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) { } func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -327,8 +327,8 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { } func TestReverseProxyHealthCheck(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { skip_install_trust admin localhost:2999 @@ -364,7 +364,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) { if runtime.GOOS == "windows" { t.SkipNow() } - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) f, err := os.CreateTemp("", "*.sock") if err != nil { t.Errorf("failed to create TempFile: %s", err) @@ -395,7 +395,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) { }) runtime.Gosched() // Allow other goroutines to run - tester.InitServer(fmt.Sprintf(` + tester.LoadConfig(fmt.Sprintf(` { skip_install_trust admin localhost:2999 @@ -422,7 +422,7 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) { if runtime.GOOS == "windows" { t.SkipNow() } - tester := caddytest.NewTester(t) + tester := caddytest.StartHarness(t) f, err := os.CreateTemp("", "*.sock") if err != nil { t.Errorf("failed to create TempFile: %s", err) @@ -453,7 +453,7 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) { }) runtime.Gosched() // Allow other goroutines to run - tester.InitServer(fmt.Sprintf(` + tester.LoadConfig(fmt.Sprintf(` { skip_install_trust admin localhost:2999 diff --git a/caddytest/integration/sni_test.go b/caddytest/integration/sni_test.go index 188f9354135..7830bf725a4 100644 --- a/caddytest/integration/sni_test.go +++ b/caddytest/integration/sni_test.go @@ -8,8 +8,8 @@ import ( func TestDefaultSNI(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(`{ + tester := caddytest.StartHarness(t) + tester.LoadConfig(`{ "admin": { "listen": "localhost:2999" }, @@ -107,8 +107,8 @@ func TestDefaultSNI(t *testing.T) { func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -211,8 +211,8 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { func TestDefaultSNIWithPortMappingOnly(t *testing.T) { // arrange - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" diff --git a/caddytest/integration/stream_test.go b/caddytest/integration/stream_test.go index d82882d6997..77c9bdfceff 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -20,8 +20,8 @@ import ( // (see https://github.com/caddyserver/caddy/issues/3556 for use case) func TestH2ToH2CStream(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" @@ -204,8 +204,8 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { // (see https://github.com/caddyserver/caddy/issues/3606 for use case) func TestH2ToH1ChunkedResponse(t *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` + tester := caddytest.StartHarness(t) + tester.LoadConfig(` { "admin": { "listen": "localhost:2999" diff --git a/caddytest/testing_harness.go b/caddytest/testing_harness.go new file mode 100644 index 00000000000..581d2f7ed37 --- /dev/null +++ b/caddytest/testing_harness.go @@ -0,0 +1,223 @@ +package caddytest + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +// use the convention to replace /[certificatename].[crt|key] with the full path +// this helps reduce the noise in test configurations and also allow this +// to run in any path +func prependCaddyFilePath(rawConfig string) string { + r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1") + r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1") + return r +} + +var ( + matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`) + matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`) +) + +type TestHarness struct { + t testing.TB + + tester *Tester +} + +// StartHarness creates and starts a test harness environment which spans the lifetime a single caddy instance +// This is used for the integration tests +func StartHarness(t *testing.T) *TestHarness { + if testing.Short() { + t.SkipNow() + return nil + } + o := &TestHarness{t: t} + o.init() + return o +} + +func (tc *TestHarness) Client() *http.Client { + return tc.tester.Client +} + +func (tc *TestHarness) LoadConfig(rawConfig, configType string) { + rawConfig = prependCaddyFilePath(rawConfig) + err := tc.tester.LoadConfig(rawConfig, configType) + require.NoError(tc.t, err) +} + +func (tc *TestHarness) init() { + // start the server + tester, err := NewTester() + if err != nil { + tc.t.Errorf("Failed to create caddy tester: %s", err) + return + } + tc.tester = tester + err = tc.tester.LaunchCaddy() + if err != nil { + tc.t.Errorf("Failed to launch caddy tester: %s", err) + } + // cleanup + tc.t.Cleanup(func() { + func() { + if tc.t.Failed() { + res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + if err != nil { + tc.t.Log("unable to read the current config") + return + } + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + var out bytes.Buffer + _ = json.Indent(&out, body, "", " ") + tc.t.Logf("----------- failed with config -----------\n%s", out.String()) + } + }() + // shutdown server after extracing the config + err = tc.tester.CleanupCaddy() + if err != nil { + tc.t.Errorf("failed to clean up caddy instance: %s", err) + } + }) +} + +// AssertRedirect makes a request and asserts the redirection happens +func (tc *TestHarness) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { + redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // using the existing client, we override the check redirect policy for this test + old := tc.tester.Client.CheckRedirect + tc.tester.Client.CheckRedirect = redirectPolicyFunc + defer func() { tc.tester.Client.CheckRedirect = old }() + + resp, err := tc.tester.Client.Get(requestURI) + if err != nil { + tc.t.Errorf("failed to call server %s", err) + return nil + } + + if expectedStatusCode != resp.StatusCode { + tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode) + } + + loc, err := resp.Location() + if err != nil { + tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err) + } + if loc == nil && expectedToLocation != "" { + tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI) + } + if loc != nil { + if expectedToLocation != loc.String() { + tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String()) + } + } + + return resp +} + +// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions +func (tc *TestHarness) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response { + resp, err := tc.tester.Client.Do(req) + if err != nil { + tc.t.Fatalf("failed to call server %s", err) + } + + if expectedStatusCode != resp.StatusCode { + tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode) + } + + return resp +} + +// AssertResponse request a URI and assert the status code and the body contains a string +func (tc *TestHarness) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) { + resp := tc.AssertResponseCode(req, expectedStatusCode) + + defer resp.Body.Close() + bytes, err := io.ReadAll(resp.Body) + if err != nil { + tc.t.Fatalf("unable to read the response body %s", err) + } + + body := string(bytes) + + if body != expectedBody { + tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body) + } + + return resp, body +} + +// Verb specific test functions + +// AssertGetResponse GET a URI and expect a statusCode and body text +func (tc *TestHarness) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { + req, err := http.NewRequest("GET", requestURI, nil) + if err != nil { + tc.t.Fatalf("unable to create request %s", err) + } + + return tc.AssertResponse(req, expectedStatusCode, expectedBody) +} + +// AssertDeleteResponse request a URI and expect a statusCode and body text +func (tc *TestHarness) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { + req, err := http.NewRequest("DELETE", requestURI, nil) + if err != nil { + tc.t.Fatalf("unable to create request %s", err) + } + + return tc.AssertResponse(req, expectedStatusCode, expectedBody) +} + +// AssertPostResponseBody POST to a URI and assert the response code and body +func (tc *TestHarness) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { + req, err := http.NewRequest("POST", requestURI, requestBody) + if err != nil { + tc.t.Errorf("failed to create request %s", err) + return nil, "" + } + + applyHeaders(tc.t, req, requestHeaders) + + return tc.AssertResponse(req, expectedStatusCode, expectedBody) +} + +// AssertPutResponseBody PUT to a URI and assert the response code and body +func (tc *TestHarness) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { + req, err := http.NewRequest("PUT", requestURI, requestBody) + if err != nil { + tc.t.Errorf("failed to create request %s", err) + return nil, "" + } + + applyHeaders(tc.t, req, requestHeaders) + + return tc.AssertResponse(req, expectedStatusCode, expectedBody) +} + +// AssertPatchResponseBody PATCH to a URI and assert the response code and body +func (tc *TestHarness) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { + req, err := http.NewRequest("PATCH", requestURI, requestBody) + if err != nil { + tc.t.Errorf("failed to create request %s", err) + return nil, "" + } + + applyHeaders(tc.t, req, requestHeaders) + + return tc.AssertResponse(req, expectedStatusCode, expectedBody) +} From b98c89fbb6352d98deb8ace26134d4aa9de37114 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 19:46:43 -0500 Subject: [PATCH 03/14] noot --- caddytest/caddytest_assert.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/caddytest/caddytest_assert.go b/caddytest/caddytest_assert.go index d81764d667e..359538ad314 100644 --- a/caddytest/caddytest_assert.go +++ b/caddytest/caddytest_assert.go @@ -10,12 +10,18 @@ import ( "github.com/aryann/difflib" "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/stretchr/testify/require" ) // AssertLoadError will load a config and expect an error func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { - tc := StartHarness(t) - err := tc.tester.LoadConfig(rawConfig, configType) + tc, err := NewTester() + require.NoError(t, err) + err = tc.LaunchCaddy() + require.NoError(t, err) + defer tc.CleanupCaddy() + + err = tc.LoadConfig(rawConfig, configType) if !strings.Contains(err.Error(), expectedError) { t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) } From 01cb87808708ef047bb82cc8083ef19708ea3f54 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 20:08:38 -0500 Subject: [PATCH 04/14] noot --- caddy.go | 2 +- caddytest/caddytest.go | 75 +++++++++-------------------------- caddytest/caddytest_assert.go | 3 +- caddytest/testing_harness.go | 16 +++++++- 4 files changed, 36 insertions(+), 60 deletions(-) diff --git a/caddy.go b/caddy.go index 9aba97fa415..2c18053a5c9 100644 --- a/caddy.go +++ b/caddy.go @@ -780,7 +780,7 @@ func exitProcess(ctx context.Context, logger *zap.Logger) { logger.Error("unclean shutdown") } // check if we are in test environment, and dont call exit if we are - if flag.Lookup("test.v") == nil || strings.Contains(os.Args[0], ".test") { + if flag.Lookup("test.v") == nil || !strings.Contains(os.Args[0], ".test") { os.Exit(exitCode) } }() diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index f22a09ef25e..44ae991eeb8 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -4,7 +4,6 @@ import ( "context" "crypto/tls" "encoding/json" - "errors" "fmt" "io" "log" @@ -12,15 +11,11 @@ import ( "net/http" "net/http/cookiejar" "os" - "path" - "reflect" - "runtime" "strings" "time" 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" ) @@ -96,7 +91,7 @@ func (tc *Tester) CleanupCaddy() error { }() _, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", Default.AdminPort), "", nil) if err != nil { - return fmt.Errorf("couldn't stop caddytest server") + return fmt.Errorf("couldn't stop caddytest server: %w", err) } time.Sleep(200 * time.Millisecond) for retries := 0; retries < 10; retries++ { @@ -112,7 +107,6 @@ func (tc *Tester) CleanupCaddy() error { // LoadConfig loads the config to the tester server and also ensures that the config was loaded func (tc *Tester) LoadConfig(rawConfig string, configType string) error { - originalRawConfig := rawConfig // normalize JSON config if configType == "json" { var conf any @@ -157,56 +151,32 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { } tc.configLoaded = true - return tc.ensureConfigRunning(originalRawConfig, configType) + + // 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(rawConfig) - if configType != "json" { - adapter := caddyconfig.GetAdapter(configType) - if adapter == nil { - return fmt.Errorf("adapter of config type is missing: %s", configType) - } - expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil) +func (tc *Tester) GetCurrentConfig(receiver any) error { + client := &http.Client{ + Timeout: Default.LoadRequestTimeout, } - var expected any - err := json.Unmarshal(expectedBytes, &expected) + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) if err != nil { return err } - - client := &http.Client{ - Timeout: Default.LoadRequestTimeout, - } - - fetchConfig := func(client *http.Client) any { - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) - if err != nil { - return nil - } - defer resp.Body.Close() - actualBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil - } - var actual any - err = json.Unmarshal(actualBytes, &actual) - if err != nil { - return nil - } - return actual + defer resp.Body.Close() + actualBytes, err := io.ReadAll(resp.Body) + if err != nil { + return err } - - // TODO: does this really need to be tried more than once? - // Caddy should block until the new config is loaded, which means needing to wait is a caddy bug - for retries := 3; retries > 0; retries-- { - if reflect.DeepEqual(expected, fetchConfig(client)) { - return nil - } - time.Sleep(1 * time.Second) + err = json.Unmarshal(actualBytes, receiver) + if err != nil { + return err } - return errors.New("EnsureConfigRunning: POSTed configuration isn't active") + return nil } const initConfig = `{ @@ -258,15 +228,6 @@ func isCaddyAdminRunning() 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) -} - // CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally func CreateTestingTransport() *http.Transport { dialer := net.Dialer{ diff --git a/caddytest/caddytest_assert.go b/caddytest/caddytest_assert.go index 359538ad314..cfb019356aa 100644 --- a/caddytest/caddytest_assert.go +++ b/caddytest/caddytest_assert.go @@ -19,12 +19,13 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected require.NoError(t, err) err = tc.LaunchCaddy() require.NoError(t, err) - defer tc.CleanupCaddy() err = tc.LoadConfig(rawConfig, configType) if !strings.Contains(err.Error(), expectedError) { t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) } + err = tc.CleanupCaddy() + require.NoError(t, err) } // CompareAdapt adapts a config and then compares it against an expected result diff --git a/caddytest/testing_harness.go b/caddytest/testing_harness.go index 581d2f7ed37..f1743097b4b 100644 --- a/caddytest/testing_harness.go +++ b/caddytest/testing_harness.go @@ -6,7 +6,9 @@ import ( "fmt" "io" "net/http" + "path" "regexp" + "runtime" "testing" "github.com/stretchr/testify/require" @@ -21,6 +23,15 @@ func prependCaddyFilePath(rawConfig string) string { 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)`) @@ -64,7 +75,9 @@ func (tc *TestHarness) init() { tc.tester = tester err = tc.tester.LaunchCaddy() if err != nil { - tc.t.Errorf("Failed to launch caddy tester: %s", err) + tc.t.Errorf("Failed to launch caddy server: %s", err) + tc.t.FailNow() + return } // cleanup tc.t.Cleanup(func() { @@ -87,6 +100,7 @@ func (tc *TestHarness) init() { err = tc.tester.CleanupCaddy() if err != nil { tc.t.Errorf("failed to clean up caddy instance: %s", err) + tc.t.FailNow() } }) } From b491fc5d6ce68eb4630e950fa514575965172682 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 20:11:56 -0500 Subject: [PATCH 05/14] noot --- caddy.go | 2 +- cmd/commandfuncs.go | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/caddy.go b/caddy.go index 2c18053a5c9..e7afc13b86e 100644 --- a/caddy.go +++ b/caddy.go @@ -780,7 +780,7 @@ func exitProcess(ctx context.Context, logger *zap.Logger) { logger.Error("unclean shutdown") } // check if we are in test environment, and dont call exit if we are - if flag.Lookup("test.v") == nil || !strings.Contains(os.Args[0], ".test") { + if flag.Lookup("test.v") == nil && !strings.Contains(os.Args[0], ".test") { os.Exit(exitCode) } }() diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 746cf3da6b6..bdee24d11a8 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -20,6 +20,7 @@ import ( "crypto/rand" "encoding/json" "errors" + "flag" "fmt" "io" "io/fs" @@ -257,6 +258,7 @@ func cmdRun(fl Flags) (int, error) { // if enabled, reload config file automatically on changes // (this better only be used in dev!) + // do not enable this during tests, it will cause leaks if watchFlag { go watchConfigFile(configFile, configAdapterFlag) } @@ -280,7 +282,11 @@ func cmdRun(fl Flags) (int, error) { } } - select {} + if flag.Lookup("test.v") == nil || !strings.Contains(os.Args[0], ".test") { + return caddy.ExitCodeSuccess, nil + } else { + select {} + } } func cmdStop(fl Flags) (int, error) { From 41a4320fd34010e53835f4e26f5729aa5f220545 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 20:14:51 -0500 Subject: [PATCH 06/14] noot --- caddytest/caddytest.go | 5 ++--- caddytest/caddytest_assert.go | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 44ae991eeb8..a1ec970482f 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -93,7 +93,6 @@ func (tc *Tester) CleanupCaddy() error { if err != nil { return fmt.Errorf("couldn't stop caddytest server: %w", err) } - time.Sleep(200 * time.Millisecond) for retries := 0; retries < 10; retries++ { if isCaddyAdminRunning() != nil { return nil @@ -206,8 +205,8 @@ func (tc *Tester) startServer() error { caddycmd.Main() }() // wait for caddy admin api to start. it should happen quickly. - for retries := 3; retries > 0 && isCaddyAdminRunning() != nil; retries-- { - time.Sleep(1 * time.Second) + for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { + time.Sleep(100 * time.Millisecond) } // one more time to return the error diff --git a/caddytest/caddytest_assert.go b/caddytest/caddytest_assert.go index cfb019356aa..8587abaedd9 100644 --- a/caddytest/caddytest_assert.go +++ b/caddytest/caddytest_assert.go @@ -24,8 +24,7 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected if !strings.Contains(err.Error(), expectedError) { t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) } - err = tc.CleanupCaddy() - require.NoError(t, err) + tc.CleanupCaddy() } // CompareAdapt adapts a config and then compares it against an expected result From b19feec6dcb465ceeb15241e5dc251f6231f6d29 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 21:01:15 -0500 Subject: [PATCH 07/14] noot --- cmd/commandfuncs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index bdee24d11a8..8de079bfe4b 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -283,9 +283,9 @@ func cmdRun(fl Flags) (int, error) { } if flag.Lookup("test.v") == nil || !strings.Contains(os.Args[0], ".test") { - return caddy.ExitCodeSuccess, nil - } else { select {} + } else { + return caddy.ExitCodeSuccess, nil } } From 841fe2544d7e0fe7cf938c818cd6c88085ce5879 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 21:17:51 -0500 Subject: [PATCH 08/14] noot --- caddytest/caddytest.go | 3 +-- cmd/main.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index a1ec970482f..3de5b96b018 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -200,9 +200,8 @@ func (tc *Tester) startServer() error { } // start inprocess caddy server - os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"} go func() { - caddycmd.Main() + caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile") }() // wait for caddy admin api to start. it should happen quickly. for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { diff --git a/cmd/main.go b/cmd/main.go index 3c3ae627087..3e0359334a4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,6 +34,7 @@ import ( "time" "github.com/caddyserver/certmagic" + "github.com/spf13/cobra" "github.com/spf13/pflag" "go.uber.org/automaxprocs/maxprocs" "go.uber.org/zap" @@ -81,6 +82,24 @@ 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(). + tmpRootCmp := cobra.Command{ + Use: rootCmd.Use, + Long: rootCmd.Long, + Example: rootCmd.Example, + SilenceUsage: rootCmd.SilenceUsage, + Version: rootCmd.Version, + } + tmpRootCmp.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 { From 926fb82f6b65fc34193f8cba46ede8ec9e34d3e2 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 21:18:07 -0500 Subject: [PATCH 09/14] noot --- caddytest/caddytest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 3de5b96b018..5585a934341 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -201,7 +201,7 @@ func (tc *Tester) startServer() error { // start inprocess caddy server go func() { - caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile") + caddycmd.MainForTesting("caddy", "run", "--config", tc.configFileName, "--adapter", "caddyfile") }() // wait for caddy admin api to start. it should happen quickly. for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { From edf4168c8e3e9659612d4e8da71944769dae0391 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 21:18:38 -0500 Subject: [PATCH 10/14] noot --- caddytest/caddytest.go | 2 +- cmd/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 5585a934341..3de5b96b018 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -201,7 +201,7 @@ func (tc *Tester) startServer() error { // start inprocess caddy server go func() { - caddycmd.MainForTesting("caddy", "run", "--config", tc.configFileName, "--adapter", "caddyfile") + caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile") }() // wait for caddy admin api to start. it should happen quickly. for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { diff --git a/cmd/main.go b/cmd/main.go index 3e0359334a4..4526b26b665 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -94,7 +94,7 @@ func MainForTesting(args ...string) error { Version: rootCmd.Version, } tmpRootCmp.SetArgs(args) - if err := rootCmd.Execute(); err != nil { + if err := tmpRootCmp.Execute(); err != nil { return err } return nil From 7bc7e1680e79863f97bc60be44d9d0199fdda2a0 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 21:57:46 -0500 Subject: [PATCH 11/14] noot --- cmd/cobra.go | 26 ++++++++++++--------- cmd/commandfactory.go | 28 +++++++++++++++++++++++ cmd/commands.go | 53 +++++++++++++++++++++++-------------------- cmd/main.go | 15 ++++-------- 4 files changed, 76 insertions(+), 46 deletions(-) create mode 100644 cmd/commandfactory.go diff --git a/cmd/cobra.go b/cmd/cobra.go index 1a2509206a8..5323d2ec57e 100644 --- a/cmd/cobra.go +++ b/cmd/cobra.go @@ -8,9 +8,10 @@ import ( "github.com/caddyserver/caddy/v2" ) -var rootCmd = &cobra.Command{ - Use: "caddy", - Long: `Caddy is an extensible server platform written in Go. +var defaultFactory = NewRootCommandFactory(func() *cobra.Command { + return &cobra.Command{ + Use: "caddy", + Long: `Caddy is an extensible server platform written in Go. At its core, Caddy merely manages configuration. Modules are plugged in statically at compile-time to provide useful functionality. Caddy's @@ -91,23 +92,26 @@ package installers: https://caddyserver.com/docs/install Instructions for running Caddy in production are also available: https://caddyserver.com/docs/running `, - Example: ` $ caddy run + Example: ` $ caddy run $ caddy run --config caddy.json $ caddy reload --config caddy.json $ caddy stop`, - // kind of annoying to have all the help text printed out if - // caddy has an error provisioning its modules, for instance... - SilenceUsage: true, - Version: onlyVersionText(), -} + // kind of annoying to have all the help text printed out if + // caddy has an error provisioning its modules, for instance... + SilenceUsage: true, + Version: onlyVersionText(), + } +}) const fullDocsFooter = `Full documentation is available at: https://caddyserver.com/docs/command-line` func init() { - rootCmd.SetVersionTemplate("{{.Version}}\n") - rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") + defaultFactory.Use(func(cmd *cobra.Command) { + cmd.SetVersionTemplate("{{.Version}}\n") + cmd.SetHelpTemplate(cmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") + }) } func onlyVersionText() string { diff --git a/cmd/commandfactory.go b/cmd/commandfactory.go new file mode 100644 index 00000000000..49a38a4e194 --- /dev/null +++ b/cmd/commandfactory.go @@ -0,0 +1,28 @@ +package caddycmd + +import ( + "github.com/spf13/cobra" +) + +type RootCommandFactory struct { + constructor func() *cobra.Command + options []func(*cobra.Command) +} + +func NewRootCommandFactory(fn func() *cobra.Command) *RootCommandFactory { + return &RootCommandFactory{ + constructor: fn, + } +} + +func (f *RootCommandFactory) Use(fn func(cmd *cobra.Command)) { + f.options = append(f.options, fn) +} + +func (f *RootCommandFactory) Build() *cobra.Command { + o := f.constructor() + for _, v := range f.options { + v(o) + } + return o +} diff --git a/cmd/commands.go b/cmd/commands.go index e5e1265e441..7e7af1c77d0 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -459,7 +459,8 @@ argument of --directory. If the directory does not exist, it will be created. if err := os.MkdirAll(dir, 0o755); err != nil { return caddy.ExitCodeFailedQuit, err } - if err := doc.GenManTree(rootCmd, &doc.GenManHeader{ + ccmd := defaultFactory.Build() + if err := doc.GenManTree(ccmd, &doc.GenManHeader{ Title: "Caddy", Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections }, dir); err != nil { @@ -471,10 +472,11 @@ argument of --directory. If the directory does not exist, it will be created. }) // source: https://github.com/spf13/cobra/blob/main/shell_completions.md - rootCmd.AddCommand(&cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Long: fmt.Sprintf(`To load completions: + defaultFactory.Use(func(ccmd *cobra.Command) { + ccmd.AddCommand(&cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate completion script", + Long: fmt.Sprintf(`To load completions: Bash: @@ -512,24 +514,25 @@ argument of --directory. If the directory does not exist, it will be created. # To load completions for every new session, run: PS> %[1]s completion powershell > %[1]s.ps1 # and source this file from your PowerShell profile. - `, rootCmd.Root().Name()), - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), - RunE: func(cmd *cobra.Command, args []string) error { - switch args[0] { - case "bash": - return cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - return cmd.Root().GenZshCompletion(os.Stdout) - case "fish": - return cmd.Root().GenFishCompletion(os.Stdout, true) - case "powershell": - return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - default: - return fmt.Errorf("unrecognized shell: %s", args[0]) - } - }, + `, defaultFactory.constructor().Name()), + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: func(cmd *cobra.Command, args []string) error { + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + return cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + return cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + default: + return fmt.Errorf("unrecognized shell: %s", args[0]) + } + }, + }) }) } @@ -563,7 +566,9 @@ func RegisterCommand(cmd Command) { if !commandNameRegex.MatchString(cmd.Name) { panic("invalid command name") } - rootCmd.AddCommand(caddyCmdToCobra(cmd)) + defaultFactory.Use(func(ccmd *cobra.Command) { + ccmd.AddCommand(caddyCmdToCobra(cmd)) + }) } var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`) diff --git a/cmd/main.go b/cmd/main.go index 4526b26b665..6defac7568b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,7 +34,6 @@ import ( "time" "github.com/caddyserver/certmagic" - "github.com/spf13/cobra" "github.com/spf13/pflag" "go.uber.org/automaxprocs/maxprocs" "go.uber.org/zap" @@ -72,7 +71,7 @@ func Main() { if err != nil { caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err)) } - + rootCmd := defaultFactory.Build() if err := rootCmd.Execute(); err != nil { var exitError *exitError if errors.As(err, &exitError) { @@ -86,15 +85,9 @@ func Main() { func MainForTesting(args ...string) error { // create a root command for testing which will not pollute the global namespace, and does not // call os.Exit(). - tmpRootCmp := cobra.Command{ - Use: rootCmd.Use, - Long: rootCmd.Long, - Example: rootCmd.Example, - SilenceUsage: rootCmd.SilenceUsage, - Version: rootCmd.Version, - } - tmpRootCmp.SetArgs(args) - if err := tmpRootCmp.Execute(); err != nil { + rootCmd := defaultFactory.Build() + rootCmd.SetArgs(args) + if err := rootCmd.Execute(); err != nil { return err } return nil From c0d9a2383e7c6b3f370605834fc9cf62ba3753eb Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 22:36:02 -0500 Subject: [PATCH 12/14] noot --- caddytest/caddytest.go | 91 +++++++++++++------ caddytest/caddytest_assert.go | 5 +- caddytest/caddytest_test.go | 2 +- caddytest/integration/acme_test.go | 4 +- caddytest/integration/acmeserver_test.go | 6 +- caddytest/integration/autohttps_test.go | 12 +-- caddytest/integration/caddyfile_test.go | 43 +++++---- caddytest/integration/handler_test.go | 4 +- caddytest/integration/intercept_test.go | 2 +- caddytest/integration/leafcertloaders_test.go | 2 +- caddytest/integration/listener_test.go | 2 +- caddytest/integration/map_test.go | 6 +- caddytest/integration/reverseproxy_test.go | 14 +-- caddytest/integration/sni_test.go | 6 +- caddytest/integration/stream_test.go | 4 +- caddytest/testing_harness.go | 2 +- 16 files changed, 121 insertions(+), 84 deletions(-) diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 3de5b96b018..9a89d6b1e95 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/cookiejar" "os" + "strconv" "strings" "time" @@ -22,8 +23,6 @@ import ( // Defaults store any configuration required to make the tests run type Defaults struct { - // Port we expect caddy to listening on - AdminPort int // Certificates we expect to be loaded before attempting to run the tests Certificates []string // TestRequestTimeout is the time to wait for a http request to @@ -34,7 +33,6 @@ type Defaults struct { // Default testing values var Default = Defaults{ - AdminPort: 2999, // different from what a real server also running on a developer's machine might be Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"}, TestRequestTimeout: 5 * time.Second, LoadRequestTimeout: 5 * time.Second, @@ -42,9 +40,12 @@ var Default = Defaults{ // Tester represents an instance of a test client. type Tester struct { - Client *http.Client + Client *http.Client + + adminPort int configLoaded bool configFileName string + envFileName string } // NewTester will create a new testing client with an attached cookie jar @@ -86,26 +87,37 @@ func (tc *Tester) LaunchCaddy() error { func (tc *Tester) CleanupCaddy() error { // now shutdown the server, since the test is done. defer func() { - // try to remove the tmp config file we created - os.Remove(tc.configFileName) + // try to remove pthe tmp config file we created + if tc.configFileName != "" { + os.Remove(tc.configFileName) + } + if tc.envFileName != "" { + os.Remove(tc.envFileName) + } }() - _, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", Default.AdminPort), "", nil) + resp, err := http.Post(fmt.Sprintf("http://localhost:%d/stop", tc.adminPort), "", nil) if err != nil { return fmt.Errorf("couldn't stop caddytest server: %w", err) } + resp.Body.Close() for retries := 0; retries < 10; retries++ { - if isCaddyAdminRunning() != nil { + if tc.isCaddyAdminRunning() != nil { return nil } time.Sleep(100 * time.Millisecond) } return fmt.Errorf("timed out waiting for caddytest server to stop") - } // LoadConfig loads the config to the tester server and also ensures that the config was loaded +// it should not be run func (tc *Tester) LoadConfig(rawConfig string, configType string) error { + if tc.adminPort == 0 { + return fmt.Errorf("load config called where startServer didnt succeed") + } + // replace special testing placeholders so we can have our admin api be on a random port + rawConfig = strings.ReplaceAll(rawConfig, "{$TESTING_ADMIN_API}", fmt.Sprintf("localhost:%d", tc.adminPort)) // normalize JSON config if configType == "json" { var conf any @@ -122,7 +134,7 @@ func (tc *Tester) LoadConfig(rawConfig string, configType string) error { Timeout: Default.LoadRequestTimeout, } start := time.Now() - req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig)) + req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.adminPort), strings.NewReader(rawConfig)) if err != nil { return fmt.Errorf("failed to create request. %w", err) } @@ -162,7 +174,7 @@ func (tc *Tester) GetCurrentConfig(receiver any) error { Timeout: Default.LoadRequestTimeout, } - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort)) if err != nil { return err } @@ -178,48 +190,73 @@ func (tc *Tester) GetCurrentConfig(receiver any) error { return nil } -const initConfig = `{ - admin localhost:2999 +func getFreePort() (int, error) { + lr, err := net.Listen("tcp", "localhost:0") + if err != nil { + return 0, err + } + port := strings.Split(lr.Addr().String(), ":") + if len(port) < 2 { + return 0, fmt.Errorf("no port available") + } + i, err := strconv.Atoi(port[1]) + if err != nil { + return 0, err + } + err = lr.Close() + if err != nil { + return 0, fmt.Errorf("failed to close listener: %w", err) + } + return i, nil } -` // launches caddy, and then ensures the Caddy sub-process is running. func (tc *Tester) startServer() error { - if isCaddyAdminRunning() == nil { + if tc.isCaddyAdminRunning() == nil { return fmt.Errorf("caddy test admin port still in use") } - // setup the init config file, and set the cleanup afterwards - f, err := os.CreateTemp("", "") + a, err := getFreePort() if err != nil { - return err + return fmt.Errorf("could not find a open port to listen on: %w", err) } - tc.configFileName = f.Name() + tc.adminPort = a + // setup the init config file, and set the cleanup afterwards + { + f, err := os.CreateTemp("", "") + if err != nil { + return err + } + tc.configFileName = f.Name() - if _, err := f.WriteString(initConfig); err != nil { - return err + initConfig := fmt.Sprintf(`{ + admin localhost:%d +}`, a) + if _, err := f.WriteString(initConfig); err != nil { + return err + } } // start inprocess caddy server go func() { - caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile") + _ = caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile") }() // wait for caddy admin api to start. it should happen quickly. - for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { + for retries := 10; retries > 0 && tc.isCaddyAdminRunning() != nil; retries-- { time.Sleep(100 * time.Millisecond) } // one more time to return the error - return isCaddyAdminRunning() + return tc.isCaddyAdminRunning() } -func isCaddyAdminRunning() error { +func (tc *Tester) isCaddyAdminRunning() error { // assert that caddy is running client := &http.Client{ Timeout: Default.LoadRequestTimeout, } - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort)) if err != nil { - return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort) + return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.adminPort) } resp.Body.Close() diff --git a/caddytest/caddytest_assert.go b/caddytest/caddytest_assert.go index 8587abaedd9..23054424d87 100644 --- a/caddytest/caddytest_assert.go +++ b/caddytest/caddytest_assert.go @@ -9,8 +9,9 @@ import ( "testing" "github.com/aryann/difflib" - "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/stretchr/testify/require" + + "github.com/caddyserver/caddy/v2/caddyconfig" ) // AssertLoadError will load a config and expect an error @@ -24,7 +25,7 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected if !strings.Contains(err.Error(), expectedError) { t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error()) } - tc.CleanupCaddy() + _ = tc.CleanupCaddy() } // CompareAdapt adapts a config and then compares it against an expected result diff --git a/caddytest/caddytest_test.go b/caddytest/caddytest_test.go index 589c33ea6dc..1cca9ff0fb0 100644 --- a/caddytest/caddytest_test.go +++ b/caddytest/caddytest_test.go @@ -68,7 +68,7 @@ func TestLoadUnorderedJSON(t *testing.T) { } }, "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "pki": { diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index 9697cf9551b..8cd3220dfc7 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -28,7 +28,7 @@ func TestACMEServerWithDefaults(t *testing.T) { tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 local_certs @@ -96,7 +96,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) { tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 local_certs diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index e3ce29ac094..604a59f76a5 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -20,7 +20,7 @@ func TestACMEServerDirectory(t *testing.T) { { skip_install_trust local_certs - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 pki { @@ -46,7 +46,7 @@ func TestACMEServerAllowPolicy(t *testing.T) { { skip_install_trust local_certs - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 pki { @@ -132,7 +132,7 @@ func TestACMEServerDenyPolicy(t *testing.T) { { skip_install_trust local_certs - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 pki { diff --git a/caddytest/integration/autohttps_test.go b/caddytest/integration/autohttps_test.go index 993fa2b9aeb..89070b29e78 100644 --- a/caddytest/integration/autohttps_test.go +++ b/caddytest/integration/autohttps_test.go @@ -11,7 +11,7 @@ func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} skip_install_trust http_port 9080 https_port 9443 @@ -28,7 +28,7 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) { tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 } @@ -44,7 +44,7 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 } @@ -60,7 +60,7 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "http": { @@ -102,7 +102,7 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) { tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 local_certs @@ -127,7 +127,7 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 local_certs diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go index 44c28cd5850..647d6b7ce0f 100644 --- a/caddytest/integration/caddyfile_test.go +++ b/caddytest/integration/caddyfile_test.go @@ -13,7 +13,7 @@ func TestRespond(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns @@ -35,7 +35,7 @@ func TestRedirect(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns @@ -85,7 +85,7 @@ func TestReadCookie(t *testing.T) { tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns @@ -110,7 +110,7 @@ func TestReplIndex(t *testing.T) { tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns @@ -485,7 +485,7 @@ func TestUriReplace(t *testing.T) { tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -502,7 +502,7 @@ func TestUriOps(t *testing.T) { tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -527,7 +527,7 @@ func TestHttpRequestLocalPortPlaceholder(t *testing.T) { tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -541,7 +541,7 @@ func TestSetThenAddQueryParams(t *testing.T) { tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -558,7 +558,7 @@ func TestSetThenDeleteParams(t *testing.T) { tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -575,7 +575,7 @@ func TestRenameAndOtherOps(t *testing.T) { tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -593,7 +593,7 @@ func TestReplaceOps(t *testing.T) { tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -607,7 +607,7 @@ func TestReplaceWithReplacementPlaceholder(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -615,14 +615,13 @@ func TestReplaceWithReplacementPlaceholder(t *testing.T) { respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=baz&foo=bar", 200, "foo=baz&placeholder=baz") - } func TestReplaceWithKeyPlaceholder(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -636,7 +635,7 @@ func TestPartialReplacement(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -650,7 +649,7 @@ func TestNonExistingSearch(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -665,7 +664,7 @@ func TestReplaceAllOps(t *testing.T) { tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -680,7 +679,7 @@ func TestUriOpsBlock(t *testing.T) { tester.LoadConfig(` { - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } :9080 @@ -697,7 +696,7 @@ func TestUriOpsBlock(t *testing.T) { func TestHandleErrorSimpleCodes(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(`{ - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } localhost:9080 { @@ -717,7 +716,7 @@ func TestHandleErrorSimpleCodes(t *testing.T) { func TestHandleErrorRange(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(`{ - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } localhost:9080 { @@ -737,7 +736,7 @@ func TestHandleErrorRange(t *testing.T) { func TestHandleErrorSort(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(`{ - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } localhost:9080 { @@ -761,7 +760,7 @@ func TestHandleErrorSort(t *testing.T) { func TestHandleErrorRangeAndCodes(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(`{ - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 } localhost:9080 { diff --git a/caddytest/integration/handler_test.go b/caddytest/integration/handler_test.go index 62b37939f7d..11d852420f1 100644 --- a/caddytest/integration/handler_test.go +++ b/caddytest/integration/handler_test.go @@ -13,7 +13,7 @@ func TestBrowse(t *testing.T) { tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns @@ -36,7 +36,7 @@ func TestRespondWithJSON(t *testing.T) { tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns diff --git a/caddytest/integration/intercept_test.go b/caddytest/integration/intercept_test.go index ef33f1cd8f3..2e4d780caf6 100644 --- a/caddytest/integration/intercept_test.go +++ b/caddytest/integration/intercept_test.go @@ -10,7 +10,7 @@ func TestIntercept(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(`{ skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go index 39501fe5b57..90c396f48ce 100644 --- a/caddytest/integration/leafcertloaders_test.go +++ b/caddytest/integration/leafcertloaders_test.go @@ -11,7 +11,7 @@ func TestLeafCertLoaders(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "http": { diff --git a/caddytest/integration/listener_test.go b/caddytest/integration/listener_test.go index b2e327b04c6..24b86509359 100644 --- a/caddytest/integration/listener_test.go +++ b/caddytest/integration/listener_test.go @@ -32,7 +32,7 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy tester.LoadConfig(fmt.Sprintf(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 local_certs diff --git a/caddytest/integration/map_test.go b/caddytest/integration/map_test.go index c5e7d7d3d58..ac20af45da0 100644 --- a/caddytest/integration/map_test.go +++ b/caddytest/integration/map_test.go @@ -12,7 +12,7 @@ func TestMap(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(`{ skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns @@ -42,7 +42,7 @@ func TestMapRespondWithDefault(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(`{ skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 } @@ -71,7 +71,7 @@ func TestMapAsJSON(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "pki": { diff --git a/caddytest/integration/reverseproxy_test.go b/caddytest/integration/reverseproxy_test.go index f165c2e5cdc..fdb742e8cac 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -18,7 +18,7 @@ func TestSRVReverseProxy(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "pki": { @@ -91,7 +91,7 @@ func TestDialWithPlaceholderUnix(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "pki": { @@ -143,7 +143,7 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "pki": { @@ -237,7 +237,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "pki": { @@ -331,7 +331,7 @@ func TestReverseProxyHealthCheck(t *testing.T) { tester.LoadConfig(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns @@ -398,7 +398,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) { tester.LoadConfig(fmt.Sprintf(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns @@ -456,7 +456,7 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) { tester.LoadConfig(fmt.Sprintf(` { skip_install_trust - admin localhost:2999 + admin {$TESTING_ADMIN_API} http_port 9080 https_port 9443 grace_period 1ns diff --git a/caddytest/integration/sni_test.go b/caddytest/integration/sni_test.go index 7830bf725a4..e5aced6e997 100644 --- a/caddytest/integration/sni_test.go +++ b/caddytest/integration/sni_test.go @@ -11,7 +11,7 @@ func TestDefaultSNI(t *testing.T) { tester := caddytest.StartHarness(t) tester.LoadConfig(`{ "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "http": { @@ -111,7 +111,7 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "http": { @@ -215,7 +215,7 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "http": { diff --git a/caddytest/integration/stream_test.go b/caddytest/integration/stream_test.go index 77c9bdfceff..e47697793c1 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -24,7 +24,7 @@ func TestH2ToH2CStream(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "http": { @@ -208,7 +208,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { tester.LoadConfig(` { "admin": { - "listen": "localhost:2999" + "listen": "{$TESTING_ADMIN_API}" }, "apps": { "http": { diff --git a/caddytest/testing_harness.go b/caddytest/testing_harness.go index f1743097b4b..c5af3f6f547 100644 --- a/caddytest/testing_harness.go +++ b/caddytest/testing_harness.go @@ -83,7 +83,7 @@ func (tc *TestHarness) init() { tc.t.Cleanup(func() { func() { if tc.t.Failed() { - res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.tester.adminPort)) if err != nil { tc.t.Log("unable to read the current config") return From 3c591ecac96a4236d96dcb377ed542a818ff5165 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 23:45:54 -0500 Subject: [PATCH 13/14] noot --- caddytest/caddytest.go | 39 ++- caddytest/caddytest_test.go | 21 +- caddytest/integration/acme_test.go | 28 +- caddytest/integration/acmeserver_test.go | 49 +-- caddytest/integration/autohttps_test.go | 93 +++--- caddytest/integration/caddyfile_test.go | 301 ++++++++++-------- caddytest/integration/handler_test.go | 29 +- caddytest/integration/intercept_test.go | 21 +- caddytest/integration/leafcertloaders_test.go | 12 +- caddytest/integration/listener_test.go | 24 +- caddytest/integration/map_test.go | 61 ++-- caddytest/integration/reverseproxy_test.go | 92 +++--- caddytest/integration/sni_test.go | 46 +-- caddytest/integration/stream_test.go | 48 +-- caddytest/testing_harness.go | 4 + 15 files changed, 470 insertions(+), 398 deletions(-) diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 9a89d6b1e95..58cbfa06b13 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -13,6 +13,7 @@ import ( "os" "strconv" "strings" + "sync/atomic" "time" caddycmd "github.com/caddyserver/caddy/v2/cmd" @@ -42,7 +43,12 @@ var Default = Defaults{ type Tester struct { Client *http.Client - adminPort int + adminPort int + + portOne int + portTwo int + + started atomic.Bool configLoaded bool configFileName string envFileName string @@ -78,6 +84,9 @@ func timeElapsed(start time.Time, name string) { // 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.startServer(); err != nil { return fmt.Errorf("failed to start server: %w", err) } @@ -110,14 +119,32 @@ func (tc *Tester) CleanupCaddy() error { return fmt.Errorf("timed out waiting for caddytest server to stop") } +func (tc *Tester) AdminPort() int { + return tc.adminPort +} +func (tc *Tester) PortOne() int { + return tc.portOne +} +func (tc *Tester) PortTwo() int { + return tc.portTwo +} + +func (tc *Tester) ReplaceTestingPlaceholders(x string) string { + x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_BIND}", fmt.Sprintf("localhost:%d", tc.adminPort)) + x = strings.ReplaceAll(x, "{$TESTING_CADDY_ADMIN_PORT}", fmt.Sprintf("%d", tc.adminPort)) + x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_ONE}", fmt.Sprintf("%d", tc.portOne)) + x = strings.ReplaceAll(x, "{$TESTING_CADDY_PORT_TWO}", fmt.Sprintf("%d", tc.portTwo)) + return x +} + // LoadConfig loads the config to the tester server and also ensures that the config was loaded // it should not be run func (tc *Tester) LoadConfig(rawConfig string, configType string) error { if tc.adminPort == 0 { return fmt.Errorf("load config called where startServer didnt succeed") } + rawConfig = tc.ReplaceTestingPlaceholders(rawConfig) // replace special testing placeholders so we can have our admin api be on a random port - rawConfig = strings.ReplaceAll(rawConfig, "{$TESTING_ADMIN_API}", fmt.Sprintf("localhost:%d", tc.adminPort)) // normalize JSON config if configType == "json" { var conf any @@ -220,6 +247,14 @@ func (tc *Tester) startServer() error { return fmt.Errorf("could not find a open port to listen on: %w", err) } tc.adminPort = a + tc.portOne, err = getFreePort() + if err != nil { + return fmt.Errorf("could not find a open portOne: %w", err) + } + tc.portTwo, err = getFreePort() + if err != nil { + return fmt.Errorf("could not find a open portOne: %w", err) + } // setup the init config file, and set the cleanup afterwards { f, err := os.CreateTemp("", "") diff --git a/caddytest/caddytest_test.go b/caddytest/caddytest_test.go index 1cca9ff0fb0..d788a810b5e 100644 --- a/caddytest/caddytest_test.go +++ b/caddytest/caddytest_test.go @@ -1,13 +1,14 @@ 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 { } @@ -34,8 +35,8 @@ func TestReplaceCertificatePaths(t *testing.T) { } func TestLoadUnorderedJSON(t *testing.T) { - tester := StartHarness(t) - tester.LoadConfig(` + harness := StartHarness(t) + harness.LoadConfig(` { "logging": { "logs": { @@ -68,7 +69,7 @@ func TestLoadUnorderedJSON(t *testing.T) { } }, "admin": { - "listen": "{$TESTING_ADMIN_API}" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "pki": { @@ -79,13 +80,13 @@ func TestLoadUnorderedJSON(t *testing.T) { } }, "http": { - "http_port": 9080, - "https_port": 9443, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, "servers": { "s_server": { "listen": [ - ":9443", - ":9080" + ":{$TESTING_CADDY_PORT_ONE}", + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -120,10 +121,10 @@ func TestLoadUnorderedJSON(t *testing.T) { } } `, "json") - req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), nil) if err != nil { t.Fail() return } - tester.AssertResponseCode(req, 200) + harness.AssertResponseCode(req, 200) } diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index 8cd3220dfc7..63cd1f81d3c 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -24,13 +24,13 @@ const acmeChallengePort = 9081 // Test the basic functionality of Caddy's ACME server func TestACMEServerWithDefaults(t *testing.T) { ctx := context.Background() - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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 { @@ -41,8 +41,8 @@ func TestACMEServerWithDefaults(t *testing.T) { logger := caddy.Log().Named("acmeserver") client := acmez.Client{ Client: &acme.Client{ - Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client(), + Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()), + HTTPClient: harness.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ @@ -92,13 +92,13 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) { ctx := context.Background() logger := caddy.Log().Named("acmez") - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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 { @@ -110,8 +110,8 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) { client := acmez.Client{ Client: &acme.Client{ - Directory: "https://acme.localhost:9443/acme/local/directory", - HTTPClient: tester.Client(), + Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()), + HTTPClient: harness.Client(), Logger: logger, }, ChallengeSolvers: map[string]acmez.Solver{ diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index 604a59f76a5..4c7946ac6fd 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" "strings" "testing" @@ -15,40 +16,40 @@ import ( ) func TestACMEServerDirectory(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust local_certs - admin {$TESTING_ADMIN_API} - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} pki { ca local { name "Caddy Local Authority" } } } - acme.localhost:9443 { + acme.localhost:{$TESTING_CADDY_PORT_TWO} { acme_server } `, "caddyfile") - tester.AssertGetResponse( - "https://acme.localhost:9443/acme/local/directory", + harness.AssertGetResponse( + fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()), 200, - `{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"} -`) + fmt.Sprintf(`{"newNonce":"https://acme.localhost:%[1]d/acme/local/new-nonce","newAccount":"https://acme.localhost:%[1]d/acme/local/new-account","newOrder":"https://acme.localhost:%[1]d/acme/local/new-order","revokeCert":"https://acme.localhost:%[1]d/acme/local/revoke-cert","keyChange":"https://acme.localhost:%[1]d/acme/local/key-change"} +`, harness.Tester().PortTwo())) } func TestACMEServerAllowPolicy(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust local_certs - admin {$TESTING_ADMIN_API} - 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" @@ -70,8 +71,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: logger, }, ChallengeSolvers: map[string]acmez.Solver{ @@ -127,14 +128,14 @@ func TestACMEServerAllowPolicy(t *testing.T) { } func TestACMEServerDenyPolicy(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust local_certs - admin {$TESTING_ADMIN_API} - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} pki { ca local { name "Caddy Local Authority" @@ -155,8 +156,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: logger, }, ChallengeSolvers: map[string]acmez.Solver{ diff --git a/caddytest/integration/autohttps_test.go b/caddytest/integration/autohttps_test.go index 89070b29e78..ec296400394 100644 --- a/caddytest/integration/autohttps_test.go +++ b/caddytest/integration/autohttps_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "net/http" "testing" @@ -8,69 +9,69 @@ import ( ) func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} + 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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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 647d6b7ce0f..dab5a7d1db1 100644 --- a/caddytest/integration/caddyfile_test.go +++ b/caddytest/integration/caddyfile_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "net/http" "net/url" "testing" @@ -10,16 +11,16 @@ import ( func TestRespond(t *testing.T) { // arrange - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - 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" } @@ -27,23 +28,23 @@ func TestRespond(t *testing.T) { `, "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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - 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} { - redir / http://localhost:9080/hello 301 + redir / http://localhost:{$TESTING_CADDY_PORT_ONE}/hello 301 respond /hello 200 { body "hello from localhost" @@ -51,21 +52,22 @@ func TestRedirect(t *testing.T) { } `, "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.StartHarness(t) - tester.Client().Jar.SetCookies(localhost, []*http.Cookie{&cookie}) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.Client().Jar.SetCookies(localhost, []*http.Cookie{&cookie}) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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,31 +485,32 @@ func TestValidPrefix(t *testing.T) { } func TestUriReplace(t *testing.T) { - tester := caddytest.StartHarness(t) + harness := caddytest.StartHarness(t) - tester.LoadConfig(` + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) + harness := caddytest.StartHarness(t) - tester.LoadConfig(` + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - 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 @@ -514,7 +519,8 @@ func TestUriOps(t *testing.T) { 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,166 +529,176 @@ 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.StartHarness(t) + harness := caddytest.StartHarness(t) - tester.LoadConfig(` + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) + harness := caddytest.StartHarness(t) - tester.LoadConfig(` + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) + harness := caddytest.StartHarness(t) - tester.LoadConfig(` + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) + harness := caddytest.StartHarness(t) - tester.LoadConfig(` + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) + harness := caddytest.StartHarness(t) - tester.LoadConfig(` + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$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.StartHarness(t) + harness := caddytest.StartHarness(t) - tester.LoadConfig(` + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$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.StartHarness(t) + harness := caddytest.StartHarness(t) - tester.LoadConfig(` + harness.LoadConfig(` { - admin {$TESTING_ADMIN_API} - http_port 9080 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} } - :9080 + :{$TESTING_CADDY_PORT_ONE} uri query { +foo bar -baz @@ -690,16 +706,17 @@ func TestUriOpsBlock(t *testing.T) { } 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.StartHarness(t) - tester.LoadConfig(`{ - admin {$TESTING_ADMIN_API} - 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 @@ -709,17 +726,18 @@ func TestHandleErrorSimpleCodes(t *testing.T) { } }`, "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.StartHarness(t) - tester.LoadConfig(`{ - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) - tester.LoadConfig(`{ - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) - tester.LoadConfig(`{ - admin {$TESTING_ADMIN_API} - 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,9 +797,10 @@ func TestHandleErrorRangeAndCodes(t *testing.T) { } }`, "caddyfile") // act and assert - tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Error code is equal to 500 or in the [300..399] range") - tester.AssertGetResponse("http://localhost:9080/threehundred", 301, "Error code is equal to 500 or in the [300..399] range") - tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range") + target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()) + harness.AssertGetResponse(target+"internalerr", 500, "Error code is equal to 500 or in the [300..399] range") + harness.AssertGetResponse(target+"threehundred", 301, "Error code is equal to 500 or in the [300..399] range") + harness.AssertGetResponse(target+"private", 410, "Error in the [400 .. 499] range") } func TestInvalidSiteAddressesAsDirectives(t *testing.T) { diff --git a/caddytest/integration/handler_test.go b/caddytest/integration/handler_test.go index 11d852420f1..500c0e448c0 100644 --- a/caddytest/integration/handler_test.go +++ b/caddytest/integration/handler_test.go @@ -2,6 +2,7 @@ package integration import ( "bytes" + "fmt" "net/http" "testing" @@ -9,36 +10,36 @@ import ( ) func TestBrowse(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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 2e4d780caf6..f52ac5ffac5 100644 --- a/caddytest/integration/intercept_test.go +++ b/caddytest/integration/intercept_test.go @@ -1,22 +1,23 @@ package integration import ( + "fmt" "testing" "github.com/caddyserver/caddy/v2/caddytest" ) func TestIntercept(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(`{ + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ skip_install_trust - admin {$TESTING_ADMIN_API} - http_port 9080 - https_port 9443 + admin {$TESTING_CADDY_ADMIN_BIND} + http_port {$TESTING_CADDY_PORT_ONE} + https_port {$TESTING_CADDY_PORT_TWO} grace_period 1ns } - - localhost:9080 { + + localhost:{$TESTING_CADDY_PORT_ONE} { respond /intercept "I'm a teapot" 408 respond /no-intercept "I'm not a teapot" @@ -25,10 +26,10 @@ func TestIntercept(t *testing.T) { handle_response @teapot { respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503 } - } + } } `, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") - tester.AssertGetResponse("http://localhost:9080/no-intercept", 200, "I'm not a teapot") + harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/intercept", harness.Tester().PortOne()), 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") + harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/no-intercept", harness.Tester().PortOne()), 200, "I'm not a teapot") } diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go index 90c396f48ce..502674edbc2 100644 --- a/caddytest/integration/leafcertloaders_test.go +++ b/caddytest/integration/leafcertloaders_test.go @@ -7,21 +7,21 @@ import ( ) func TestLeafCertLoaders(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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 24b86509359..f22f5133d8f 100644 --- a/caddytest/integration/listener_test.go +++ b/caddytest/integration/listener_test.go @@ -28,15 +28,15 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy _ = srv.Close() _ = l.Close() }) - tester := caddytest.StartHarness(t) - tester.LoadConfig(fmt.Sprintf(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(fmt.Sprintf(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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 ac20af45da0..88ceb9e3638 100644 --- a/caddytest/integration/map_test.go +++ b/caddytest/integration/map_test.go @@ -2,6 +2,7 @@ package integration import ( "bytes" + "fmt" "testing" "github.com/caddyserver/caddy/v2/caddytest" @@ -9,16 +10,16 @@ import ( func TestMap(t *testing.T) { // arrange - tester := caddytest.StartHarness(t) - tester.LoadConfig(`{ + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ skip_install_trust - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) - tester.LoadConfig(`{ + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ skip_install_trust - admin {$TESTING_ADMIN_API} - 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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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 fdb742e8cac..f141f6130f0 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -14,11 +14,11 @@ import ( ) func TestSRVReverseProxy(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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.StartHarness(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.LoadConfig(fmt.Sprintf(` + harness.LoadConfig(fmt.Sprintf(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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.StartHarness(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.LoadConfig(fmt.Sprintf(` + harness.LoadConfig(fmt.Sprintf(` { skip_install_trust - admin {$TESTING_ADMIN_API} - 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 e5aced6e997..8a5cfb73628 100644 --- a/caddytest/integration/sni_test.go +++ b/caddytest/integration/sni_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "testing" "github.com/caddyserver/caddy/v2/caddytest" @@ -8,20 +9,20 @@ import ( func TestDefaultSNI(t *testing.T) { // arrange - tester := caddytest.StartHarness(t) - tester.LoadConfig(`{ + harness := caddytest.StartHarness(t) + harness.LoadConfig(`{ "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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 e47697793c1..e23c4f3d8ec 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -20,21 +20,21 @@ import ( // (see https://github.com/caddyserver/caddy/issues/3556 for use case) func TestH2ToH2CStream(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "listen": "{$TESTING_CADDY_ADMIN_BIND}" }, "apps": { "http": { - "http_port": 9080, - "https_port": 9443, - "grace_period": 1, + "http_port": {$TESTING_CADDY_PORT_ONE}, + "https_port": {$TESTING_CADDY_PORT_TWO}, + "grace_period": 1, "servers": { "srv0": { "listen": [ - ":9443" + ":{$TESTING_CADDY_PORT_TWO}" ], "routes": [ { @@ -102,7 +102,7 @@ func TestH2ToH2CStream(t *testing.T) { expectedBody := "some data to be echoed" // start the server - server := testH2ToH2CStreamServeH2C(t) + server := testH2ToH2CStreamServeH2C(harness, t) go server.ListenAndServe() defer func() { ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) @@ -116,7 +116,7 @@ func TestH2ToH2CStream(t *testing.T) { Body: io.NopCloser(r), URL: &url.URL{ Scheme: "https", - Host: "127.0.0.1:9443", + Host: fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()), Path: "/tov2ray", }, Proto: "HTTP/2", @@ -127,7 +127,7 @@ func TestH2ToH2CStream(t *testing.T) { // Disable any compression method from server. req.Header.Set("Accept-Encoding", "identity") - resp := tester.AssertResponseCode(req, http.StatusOK) + resp := harness.AssertResponseCode(req, http.StatusOK) if resp.StatusCode != http.StatusOK { return } @@ -149,7 +149,7 @@ func TestH2ToH2CStream(t *testing.T) { } } -func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { +func testH2ToH2CStreamServeH2C(harness *caddytest.TestHarness, t *testing.T) *http.Server { h2s := &http2.Server{} handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rstring, err := httputil.DumpRequest(r, false) @@ -163,7 +163,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { return } - if r.Host != "127.0.0.1:9443" { + if r.Host != fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()) { t.Errorf("r.Host doesn't match, %v!", r.Host) w.WriteHeader(http.StatusNotFound) return @@ -204,21 +204,21 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server { // (see https://github.com/caddyserver/caddy/issues/3606 for use case) func TestH2ToH1ChunkedResponse(t *testing.T) { - tester := caddytest.StartHarness(t) - tester.LoadConfig(` + harness := caddytest.StartHarness(t) + harness.LoadConfig(` { "admin": { - "listen": "{$TESTING_ADMIN_API}" + "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": [ { @@ -305,7 +305,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { } // start the server - server := testH2ToH1ChunkedResponseServeH1(t) + server := testH2ToH1ChunkedResponseServeH1(harness, t) go server.ListenAndServe() defer func() { ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) @@ -319,7 +319,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { Body: io.NopCloser(r), URL: &url.URL{ Scheme: "https", - Host: "127.0.0.1:9443", + Host: fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()), Path: "/tov2ray", }, Proto: "HTTP/2", @@ -333,7 +333,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { fmt.Fprint(w, expectedBody) w.Close() }() - resp := tester.AssertResponseCode(req, http.StatusOK) + resp := harness.AssertResponseCode(req, http.StatusOK) if resp.StatusCode != http.StatusOK { return } @@ -351,9 +351,9 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { } } -func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server { +func testH2ToH1ChunkedResponseServeH1(harness *caddytest.TestHarness, t *testing.T) *http.Server { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Host != "127.0.0.1:9443" { + if r.Host != fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()) { t.Errorf("r.Host doesn't match, %v!", r.Host) w.WriteHeader(http.StatusNotFound) return diff --git a/caddytest/testing_harness.go b/caddytest/testing_harness.go index c5af3f6f547..c9d7a70f462 100644 --- a/caddytest/testing_harness.go +++ b/caddytest/testing_harness.go @@ -55,6 +55,10 @@ func StartHarness(t *testing.T) *TestHarness { return o } +func (tc *TestHarness) Tester() *Tester { + return tc.tester +} + func (tc *TestHarness) Client() *http.Client { return tc.tester.Client } From 3f1ff118f8bd4fad73e09d6374f5cbfe68b5a4c3 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 18 Jun 2024 23:48:21 -0500 Subject: [PATCH 14/14] noot --- caddytest/caddytest.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 58cbfa06b13..c67b9cbb898 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -122,9 +122,11 @@ func (tc *Tester) CleanupCaddy() error { func (tc *Tester) AdminPort() int { return tc.adminPort } + func (tc *Tester) PortOne() int { return tc.portOne } + func (tc *Tester) PortTwo() int { return tc.portTwo }