testing: make it so caddytest launches an instance of caddy per server

This commit is contained in:
a 2024-06-23 19:58:42 -05:00
parent c2ccf8690f
commit b732a7999a
No known key found for this signature in database
GPG key ID: 374BC539FE795AF0
24 changed files with 1071 additions and 881 deletions

View file

@ -20,6 +20,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"flag"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@ -778,7 +779,10 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
} else { } else {
logger.Error("unclean shutdown") 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 { if remoteAdminServer != nil {

View file

@ -1,40 +1,29 @@
package caddytest package caddytest
import ( import (
"bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"log" "log"
"net" "net"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"os" "os"
"path" "strconv"
"reflect"
"regexp"
"runtime"
"strings" "strings"
"testing" "sync/atomic"
"time" "time"
"github.com/aryann/difflib"
caddycmd "github.com/caddyserver/caddy/v2/cmd" caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/caddyconfig"
// plug in Caddy modules here // plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard" _ "github.com/caddyserver/caddy/v2/modules/standard"
) )
// Defaults store any configuration required to make the tests run // Defaults store any configuration required to make the tests run
type Defaults struct { 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 we expect to be loaded before attempting to run the tests
Certificates []string Certificates []string
// TestRequestTimeout is the time to wait for a http request to // TestRequestTimeout is the time to wait for a http request to
@ -45,29 +34,31 @@ type Defaults struct {
// Default testing values // Default testing values
var Default = Defaults{ 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"}, Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second, TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second, LoadRequestTimeout: 5 * time.Second,
} }
var (
matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
)
// Tester represents an instance of a test client. // Tester represents an instance of a test client.
type Tester struct { type Tester struct {
Client *http.Client Client *http.Client
configLoaded bool
t testing.TB adminPort int
portOne int
portTwo int
started atomic.Bool
configLoaded bool
configFileName string
envFileName string
} }
// NewTester will create a new testing client with an attached cookie jar // 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) jar, err := cookiejar.New(nil)
if err != nil { if err != nil {
t.Fatalf("failed to create cookiejar: %s", err) return nil, fmt.Errorf("failed to create cookiejar: %w", err)
} }
return &Tester{ return &Tester{
@ -77,8 +68,7 @@ func NewTester(t testing.TB) *Tester {
Timeout: Default.TestRequestTimeout, Timeout: Default.TestRequestTimeout,
}, },
configLoaded: false, configLoaded: false,
t: t, }, nil
}
} }
type configLoadError struct { type configLoadError struct {
@ -92,53 +82,73 @@ func timeElapsed(start time.Time, name string) {
log.Printf("%s took %s", name, elapsed) log.Printf("%s took %s", name, elapsed)
} }
// InitServer this will configure the server with a configurion of a specific // launch caddy will start the server
// type. The configType must be either "json" or the adapter type. func (tc *Tester) LaunchCaddy() error {
func (tc *Tester) InitServer(rawConfig string, configType string) { if !tc.started.CompareAndSwap(false, true) {
if err := tc.initServer(rawConfig, configType); err != nil { return fmt.Errorf("already launched caddy with this tester")
tc.t.Logf("failed to load config: %s", err)
tc.t.Fail()
} }
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil { if err := tc.startServer(); err != nil {
tc.t.Logf("failed ensuring config is running: %s", err) return fmt.Errorf("failed to start server: %w", err)
tc.t.Fail()
} }
return nil
} }
// InitServer this will configure the server with a configurion of a specific func (tc *Tester) CleanupCaddy() error {
// type. The configType must be either "json" or the adapter type. // now shutdown the server, since the test is done.
func (tc *Tester) initServer(rawConfig string, configType string) error { defer func() {
if testing.Short() { // try to remove pthe tmp config file we created
tc.t.SkipNow() if tc.configFileName != "" {
return nil os.Remove(tc.configFileName)
}
err := validateTestPrerequisites(tc.t)
if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil
}
tc.t.Cleanup(func() {
if tc.t.Failed() && tc.configLoaded {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil {
tc.t.Log("unable to read the current config")
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
var out bytes.Buffer
_ = json.Indent(&out, body, "", " ")
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
} }
}) if tc.envFileName != "" {
os.Remove(tc.envFileName)
}
}()
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 tc.isCaddyAdminRunning() != nil {
return nil
}
time.Sleep(100 * time.Millisecond)
}
rawConfig = prependCaddyFilePath(rawConfig) 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
// normalize JSON config // normalize JSON config
if configType == "json" { if configType == "json" {
tc.t.Logf("Before: %s", rawConfig)
var conf any var conf any
if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil { if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil {
return err return err
@ -148,16 +158,14 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return err return err
} }
rawConfig = string(c) rawConfig = string(c)
tc.t.Logf("After: %s", rawConfig)
} }
client := &http.Client{ client := &http.Client{
Timeout: Default.LoadRequestTimeout, Timeout: Default.LoadRequestTimeout,
} }
start := time.Now() 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 { if err != nil {
tc.t.Errorf("failed to create request. %s", err) return fmt.Errorf("failed to create request. %w", err)
return err
} }
if configType == "json" { if configType == "json" {
@ -168,16 +176,14 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
tc.t.Errorf("unable to contact caddy server. %s", err) return fmt.Errorf("unable to contact caddy server. %w", err)
return err
} }
timeElapsed(start, "caddytest: config load time") timeElapsed(start, "caddytest: config load time")
defer res.Body.Close() defer res.Body.Close()
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
tc.t.Errorf("unable to read response. %s", err) return fmt.Errorf("unable to read response. %w", err)
return err
} }
if res.StatusCode != 200 { if res.StatusCode != 200 {
@ -185,133 +191,115 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
} }
tc.configLoaded = true tc.configLoaded = true
// if the config is not loaded at this point, it is a bug in caddy's config.Load
// the contract for config.Load states that the config must be loaded before it returns, and that it will
// error if the config fails to apply
return nil return nil
} }
func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error { func (tc *Tester) GetCurrentConfig(receiver any) error {
expectedBytes := []byte(prependCaddyFilePath(rawConfig))
if configType != "json" {
adapter := caddyconfig.GetAdapter(configType)
if adapter == nil {
return fmt.Errorf("adapter of config type is missing: %s", configType)
}
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
}
var expected any
err := json.Unmarshal(expectedBytes, &expected)
if err != nil {
return err
}
client := &http.Client{ client := &http.Client{
Timeout: Default.LoadRequestTimeout, Timeout: Default.LoadRequestTimeout,
} }
fetchConfig := func(client *http.Client) any { resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.adminPort))
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) if err != nil {
if err != nil { return err
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()
for retries := 10; retries > 0; retries-- { actualBytes, err := io.ReadAll(resp.Body)
if reflect.DeepEqual(expected, fetchConfig(client)) { if err != nil {
return nil return err
}
time.Sleep(1 * time.Second)
} }
tc.t.Errorf("POSTed configuration isn't active") err = json.Unmarshal(actualBytes, receiver)
return errors.New("EnsureConfigRunning: POSTed configuration isn't active") if err != nil {
return err
}
return nil
} }
const initConfig = `{ func getFreePort() (int, error) {
admin localhost:2999 lr, err := net.Listen("tcp", "localhost:0")
} if err != nil {
` return 0, err
// validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running.
func validateTestPrerequisites(t testing.TB) error {
// check certificates are found
for _, certName := range Default.Certificates {
if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
}
} }
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
}
if isCaddyAdminRunning() != nil { // launches caddy, and then ensures the Caddy sub-process is running.
// setup the init config file, and set the cleanup afterwards func (tc *Tester) startServer() error {
if tc.isCaddyAdminRunning() == nil {
return fmt.Errorf("caddy test admin port still in use")
}
a, err := getFreePort()
if err != nil {
return fmt.Errorf("could not find a open port to listen on: %w", err)
}
tc.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("", "") f, err := os.CreateTemp("", "")
if err != nil { if err != nil {
return err return err
} }
t.Cleanup(func() { tc.configFileName = f.Name()
os.Remove(f.Name())
}) initConfig := fmt.Sprintf(`{
admin localhost:%d
}`, a)
if _, err := f.WriteString(initConfig); err != nil { if _, err := f.WriteString(initConfig); err != nil {
return err return err
} }
}
// start inprocess caddy server // start inprocess caddy server
os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"} go func() {
go func() { _ = caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile")
caddycmd.Main() }()
}() // wait for caddy admin api to start. it should happen quickly.
for retries := 10; retries > 0 && tc.isCaddyAdminRunning() != nil; retries-- {
// wait for caddy to start serving the initial config time.Sleep(100 * time.Millisecond)
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(1 * time.Second)
}
} }
// one more time to return the error // 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 // assert that caddy is running
client := &http.Client{ client := &http.Client{
Timeout: Default.LoadRequestTimeout, 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 { 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() resp.Body.Close()
return nil return nil
} }
func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic("unable to determine the current file path")
}
return path.Dir(filename)
}
// use the convention to replace /[certificatename].[crt|key] with the full path
// this helps reduce the noise in test configurations and also allow this
// to run in any path
func prependCaddyFilePath(rawConfig string) string {
r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
return r
}
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally // CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func CreateTestingTransport() *http.Transport { func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{ dialer := net.Dialer{
@ -338,231 +326,3 @@ func CreateTestingTransport() *http.Transport {
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
} }
} }
// AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc := NewTester(t)
err := tc.initServer(rawConfig, configType)
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
}
}
// AssertRedirect makes a request and asserts the redirection happens
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
// using the existing client, we override the check redirect policy for this test
old := tc.Client.CheckRedirect
tc.Client.CheckRedirect = redirectPolicyFunc
defer func() { tc.Client.CheckRedirect = old }()
resp, err := tc.Client.Get(requestURI)
if err != nil {
tc.t.Errorf("failed to call server %s", err)
return nil
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
}
loc, err := resp.Location()
if err != nil {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
}
if loc == nil && expectedToLocation != "" {
tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI)
}
if loc != nil {
if expectedToLocation != loc.String() {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
}
}
return resp
}
// CompareAdapt adapts a config and then compares it against an expected result
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName)
return false
}
options := make(map[string]any)
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
t.Logf("adapting config using %s adapter: %v", adapterName, err)
return false
}
// prettify results to keep tests human-manageable
var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, result, "", "\t")
if err != nil {
return false
}
result = prettyBuf.Bytes()
if len(warnings) > 0 {
for _, w := range warnings {
t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message)
}
}
diff := difflib.Diff(
strings.Split(expectedResponse, "\n"),
strings.Split(string(result), "\n"))
// scan for failure
failed := false
for _, d := range diff {
if d.Delta != difflib.Common {
failed = true
break
}
}
if failed {
for _, d := range diff {
switch d.Delta {
case difflib.Common:
fmt.Printf(" %s\n", d.Payload)
case difflib.LeftOnly:
fmt.Printf(" - %s\n", d.Payload)
case difflib.RightOnly:
fmt.Printf(" + %s\n", d.Payload)
}
}
return false
}
return true
}
// AssertAdapt adapts a config and then tests it against an expected result
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
if !ok {
t.Fail()
}
}
// Generic request functions
func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
requestContentType := ""
for _, requestHeader := range requestHeaders {
arr := strings.SplitAfterN(requestHeader, ":", 2)
k := strings.TrimRight(arr[0], ":")
v := strings.TrimSpace(arr[1])
if k == "Content-Type" {
requestContentType = v
}
t.Logf("Request header: %s => %s", k, v)
req.Header.Set(k, v)
}
if requestContentType == "" {
t.Logf("Content-Type header not provided")
}
}
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.Client.Do(req)
if err != nil {
tc.t.Fatalf("failed to call server %s", err)
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
}
return resp
}
// AssertResponse request a URI and assert the status code and the body contains a string
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
tc.t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if body != expectedBody {
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return resp, body
}
// Verb specific test functions
// AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}

View file

@ -0,0 +1,116 @@
package caddytest
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/aryann/difflib"
"github.com/stretchr/testify/require"
"github.com/caddyserver/caddy/v2/caddyconfig"
)
// AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc, err := NewTester()
require.NoError(t, err)
err = tc.LaunchCaddy()
require.NoError(t, err)
err = tc.LoadConfig(rawConfig, configType)
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected error \"%s\" but got \"%s\"", expectedError, err.Error())
}
_ = tc.CleanupCaddy()
}
// CompareAdapt adapts a config and then compares it against an expected result
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName)
return false
}
options := make(map[string]any)
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
if err != nil {
t.Logf("adapting config using %s adapter: %v", adapterName, err)
return false
}
// prettify results to keep tests human-manageable
var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, result, "", "\t")
if err != nil {
return false
}
result = prettyBuf.Bytes()
if len(warnings) > 0 {
for _, w := range warnings {
t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message)
}
}
diff := difflib.Diff(
strings.Split(expectedResponse, "\n"),
strings.Split(string(result), "\n"))
// scan for failure
failed := false
for _, d := range diff {
if d.Delta != difflib.Common {
failed = true
break
}
}
if failed {
for _, d := range diff {
switch d.Delta {
case difflib.Common:
fmt.Printf(" %s\n", d.Payload)
case difflib.LeftOnly:
fmt.Printf(" - %s\n", d.Payload)
case difflib.RightOnly:
fmt.Printf(" + %s\n", d.Payload)
}
}
return false
}
return true
}
// AssertAdapt adapts a config and then tests it against an expected result
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
if !ok {
t.Fail()
}
}
// Generic request functions
func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
requestContentType := ""
for _, requestHeader := range requestHeaders {
arr := strings.SplitAfterN(requestHeader, ":", 2)
k := strings.TrimRight(arr[0], ":")
v := strings.TrimSpace(arr[1])
if k == "Content-Type" {
requestContentType = v
}
t.Logf("Request header: %s => %s", k, v)
req.Header.Set(k, v)
}
if requestContentType == "" {
t.Logf("Content-Type header not provided")
}
}

View file

@ -1,21 +1,22 @@
package caddytest package caddytest
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
) )
func TestReplaceCertificatePaths(t *testing.T) { func TestReplaceCertificatePaths(t *testing.T) {
rawConfig := `a.caddy.localhost:9443 { rawConfig := `a.caddy.localhost:9443{
tls /caddy.localhost.crt /caddy.localhost.key { tls /caddy.localhost.crt /caddy.localhost.key {
} }
redir / https://b.caddy.localhost:9443/version 301 redir / https://b.caddy.localhost:9443/version 301
respond /version 200 { respond /version 200 {
body "hello from a.caddy.localhost" body "hello from a.caddy.localhost"
} }
}` }`
r := prependCaddyFilePath(rawConfig) r := prependCaddyFilePath(rawConfig)
@ -34,8 +35,8 @@ func TestReplaceCertificatePaths(t *testing.T) {
} }
func TestLoadUnorderedJSON(t *testing.T) { func TestLoadUnorderedJSON(t *testing.T) {
tester := NewTester(t) harness := StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"logging": { "logging": {
"logs": { "logs": {
@ -68,7 +69,7 @@ func TestLoadUnorderedJSON(t *testing.T) {
} }
}, },
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"pki": { "pki": {
@ -79,13 +80,13 @@ func TestLoadUnorderedJSON(t *testing.T) {
} }
}, },
"http": { "http": {
"http_port": 9080, "http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": 9443, "https_port": {$TESTING_CADDY_PORT_TWO},
"servers": { "servers": {
"s_server": { "s_server": {
"listen": [ "listen": [
":9443", ":{$TESTING_CADDY_PORT_ONE}",
":9080" ":{$TESTING_CADDY_PORT_TWO}"
], ],
"routes": [ "routes": [
{ {
@ -120,10 +121,10 @@ func TestLoadUnorderedJSON(t *testing.T) {
} }
} }
`, "json") `, "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 { if err != nil {
t.Fail() t.Fail()
return return
} }
tester.AssertResponseCode(req, 200) harness.AssertResponseCode(req, 200)
} }

View file

@ -24,19 +24,13 @@ const acmeChallengePort = 9081
// Test the basic functionality of Caddy's ACME server // Test the basic functionality of Caddy's ACME server
func TestACMEServerWithDefaults(t *testing.T) { func TestACMEServerWithDefaults(t *testing.T) {
ctx := context.Background() ctx := context.Background()
logger, err := zap.NewDevelopment() harness := caddytest.StartHarness(t)
if err != nil { harness.LoadConfig(`
t.Error(err)
return
}
tester := caddytest.NewTester(t)
tester.InitServer(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
local_certs local_certs
} }
acme.localhost { acme.localhost {
@ -44,10 +38,11 @@ func TestACMEServerWithDefaults(t *testing.T) {
} }
`, "caddyfile") `, "caddyfile")
logger := caddy.Log().Named("acmeserver")
client := acmez.Client{ client := acmez.Client{
Client: &acme.Client{ Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory", Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
HTTPClient: tester.Client, HTTPClient: harness.Client(),
Logger: logger, Logger: logger,
}, },
ChallengeSolvers: map[string]acmez.Solver{ ChallengeSolvers: map[string]acmez.Solver{
@ -97,13 +92,13 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
ctx := context.Background() ctx := context.Background()
logger := caddy.Log().Named("acmez") logger := caddy.Log().Named("acmez")
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
local_certs local_certs
} }
acme.localhost { acme.localhost {
@ -115,8 +110,8 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
client := acmez.Client{ client := acmez.Client{
Client: &acme.Client{ Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory", Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
HTTPClient: tester.Client, HTTPClient: harness.Client(),
Logger: logger, Logger: logger,
}, },
ChallengeSolvers: map[string]acmez.Solver{ ChallengeSolvers: map[string]acmez.Solver{

View file

@ -5,50 +5,51 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"fmt"
"strings" "strings"
"testing" "testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddytest" "github.com/caddyserver/caddy/v2/caddytest"
"github.com/mholt/acmez/v2" "github.com/mholt/acmez/v2"
"github.com/mholt/acmez/v2/acme" "github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
) )
func TestACMEServerDirectory(t *testing.T) { func TestACMEServerDirectory(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
local_certs local_certs
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
pki { pki {
ca local { ca local {
name "Caddy Local Authority" name "Caddy Local Authority"
} }
} }
} }
acme.localhost:9443 { acme.localhost:{$TESTING_CADDY_PORT_TWO} {
acme_server acme_server
} }
`, "caddyfile") `, "caddyfile")
tester.AssertGetResponse( harness.AssertGetResponse(
"https://acme.localhost:9443/acme/local/directory", fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
200, 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) { func TestACMEServerAllowPolicy(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
local_certs local_certs
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
pki { pki {
ca local { ca local {
name "Caddy Local Authority" name "Caddy Local Authority"
@ -66,16 +67,12 @@ func TestACMEServerAllowPolicy(t *testing.T) {
`, "caddyfile") `, "caddyfile")
ctx := context.Background() ctx := context.Background()
logger, err := zap.NewDevelopment() logger := caddy.Log().Named("acmez")
if err != nil {
t.Error(err)
return
}
client := acmez.Client{ client := acmez.Client{
Client: &acme.Client{ Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory", Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
HTTPClient: tester.Client, HTTPClient: harness.Client(),
Logger: logger, Logger: logger,
}, },
ChallengeSolvers: map[string]acmez.Solver{ ChallengeSolvers: map[string]acmez.Solver{
@ -131,14 +128,14 @@ func TestACMEServerAllowPolicy(t *testing.T) {
} }
func TestACMEServerDenyPolicy(t *testing.T) { func TestACMEServerDenyPolicy(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
local_certs local_certs
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
pki { pki {
ca local { ca local {
name "Caddy Local Authority" name "Caddy Local Authority"
@ -155,16 +152,12 @@ func TestACMEServerDenyPolicy(t *testing.T) {
`, "caddyfile") `, "caddyfile")
ctx := context.Background() ctx := context.Background()
logger, err := zap.NewDevelopment() logger := caddy.Log().Named("acmez")
if err != nil {
t.Error(err)
return
}
client := acmez.Client{ client := acmez.Client{
Client: &acme.Client{ Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory", Directory: fmt.Sprintf("https://acme.localhost:%d/acme/local/directory", harness.Tester().PortTwo()),
HTTPClient: tester.Client, HTTPClient: harness.Client(),
Logger: logger, Logger: logger,
}, },
ChallengeSolvers: map[string]acmez.Solver{ ChallengeSolvers: map[string]acmez.Solver{
@ -197,7 +190,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"}) _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
if err == nil { if err == nil {
t.Errorf("obtaining certificate for 'deny.localhost' domain") 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) t.Logf("unexpected error: %v", err)
} }
} }

View file

@ -1,6 +1,7 @@
package integration package integration
import ( import (
"fmt"
"net/http" "net/http"
"testing" "testing"
@ -8,69 +9,69 @@ import (
) )
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) { func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
skip_install_trust skip_install_trust
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
} }
localhost localhost
respond "Yahaha! You found me!" respond "Yahaha! You found me!"
`, "caddyfile") `, "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) { func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
} }
localhost:9443 localhost:{$TESTING_CADDY_PORT_TWO}
respond "Yahaha! You found me!" respond "Yahaha! You found me!"
`, "caddyfile") `, "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) { func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
} }
localhost:1234 localhost:1234
respond "Yahaha! You found me!" respond "Yahaha! You found me!"
`, "caddyfile") `, "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) { func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": 9443, "https_port": {$TESTING_CADDY_PORT_TWO},
"servers": { "servers": {
"ingress_server": { "ingress_server": {
"listen": [ "listen": [
":9080", ":{$TESTING_CADDY_PORT_ONE}",
":9443" ":{$TESTING_CADDY_PORT_TWO}"
], ],
"routes": [ "routes": [
{ {
@ -94,52 +95,52 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
} }
} }
`, "json") `, "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) { func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
local_certs local_certs
} }
http://:9080 { http://:{$TESTING_CADDY_PORT_ONE} {
respond "Foo" respond "Foo"
} }
http://baz.localhost:9080 { http://baz.localhost:{$TESTING_CADDY_PORT_ONE} {
respond "Baz" respond "Baz"
} }
bar.localhost { bar.localhost {
respond "Bar" respond "Bar"
} }
`, "caddyfile") `, "caddyfile")
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect) harness.AssertRedirect(fmt.Sprintf("http://bar.localhost:%d/", harness.Tester().PortOne()), "https://bar.localhost/", http.StatusPermanentRedirect)
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo") harness.AssertGetResponse(fmt.Sprintf("http://foo.localhost:%d/", harness.Tester().PortOne()), 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz") harness.AssertGetResponse(fmt.Sprintf("http://baz.localhost:%d/", harness.Tester().PortOne()), 200, "Baz")
} }
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) { func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
local_certs local_certs
} }
http://:9080 { http://:{$TESTING_CADDY_PORT_ONE} {
respond "Foo" respond "Foo"
} }
bar.localhost { bar.localhost {
respond "Bar" respond "Bar"
} }
`, "caddyfile") `, "caddyfile")
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect) harness.AssertRedirect(fmt.Sprintf("http://bar.localhost:%d/", harness.Tester().PortOne()), "https://bar.localhost/", http.StatusPermanentRedirect)
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo") harness.AssertGetResponse(fmt.Sprintf("http://foo.localhost:%d/", harness.Tester().PortOne()), 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo") harness.AssertGetResponse(fmt.Sprintf("http://baz.localhost:%d/", harness.Tester().PortOne()), 200, "Foo")
} }

View file

@ -1,6 +1,7 @@
package integration package integration
import ( import (
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
@ -10,62 +11,63 @@ import (
func TestRespond(t *testing.T) { func TestRespond(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
respond /version 200 { respond /version 200 {
body "hello from localhost" body "hello from localhost"
} }
} }
`, "caddyfile") `, "caddyfile")
// act and assert // 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) { func TestRedirect(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns 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 { respond /hello 200 {
body "hello from localhost" body "hello from localhost"
} }
} }
`, "caddyfile") `, "caddyfile")
target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
// act and assert // act and assert
tester.AssertRedirect("http://localhost:9080/", "http://localhost:9080/hello", 301) harness.AssertRedirect(target, target+"hello", 301)
// follow redirect // follow redirect
tester.AssertGetResponse("http://localhost:9080/", 200, "hello from localhost") harness.AssertGetResponse(target, 200, "hello from localhost")
} }
func TestDuplicateHosts(t *testing.T) { func TestDuplicateHosts(t *testing.T) {
// act and assert // act and assert
caddytest.AssertLoadError(t, caddytest.AssertLoadError(t,
` `
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
} }
`, `,
"caddyfile", "caddyfile",
@ -80,18 +82,18 @@ func TestReadCookie(t *testing.T) {
} }
// arrange // arrange
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.Client.Jar.SetCookies(localhost, []*http.Cookie{&cookie}) harness.Client().Jar.SetCookies(localhost, []*http.Cookie{&cookie})
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
templates { templates {
root testdata root testdata
} }
@ -102,21 +104,22 @@ func TestReadCookie(t *testing.T) {
`, "caddyfile") `, "caddyfile")
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/cookie.html", 200, "<h2>Cookie.ClientName caddytest</h2>") target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
harness.AssertGetResponse(target+"cookie.html", 200, "<h2>Cookie.ClientName caddytest</h2>")
} }
func TestReplIndex(t *testing.T) { func TestReplIndex(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
templates { templates {
root testdata root testdata
} }
@ -128,7 +131,8 @@ func TestReplIndex(t *testing.T) {
`, "caddyfile") `, "caddyfile")
// act and assert // 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) { func TestInvalidPrefix(t *testing.T) {
@ -481,40 +485,42 @@ func TestValidPrefix(t *testing.T) {
} }
func TestUriReplace(t *testing.T) { func TestUriReplace(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri replace "\}" %7D uri replace "\}" %7D
uri replace "\{" %7B uri replace "\{" %7B
respond "{query}"`, "caddyfile") 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) { func TestUriOps(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query +foo bar uri query +foo bar
uri query -baz uri query -baz
uri query taz test uri query taz test
uri query key=value example uri query key=value example
uri query changethis>changed uri query changethis>changed
respond "{query}"`, "caddyfile") 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. // Tests the `http.request.local.port` placeholder.
@ -523,204 +529,215 @@ func TestUriOps(t *testing.T) {
// refer to 127.0.0.1 or ::1. // refer to 127.0.0.1 or ::1.
// TODO: Test each http version separately (especially http/3) // TODO: Test each http version separately (especially http/3)
func TestHttpRequestLocalPortPlaceholder(t *testing.T) { func TestHttpRequestLocalPortPlaceholder(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
respond "{http.request.local.port}"`, "caddyfile") 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) { func TestSetThenAddQueryParams(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query foo bar uri query foo bar
uri query +foo baz uri query +foo baz
respond "{query}"`, "caddyfile") 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) { func TestSetThenDeleteParams(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query bar foo{query.foo} uri query bar foo{query.foo}
uri query -foo uri query -foo
respond "{query}"`, "caddyfile") 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) { func TestRenameAndOtherOps(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query foo>bar uri query foo>bar
uri query bar taz uri query bar taz
uri query +bar baz uri query +bar baz
respond "{query}"`, "caddyfile") 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) { func TestReplaceOps(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query foo bar baz uri query foo bar baz
respond "{query}"`, "caddyfile") 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) { func TestReplaceWithReplacementPlaceholder(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query foo bar {query.placeholder} uri query foo bar {query.placeholder}
respond "{query}"`, "caddyfile") 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) { func TestReplaceWithKeyPlaceholder(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query {query.placeholder} bar baz uri query {query.placeholder} bar baz
respond "{query}"`, "caddyfile") 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) { func TestPartialReplacement(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query foo ar az uri query foo ar az
respond "{query}"`, "caddyfile") 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) { func TestNonExistingSearch(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query foo var baz uri query foo var baz
respond "{query}"`, "caddyfile") 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) { func TestReplaceAllOps(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query * bar baz uri query * bar baz
respond "{query}"`, "caddyfile") 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) { func TestUriOpsBlock(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
:9080 :{$TESTING_CADDY_PORT_ONE}
uri query { uri query {
+foo bar +foo bar
-baz -baz
taz test taz test
} }
respond "{query}"`, "caddyfile") 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) { func TestHandleErrorSimpleCodes(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(`{ harness.LoadConfig(`{
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
root * /srv root * /srv
error /private* "Unauthorized" 410 error /private* "Unauthorized" 410
error /hidden* "Not found" 404 error /hidden* "Not found" 404
handle_errors 404 410 { handle_errors 404 410 {
respond "404 or 410 error" respond "404 or 410 error"
} }
}`, "caddyfile") }`, "caddyfile")
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/private", 410, "404 or 410 error") target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
tester.AssertGetResponse("http://localhost:9080/hidden", 404, "404 or 410 error") harness.AssertGetResponse(target+"private", 410, "404 or 410 error")
harness.AssertGetResponse(target+"hidden", 404, "404 or 410 error")
} }
func TestHandleErrorRange(t *testing.T) { func TestHandleErrorRange(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(`{ harness.LoadConfig(`{
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
root * /srv root * /srv
error /private* "Unauthorized" 410 error /private* "Unauthorized" 410
error /hidden* "Not found" 404 error /hidden* "Not found" 404
@ -730,17 +747,18 @@ func TestHandleErrorRange(t *testing.T) {
} }
}`, "caddyfile") }`, "caddyfile")
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range") target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range") 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) { func TestHandleErrorSort(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(`{ harness.LoadConfig(`{
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
root * /srv root * /srv
error /private* "Unauthorized" 410 error /private* "Unauthorized" 410
error /hidden* "Not found" 404 error /hidden* "Not found" 404
@ -754,17 +772,18 @@ func TestHandleErrorSort(t *testing.T) {
} }
}`, "caddyfile") }`, "caddyfile")
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Fallback route: code outside the [400..499] range") target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range") 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) { func TestHandleErrorRangeAndCodes(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(`{ harness.LoadConfig(`{
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
root * /srv root * /srv
error /private* "Unauthorized" 410 error /private* "Unauthorized" 410
error /threehundred* "Moved Permanently" 301 error /threehundred* "Moved Permanently" 301
@ -778,9 +797,10 @@ func TestHandleErrorRangeAndCodes(t *testing.T) {
} }
}`, "caddyfile") }`, "caddyfile")
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Error code is equal to 500 or in the [300..399] range") target := fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne())
tester.AssertGetResponse("http://localhost:9080/threehundred", 301, "Error code is equal to 500 or in the [300..399] range") harness.AssertGetResponse(target+"internalerr", 500, "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") 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) { func TestInvalidSiteAddressesAsDirectives(t *testing.T) {

View file

@ -2,6 +2,7 @@ package integration
import ( import (
"bytes" "bytes"
"fmt"
"net/http" "net/http"
"testing" "testing"
@ -9,36 +10,36 @@ import (
) )
func TestBrowse(t *testing.T) { func TestBrowse(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
http://localhost:9080 { http://localhost:{$TESTING_CADDY_PORT_ONE} {
file_server browse file_server browse
} }
`, "caddyfile") `, "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 { if err != nil {
t.Fail() t.Fail()
return return
} }
tester.AssertResponseCode(req, 200) harness.AssertResponseCode(req, 200)
} }
func TestRespondWithJSON(t *testing.T) { func TestRespondWithJSON(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
localhost { localhost {
@ -46,7 +47,7 @@ func TestRespondWithJSON(t *testing.T) {
} }
`, "caddyfile") `, "caddyfile")
res, _ := tester.AssertPostResponseBody("https://localhost:9443/", res, _ := harness.AssertPostResponseBody(fmt.Sprintf("https://localhost:%d/", harness.Tester().PortTwo()),
nil, nil,
bytes.NewBufferString(`{ bytes.NewBufferString(`{
"greeting": "Hello, world!" "greeting": "Hello, world!"

View file

@ -1,22 +1,23 @@
package integration package integration
import ( import (
"fmt"
"testing" "testing"
"github.com/caddyserver/caddy/v2/caddytest" "github.com/caddyserver/caddy/v2/caddytest"
) )
func TestIntercept(t *testing.T) { func TestIntercept(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(`{ harness.LoadConfig(`{
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
respond /intercept "I'm a teapot" 408 respond /intercept "I'm a teapot" 408
respond /no-intercept "I'm not a teapot" respond /no-intercept "I'm not a teapot"
@ -25,10 +26,10 @@ func TestIntercept(t *testing.T) {
handle_response @teapot { handle_response @teapot {
respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503 respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503
} }
} }
} }
`, "caddyfile") `, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") 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")
tester.AssertGetResponse("http://localhost:9080/no-intercept", 200, "I'm not a teapot") harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/no-intercept", harness.Tester().PortOne()), 200, "I'm not a teapot")
} }

View file

@ -7,21 +7,21 @@ import (
) )
func TestLeafCertLoaders(t *testing.T) { func TestLeafCertLoaders(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": 9443, "https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":9443" ":{$TESTING_CADDY_PORT_TWO}"
], ],
"routes": [ "routes": [
{ {

View file

@ -12,7 +12,7 @@ import (
"github.com/caddyserver/caddy/v2/caddytest" "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") l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Fatalf("failed to listen: %s", err) t.Fatalf("failed to listen: %s", err)
@ -28,15 +28,15 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy
_ = srv.Close() _ = srv.Close()
_ = l.Close() _ = l.Close()
}) })
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(fmt.Sprintf(` harness.LoadConfig(fmt.Sprintf(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
local_certs local_certs
servers :9443 { servers :{$TESTING_CADDY_PORT_TWO} {
listener_wrappers { listener_wrappers {
http_redirect http_redirect
tls tls
@ -47,7 +47,7 @@ func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddy
reverse_proxy %s reverse_proxy %s
} }
`, l.Addr().String()), "caddyfile") `, l.Addr().String()), "caddyfile")
return tester return harness
} }
func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) { func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
@ -56,7 +56,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
body := make([]byte, uploadSize) body := make([]byte, uploadSize)
rand.New(rand.NewSource(0)).Read(body) 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) buf := new(bytes.Buffer)
_, err := buf.ReadFrom(request.Body) _, err := buf.ReadFrom(request.Body)
if err != nil { if err != nil {
@ -69,7 +69,7 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
writer.WriteHeader(http.StatusNoContent) 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 { if err != nil {
t.Fatalf("failed to post: %s", err) t.Fatalf("failed to post: %s", err)
} }
@ -80,14 +80,14 @@ func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
} }
func TestLargeHttpRequest(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") t.Fatal("not supposed to handle a request")
}) })
// We never read the body in any way, set an extra long header instead. // 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)) req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024))
_, err := tester.Client.Do(req) _, err := harness.Client().Do(req)
if err == nil { if err == nil {
t.Fatal("not supposed to succeed") t.Fatal("not supposed to succeed")
} }

View file

@ -2,6 +2,7 @@ package integration
import ( import (
"bytes" "bytes"
"fmt"
"testing" "testing"
"github.com/caddyserver/caddy/v2/caddytest" "github.com/caddyserver/caddy/v2/caddytest"
@ -9,16 +10,16 @@ import (
func TestMap(t *testing.T) { func TestMap(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(`{ harness.LoadConfig(`{
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
map {http.request.method} {dest-1} {dest-2} { map {http.request.method} {dest-1} {dest-2} {
default unknown1 unknown2 default unknown1 unknown2
@ -28,50 +29,50 @@ func TestMap(t *testing.T) {
respond /version 200 { respond /version 200 {
body "hello from localhost {dest-1} {dest-2}" body "hello from localhost {dest-1} {dest-2}"
} }
} }
`, "caddyfile") `, "caddyfile")
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost GET-called unknown2") harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), 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.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) { func TestMapRespondWithDefault(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(`{ harness.LoadConfig(`{
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
} }
localhost:9080 { localhost:{$TESTING_CADDY_PORT_ONE} {
map {http.request.method} {dest-name} { map {http.request.method} {dest-name} {
default unknown default unknown
GET get-called GET get-called
} }
respond /version 200 { respond /version 200 {
body "hello from localhost {dest-name}" body "hello from localhost {dest-name}"
} }
} }
`, "caddyfile") `, "caddyfile")
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called") harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne()), 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown") 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) { func TestMapAsJSON(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"pki": { "pki": {
@ -82,12 +83,12 @@ func TestMapAsJSON(t *testing.T) {
} }
}, },
"http": { "http": {
"http_port": 9080, "http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": 9443, "https_port": {$TESTING_CADDY_PORT_TWO},
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":9080" ":{$TESTING_CADDY_PORT_ONE}"
], ],
"routes": [ "routes": [
{ {
@ -145,7 +146,7 @@ func TestMapAsJSON(t *testing.T) {
} }
} }
}`, "json") }`, "json")
target := fmt.Sprintf("http://localhost:%d/version", harness.Tester().PortOne())
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called") harness.AssertGetResponse(target, 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called") harness.AssertPostResponseBody(target, []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
} }

View file

@ -14,11 +14,11 @@ import (
) )
func TestSRVReverseProxy(t *testing.T) { func TestSRVReverseProxy(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"pki": { "pki": {
@ -87,11 +87,11 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
}) })
runtime.Gosched() // Allow other goroutines to run runtime.Gosched() // Allow other goroutines to run
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"pki": { "pki": {
@ -135,15 +135,15 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
return return
} }
req.Header.Set("X-Caddy-Upstream-Dial", socketName) 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) { func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"pki": { "pki": {
@ -186,7 +186,7 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
}, },
"srv1": { "srv1": {
"listen": [ "listen": [
":9080" ":{$TESTING_CADDY_PORT_ONE}"
], ],
"routes": [ "routes": [
{ {
@ -199,7 +199,7 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
], ],
"handle": [ "handle": [
{ {
"handler": "reverse_proxy", "handler": "reverse_proxy",
"upstreams": [ "upstreams": [
{ {
@ -223,21 +223,21 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
} }
`, "json") `, "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 { if err != nil {
t.Fail() t.Fail()
return return
} }
req.Header.Set("X-Caddy-Upstream-Dial", "localhost:18080") 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) { func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"pki": { "pki": {
@ -280,7 +280,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
}, },
"srv1": { "srv1": {
"listen": [ "listen": [
":9080" ":{$TESTING_CADDY_PORT_ONE}"
], ],
"routes": [ "routes": [
{ {
@ -293,7 +293,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
], ],
"handle": [ "handle": [
{ {
"handler": "reverse_proxy", "handler": "reverse_proxy",
"upstreams": [ "upstreams": [
{ {
@ -317,23 +317,23 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
} }
`, "json") `, "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 { if err != nil {
t.Fail() t.Fail()
return return
} }
req.Header.Set("X-Caddy-Upstream-Dial", "localhost") 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) { func TestReverseProxyHealthCheck(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
http://localhost:2020 { http://localhost:2020 {
@ -342,10 +342,10 @@ func TestReverseProxyHealthCheck(t *testing.T) {
http://localhost:2021 { http://localhost:2021 {
respond "ok" respond "ok"
} }
http://localhost:9080 { http://localhost:{$TESTING_CADDY_PORT_ONE} {
reverse_proxy { reverse_proxy {
to localhost:2020 to localhost:2020
health_uri /health health_uri /health
health_port 2021 health_port 2021
health_interval 10ms health_interval 10ms
@ -357,14 +357,15 @@ func TestReverseProxyHealthCheck(t *testing.T) {
`, "caddyfile") `, "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 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) { func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.SkipNow() t.SkipNow()
} }
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
f, err := os.CreateTemp("", "*.sock") f, err := os.CreateTemp("", "*.sock")
if err != nil { if err != nil {
t.Errorf("failed to create TempFile: %s", err) t.Errorf("failed to create TempFile: %s", err)
@ -395,18 +396,18 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
}) })
runtime.Gosched() // Allow other goroutines to run runtime.Gosched() // Allow other goroutines to run
tester.InitServer(fmt.Sprintf(` harness.LoadConfig(fmt.Sprintf(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
http://localhost:9080 { http://localhost:{$TESTING_CADDY_PORT_ONE} {
reverse_proxy { reverse_proxy {
to unix/%s to unix/%s
health_uri /health health_uri /health
health_port 2021 health_port 2021
health_interval 2s health_interval 2s
@ -415,14 +416,15 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
} }
`, socketName), "caddyfile") `, 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) { func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.SkipNow() t.SkipNow()
} }
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
f, err := os.CreateTemp("", "*.sock") f, err := os.CreateTemp("", "*.sock")
if err != nil { if err != nil {
t.Errorf("failed to create TempFile: %s", err) t.Errorf("failed to create TempFile: %s", err)
@ -453,18 +455,18 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
}) })
runtime.Gosched() // Allow other goroutines to run runtime.Gosched() // Allow other goroutines to run
tester.InitServer(fmt.Sprintf(` harness.LoadConfig(fmt.Sprintf(`
{ {
skip_install_trust skip_install_trust
admin localhost:2999 admin {$TESTING_CADDY_ADMIN_BIND}
http_port 9080 http_port {$TESTING_CADDY_PORT_ONE}
https_port 9443 https_port {$TESTING_CADDY_PORT_TWO}
grace_period 1ns grace_period 1ns
} }
http://localhost:9080 { http://localhost:{$TESTING_CADDY_PORT_ONE} {
reverse_proxy { reverse_proxy {
to unix/%s to unix/%s
health_uri /health health_uri /health
health_interval 2s health_interval 2s
health_timeout 5s health_timeout 5s
@ -472,5 +474,5 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
} }
`, socketName), "caddyfile") `, socketName), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") harness.AssertGetResponse(fmt.Sprintf("http://localhost:%d/", harness.Tester().PortOne()), 200, "Hello, World!")
} }

View file

@ -1,6 +1,7 @@
package integration package integration
import ( import (
"fmt"
"testing" "testing"
"github.com/caddyserver/caddy/v2/caddytest" "github.com/caddyserver/caddy/v2/caddytest"
@ -8,20 +9,20 @@ import (
func TestDefaultSNI(t *testing.T) { func TestDefaultSNI(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(`{ harness.LoadConfig(`{
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": 9443, "https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":9443" ":{$TESTING_CADDY_PORT_TWO}"
], ],
"routes": [ "routes": [
{ {
@ -102,26 +103,27 @@ func TestDefaultSNI(t *testing.T) {
// act and assert // act and assert
// makes a request with no sni // 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) { func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": 9443, "https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":9443" ":{$TESTING_CADDY_PORT_TWO}"
], ],
"routes": [ "routes": [
{ {
@ -206,26 +208,27 @@ func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// act and assert // act and assert
// makes a request with no sni // 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) { func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": 9443, "https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":9443" ":{$TESTING_CADDY_PORT_TWO}"
], ],
"routes": [ "routes": [
{ {
@ -282,7 +285,8 @@ func TestDefaultSNIWithPortMappingOnly(t *testing.T) {
// act and assert // act and assert
// makes a request with no sni // 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) { func TestHttpOnlyOnDomainWithSNI(t *testing.T) {

View file

@ -20,21 +20,21 @@ import (
// (see https://github.com/caddyserver/caddy/issues/3556 for use case) // (see https://github.com/caddyserver/caddy/issues/3556 for use case)
func TestH2ToH2CStream(t *testing.T) { func TestH2ToH2CStream(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": 9443, "https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":9443" ":{$TESTING_CADDY_PORT_TWO}"
], ],
"routes": [ "routes": [
{ {
@ -102,7 +102,7 @@ func TestH2ToH2CStream(t *testing.T) {
expectedBody := "some data to be echoed" expectedBody := "some data to be echoed"
// start the server // start the server
server := testH2ToH2CStreamServeH2C(t) server := testH2ToH2CStreamServeH2C(harness, t)
go server.ListenAndServe() go server.ListenAndServe()
defer func() { defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
@ -116,7 +116,7 @@ func TestH2ToH2CStream(t *testing.T) {
Body: io.NopCloser(r), Body: io.NopCloser(r),
URL: &url.URL{ URL: &url.URL{
Scheme: "https", Scheme: "https",
Host: "127.0.0.1:9443", Host: fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()),
Path: "/tov2ray", Path: "/tov2ray",
}, },
Proto: "HTTP/2", Proto: "HTTP/2",
@ -127,7 +127,7 @@ func TestH2ToH2CStream(t *testing.T) {
// Disable any compression method from server. // Disable any compression method from server.
req.Header.Set("Accept-Encoding", "identity") req.Header.Set("Accept-Encoding", "identity")
resp := tester.AssertResponseCode(req, http.StatusOK) resp := harness.AssertResponseCode(req, http.StatusOK)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return 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{} h2s := &http2.Server{}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rstring, err := httputil.DumpRequest(r, false) rstring, err := httputil.DumpRequest(r, false)
@ -163,7 +163,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
return 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) t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
@ -204,28 +204,21 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
// (see https://github.com/caddyserver/caddy/issues/3606 for use case) // (see https://github.com/caddyserver/caddy/issues/3606 for use case)
func TestH2ToH1ChunkedResponse(t *testing.T) { func TestH2ToH1ChunkedResponse(t *testing.T) {
tester := caddytest.NewTester(t) harness := caddytest.StartHarness(t)
tester.InitServer(` harness.LoadConfig(`
{ {
"admin": { "admin": {
"listen": "localhost:2999" "listen": "{$TESTING_CADDY_ADMIN_BIND}"
}, },
"logging": {
"logs": {
"default": {
"level": "DEBUG"
}
}
},
"apps": { "apps": {
"http": { "http": {
"http_port": 9080, "http_port": {$TESTING_CADDY_PORT_ONE},
"https_port": 9443, "https_port": {$TESTING_CADDY_PORT_TWO},
"grace_period": 1, "grace_period": 1,
"servers": { "servers": {
"srv0": { "srv0": {
"listen": [ "listen": [
":9443" ":{$TESTING_CADDY_PORT_TWO}"
], ],
"routes": [ "routes": [
{ {
@ -312,7 +305,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
} }
// start the server // start the server
server := testH2ToH1ChunkedResponseServeH1(t) server := testH2ToH1ChunkedResponseServeH1(harness, t)
go server.ListenAndServe() go server.ListenAndServe()
defer func() { defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
@ -326,7 +319,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
Body: io.NopCloser(r), Body: io.NopCloser(r),
URL: &url.URL{ URL: &url.URL{
Scheme: "https", Scheme: "https",
Host: "127.0.0.1:9443", Host: fmt.Sprintf("127.0.0.1:%d", harness.Tester().PortTwo()),
Path: "/tov2ray", Path: "/tov2ray",
}, },
Proto: "HTTP/2", Proto: "HTTP/2",
@ -340,7 +333,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
fmt.Fprint(w, expectedBody) fmt.Fprint(w, expectedBody)
w.Close() w.Close()
}() }()
resp := tester.AssertResponseCode(req, http.StatusOK) resp := harness.AssertResponseCode(req, http.StatusOK)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return return
} }
@ -358,9 +351,9 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
} }
} }
func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server { func testH2ToH1ChunkedResponseServeH1(harness *caddytest.TestHarness, t *testing.T) *http.Server {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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) t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return

View file

@ -0,0 +1,241 @@
package caddytest
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"regexp"
"runtime"
"testing"
"github.com/stretchr/testify/require"
)
// use the convention to replace /[certificatename].[crt|key] with the full path
// this helps reduce the noise in test configurations and also allow this
// to run in any path
func prependCaddyFilePath(rawConfig string) string {
r := matchKey.ReplaceAllString(rawConfig, getIntegrationDir()+"$1")
r = matchCert.ReplaceAllString(r, getIntegrationDir()+"$1")
return r
}
func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic("unable to determine the current file path")
}
return path.Dir(filename)
}
var (
matchKey = regexp.MustCompile(`(/[\w\d\.]+\.key)`)
matchCert = regexp.MustCompile(`(/[\w\d\.]+\.crt)`)
)
type TestHarness struct {
t testing.TB
tester *Tester
}
// StartHarness creates and starts a test harness environment which spans the lifetime a single caddy instance
// This is used for the integration tests
func StartHarness(t *testing.T) *TestHarness {
if testing.Short() {
t.SkipNow()
return nil
}
o := &TestHarness{t: t}
o.init()
return o
}
func (tc *TestHarness) Tester() *Tester {
return tc.tester
}
func (tc *TestHarness) Client() *http.Client {
return tc.tester.Client
}
func (tc *TestHarness) LoadConfig(rawConfig, configType string) {
rawConfig = prependCaddyFilePath(rawConfig)
err := tc.tester.LoadConfig(rawConfig, configType)
require.NoError(tc.t, err)
}
func (tc *TestHarness) init() {
// start the server
tester, err := NewTester()
if err != nil {
tc.t.Errorf("Failed to create caddy tester: %s", err)
return
}
tc.tester = tester
err = tc.tester.LaunchCaddy()
if err != nil {
tc.t.Errorf("Failed to launch caddy server: %s", err)
tc.t.FailNow()
return
}
// cleanup
tc.t.Cleanup(func() {
func() {
if tc.t.Failed() {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.tester.adminPort))
if err != nil {
tc.t.Log("unable to read the current config")
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
var out bytes.Buffer
_ = json.Indent(&out, body, "", " ")
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
}
}()
// shutdown server after extracing the config
err = tc.tester.CleanupCaddy()
if err != nil {
tc.t.Errorf("failed to clean up caddy instance: %s", err)
tc.t.FailNow()
}
})
}
// AssertRedirect makes a request and asserts the redirection happens
func (tc *TestHarness) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
// using the existing client, we override the check redirect policy for this test
old := tc.tester.Client.CheckRedirect
tc.tester.Client.CheckRedirect = redirectPolicyFunc
defer func() { tc.tester.Client.CheckRedirect = old }()
resp, err := tc.tester.Client.Get(requestURI)
if err != nil {
tc.t.Errorf("failed to call server %s", err)
return nil
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", requestURI, expectedStatusCode, resp.StatusCode)
}
loc, err := resp.Location()
if err != nil {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
}
if loc == nil && expectedToLocation != "" {
tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI)
}
if loc != nil {
if expectedToLocation != loc.String() {
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
}
}
return resp
}
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *TestHarness) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.tester.Client.Do(req)
if err != nil {
tc.t.Fatalf("failed to call server %s", err)
}
if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
}
return resp
}
// AssertResponse request a URI and assert the status code and the body contains a string
func (tc *TestHarness) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
tc.t.Fatalf("unable to read the response body %s", err)
}
body := string(bytes)
if body != expectedBody {
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
}
return resp, body
}
// Verb specific test functions
// AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *TestHarness) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *TestHarness) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil {
tc.t.Fatalf("unable to create request %s", err)
}
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *TestHarness) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *TestHarness) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *TestHarness) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil {
tc.t.Errorf("failed to create request %s", err)
return nil, ""
}
applyHeaders(tc.t, req, requestHeaders)
return tc.AssertResponse(req, expectedStatusCode, expectedBody)
}

View file

@ -8,9 +8,10 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
) )
var rootCmd = &cobra.Command{ var defaultFactory = NewRootCommandFactory(func() *cobra.Command {
Use: "caddy", return &cobra.Command{
Long: `Caddy is an extensible server platform written in Go. Use: "caddy",
Long: `Caddy is an extensible server platform written in Go.
At its core, Caddy merely manages configuration. Modules are plugged At its core, Caddy merely manages configuration. Modules are plugged
in statically at compile-time to provide useful functionality. Caddy's 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: Instructions for running Caddy in production are also available:
https://caddyserver.com/docs/running https://caddyserver.com/docs/running
`, `,
Example: ` $ caddy run Example: ` $ caddy run
$ caddy run --config caddy.json $ caddy run --config caddy.json
$ caddy reload --config caddy.json $ caddy reload --config caddy.json
$ caddy stop`, $ caddy stop`,
// kind of annoying to have all the help text printed out if // kind of annoying to have all the help text printed out if
// caddy has an error provisioning its modules, for instance... // caddy has an error provisioning its modules, for instance...
SilenceUsage: true, SilenceUsage: true,
Version: onlyVersionText(), Version: onlyVersionText(),
} }
})
const fullDocsFooter = `Full documentation is available at: const fullDocsFooter = `Full documentation is available at:
https://caddyserver.com/docs/command-line` https://caddyserver.com/docs/command-line`
func init() { func init() {
rootCmd.SetVersionTemplate("{{.Version}}\n") defaultFactory.Use(func(cmd *cobra.Command) {
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") cmd.SetVersionTemplate("{{.Version}}\n")
cmd.SetHelpTemplate(cmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
})
} }
func onlyVersionText() string { func onlyVersionText() string {

28
cmd/commandfactory.go Normal file
View file

@ -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
}

View file

@ -20,6 +20,7 @@ import (
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"errors" "errors"
"flag"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@ -257,6 +258,7 @@ func cmdRun(fl Flags) (int, error) {
// if enabled, reload config file automatically on changes // if enabled, reload config file automatically on changes
// (this better only be used in dev!) // (this better only be used in dev!)
// do not enable this during tests, it will cause leaks
if watchFlag { if watchFlag {
go watchConfigFile(configFile, configAdapterFlag) go watchConfigFile(configFile, configAdapterFlag)
} }
@ -280,7 +282,11 @@ func cmdRun(fl Flags) (int, error) {
} }
} }
select {} if flag.Lookup("test.v") == nil || !strings.Contains(os.Args[0], ".test") {
select {}
} else {
return caddy.ExitCodeSuccess, nil
}
} }
func cmdStop(fl Flags) (int, error) { func cmdStop(fl Flags) (int, error) {

View file

@ -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 { if err := os.MkdirAll(dir, 0o755); err != nil {
return caddy.ExitCodeFailedQuit, err return caddy.ExitCodeFailedQuit, err
} }
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{ ccmd := defaultFactory.Build()
if err := doc.GenManTree(ccmd, &doc.GenManHeader{
Title: "Caddy", Title: "Caddy",
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
}, dir); err != nil { }, 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 // source: https://github.com/spf13/cobra/blob/main/shell_completions.md
rootCmd.AddCommand(&cobra.Command{ defaultFactory.Use(func(ccmd *cobra.Command) {
Use: "completion [bash|zsh|fish|powershell]", ccmd.AddCommand(&cobra.Command{
Short: "Generate completion script", Use: "completion [bash|zsh|fish|powershell]",
Long: fmt.Sprintf(`To load completions: Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
Bash: 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: # To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1 PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile. # and source this file from your PowerShell profile.
`, rootCmd.Root().Name()), `, defaultFactory.constructor().Name()),
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] { switch args[0] {
case "bash": case "bash":
return cmd.Root().GenBashCompletion(os.Stdout) return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh": case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout) return cmd.Root().GenZshCompletion(os.Stdout)
case "fish": case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true) return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell": case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default: default:
return fmt.Errorf("unrecognized shell: %s", args[0]) return fmt.Errorf("unrecognized shell: %s", args[0])
} }
}, },
})
}) })
} }
@ -563,7 +566,9 @@ func RegisterCommand(cmd Command) {
if !commandNameRegex.MatchString(cmd.Name) { if !commandNameRegex.MatchString(cmd.Name) {
panic("invalid command 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]$`) var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)

View file

@ -71,7 +71,7 @@ func Main() {
if err != nil { if err != nil {
caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err)) caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
} }
rootCmd := defaultFactory.Build()
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
var exitError *exitError var exitError *exitError
if errors.As(err, &exitError) { if errors.As(err, &exitError) {
@ -81,6 +81,18 @@ func Main() {
} }
} }
// MainForTesting implements the main function of the caddy command, used internally for testing
func MainForTesting(args ...string) error {
// create a root command for testing which will not pollute the global namespace, and does not
// call os.Exit().
rootCmd := defaultFactory.Build()
rootCmd.SetArgs(args)
if err := rootCmd.Execute(); err != nil {
return err
}
return nil
}
// handlePingbackConn reads from conn and ensures it matches // handlePingbackConn reads from conn and ensures it matches
// the bytes in expect, or returns an error if it doesn't. // the bytes in expect, or returns an error if it doesn't.
func handlePingbackConn(conn net.Conn, expect []byte) error { func handlePingbackConn(conn net.Conn, expect []byte) error {

View file

@ -16,6 +16,7 @@ package caddy
import ( import (
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -699,7 +700,13 @@ type defaultCustomLog struct {
// and enables INFO-level logs and higher. // and enables INFO-level logs and higher.
func newDefaultProductionLog() (*defaultCustomLog, error) { func newDefaultProductionLog() (*defaultCustomLog, error) {
cl := new(CustomLog) 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 var err error
cl.writer, err = cl.writerOpener.OpenWriter() cl.writer, err = cl.writerOpener.OpenWriter()
if err != nil { if err != nil {

View file

@ -15,7 +15,6 @@
package fileserver package fileserver
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -691,10 +690,6 @@ func (fsrv *FileServer) getEtagFromFile(fileSystem fs.FS, filename string) (stri
if err != nil { if err != nil {
return "", fmt.Errorf("cannot read etag from file %s: %v", etagFilename, err) return "", fmt.Errorf("cannot read etag from file %s: %v", etagFilename, err)
} }
// Etags should not contain newline characters
etag = bytes.ReplaceAll(etag, []byte("\n"), []byte{})
return string(etag), nil return string(etag), nil
} }
return "", nil return "", nil