github.com/avenga/couper@v1.12.2/server/http_integration_test.go (about)

     1  package server_test
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"compress/gzip"
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"net/http"
    13  	"net/http/httptest"
    14  	"net/textproto"
    15  	"net/url"
    16  	"os"
    17  	"path"
    18  	"path/filepath"
    19  	"reflect"
    20  	"sort"
    21  	"strconv"
    22  	"strings"
    23  	"sync"
    24  	"testing"
    25  	"text/template"
    26  	"time"
    27  
    28  	"github.com/golang-jwt/jwt/v4"
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/sirupsen/logrus"
    31  	logrustest "github.com/sirupsen/logrus/hooks/test"
    32  
    33  	"github.com/avenga/couper/command"
    34  	"github.com/avenga/couper/config"
    35  	"github.com/avenga/couper/config/configload"
    36  	"github.com/avenga/couper/config/env"
    37  	"github.com/avenga/couper/errors"
    38  	"github.com/avenga/couper/internal/test"
    39  	"github.com/avenga/couper/logging"
    40  	"github.com/avenga/couper/oauth2"
    41  )
    42  
    43  var (
    44  	testBackend    *test.Backend
    45  	testWorkingDir string
    46  	testProxyAddr  = "http://127.0.0.1:9999"
    47  	testServerMu   = sync.Mutex{}
    48  )
    49  
    50  func TestMain(m *testing.M) {
    51  	setup()
    52  	code := m.Run()
    53  	teardown()
    54  	os.Exit(code)
    55  }
    56  
    57  func setup() {
    58  	println("INTEGRATION: create test backend...")
    59  	testBackend = test.NewBackend()
    60  	err := os.Setenv("COUPER_TEST_BACKEND_ADDR", testBackend.Addr())
    61  	if err != nil {
    62  		panic(err)
    63  	}
    64  
    65  	err = os.Setenv("HTTP_PROXY", testProxyAddr)
    66  	if err != nil {
    67  		panic(err)
    68  	}
    69  
    70  	wd, err := os.Getwd()
    71  	if err != nil {
    72  		panic(err)
    73  	}
    74  	testWorkingDir = wd
    75  }
    76  
    77  func teardown() {
    78  	println("INTEGRATION: close test backend...")
    79  	for _, key := range []string{"COUPER_TEST_BACKEND_ADDR", "HTTP_PROXY"} {
    80  		if err := os.Unsetenv(key); err != nil {
    81  			panic(err)
    82  		}
    83  	}
    84  	testBackend.Close()
    85  }
    86  
    87  func newCouper(file string, helper *test.Helper) (func(), *logrustest.Hook) {
    88  	couperConfig, err := configload.LoadFile(filepath.Join(testWorkingDir, file), "test")
    89  	helper.Must(err)
    90  
    91  	return newCouperWithConfig(couperConfig, helper)
    92  }
    93  
    94  func newCouperMultiFiles(file, dir string, helper *test.Helper) (func(), *logrustest.Hook) {
    95  	couperConfig, err := configload.LoadFiles([]string{file, dir}, "test", nil)
    96  	helper.Must(err)
    97  
    98  	return newCouperWithConfig(couperConfig, helper)
    99  }
   100  
   101  // newCouperWithTemplate applies given variables first and loads Couper with the resulting configuration file.
   102  // Example template:
   103  //
   104  //	My {{.message}}
   105  //
   106  // Example value:
   107  //
   108  //	map[string]interface{}{
   109  //		"message": "value",
   110  //	}
   111  func newCouperWithTemplate(file string, helper *test.Helper, vars map[string]interface{}) (func(), *logrustest.Hook, error) {
   112  	if vars == nil {
   113  		s, h := newCouper(file, helper)
   114  		return s, h, nil
   115  	}
   116  
   117  	tpl, err := template.New(filepath.Base(file)).ParseFiles(file)
   118  	helper.Must(err)
   119  
   120  	result := &bytes.Buffer{}
   121  	helper.Must(tpl.Execute(result, vars))
   122  
   123  	return newCouperWithBytes(result.Bytes(), helper)
   124  }
   125  
   126  func newCouperWithBytes(file []byte, helper *test.Helper) (func(), *logrustest.Hook, error) {
   127  	couperConfig, err := configload.LoadBytes(file, "couper-bytes.hcl")
   128  	if err != nil {
   129  		return nil, nil, err
   130  	}
   131  	s, h := newCouperWithConfig(couperConfig, helper)
   132  	return s, h, nil
   133  }
   134  
   135  func newCouperWithConfig(couperConfig *config.Couper, helper *test.Helper) (func(), *logrustest.Hook) {
   136  	testServerMu.Lock()
   137  	defer testServerMu.Unlock()
   138  
   139  	log, hook := test.NewLogger()
   140  	log.Level, _ = logrus.ParseLevel(couperConfig.Settings.LogLevel)
   141  
   142  	ctx, cancelFn := context.WithCancel(context.Background())
   143  	shutdownFn := func() {
   144  		if helper.TestFailed() { // log on error
   145  			time.Sleep(time.Second)
   146  			for _, entry := range hook.AllEntries() {
   147  				s, _ := entry.String()
   148  				helper.Logf(s)
   149  			}
   150  		}
   151  		cleanup(cancelFn, helper)
   152  	}
   153  
   154  	// ensure the previous test aren't listening
   155  	port := couperConfig.Settings.DefaultPort
   156  	test.WaitForClosedPort(port)
   157  	waitForCh := make(chan struct{}, 1)
   158  	command.RunCmdTestCallback = func() {
   159  		waitForCh <- struct{}{}
   160  	}
   161  	defer func() { command.RunCmdTestCallback = nil }()
   162  
   163  	go func() {
   164  		if err := command.NewRun(ctx).Execute(nil, couperConfig, log.WithContext(ctx)); err != nil {
   165  			command.RunCmdTestCallback()
   166  			shutdownFn()
   167  			if lerr, ok := err.(*errors.Error); ok {
   168  				panic(lerr.LogError())
   169  			} else {
   170  				panic(err)
   171  			}
   172  		}
   173  	}()
   174  	<-waitForCh
   175  
   176  	for _, entry := range hook.AllEntries() {
   177  		if entry.Level < logrus.WarnLevel {
   178  			// ignore health-check startup errors
   179  			if req, ok := entry.Data["request"]; ok {
   180  				if reqFields, ok := req.(logging.Fields); ok {
   181  					n := reqFields["name"]
   182  					if hc, ok := n.(string); ok && hc == "health-check" {
   183  						continue
   184  					}
   185  				}
   186  			}
   187  			defer os.Exit(1) // ok in loop, next line is the end
   188  			helper.Must(fmt.Errorf("error: %#v: %s", entry.Data, entry.Message))
   189  		}
   190  	}
   191  
   192  	hook.Reset() // no startup logs
   193  	return shutdownFn, hook
   194  }
   195  
   196  func newClient() *http.Client {
   197  	return test.NewHTTPClient()
   198  }
   199  
   200  func cleanup(shutdown func(), helper *test.Helper) {
   201  	testServerMu.Lock()
   202  	defer testServerMu.Unlock()
   203  
   204  	shutdown()
   205  
   206  	err := os.Chdir(testWorkingDir)
   207  	if err != nil {
   208  		helper.Must(err)
   209  	}
   210  }
   211  
   212  func TestHTTPServer_ServeHTTP(t *testing.T) {
   213  	type testRequest struct {
   214  		method, url string
   215  	}
   216  
   217  	type expectation struct {
   218  		status      int
   219  		body        []byte
   220  		header      http.Header
   221  		handlerName string
   222  	}
   223  
   224  	type requestCase struct {
   225  		req testRequest
   226  		exp expectation
   227  	}
   228  
   229  	type testCase struct {
   230  		fileName string
   231  		requests []requestCase
   232  	}
   233  
   234  	client := newClient()
   235  
   236  	for i, testcase := range []testCase{
   237  		{"spa/01_couper.hcl", []requestCase{
   238  			{
   239  				testRequest{http.MethodGet, "http://anyserver:8080/"},
   240  				expectation{http.StatusOK, []byte(`<html><body><title>1.0</title></body></html>`), nil, "spa"},
   241  			},
   242  			{
   243  				testRequest{http.MethodGet, "http://anyserver:8080/app"},
   244  				expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""},
   245  			},
   246  		}},
   247  		{"files/01_couper.hcl", []requestCase{
   248  			{
   249  				testRequest{http.MethodGet, "http://anyserver:8080/"},
   250  				expectation{http.StatusOK, []byte(`<html lang="en">index</html>`), nil, "file"},
   251  			},
   252  		}},
   253  		{"files/02_couper.hcl", []requestCase{
   254  			{
   255  				testRequest{http.MethodGet, "http://anyserver:8080/a"},
   256  				expectation{http.StatusOK, []byte(`<html lang="en">index A</html>`), nil, "file"},
   257  			},
   258  			{
   259  				testRequest{http.MethodGet, "http://couper.io:9898/a"},
   260  				expectation{http.StatusOK, []byte(`<html lang="en">index A</html>`), nil, "file"},
   261  			},
   262  			{
   263  				testRequest{http.MethodGet, "http://couper.io:9898/"},
   264  				expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""},
   265  			},
   266  			{
   267  				testRequest{http.MethodGet, "http://example.com:9898/b"},
   268  				expectation{http.StatusOK, []byte(`<html lang="en">index B</html>`), nil, "file"},
   269  			},
   270  			{
   271  				testRequest{http.MethodGet, "http://example.com:9898/"},
   272  				expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""},
   273  			},
   274  		}},
   275  		{"files_spa_api/01_couper.hcl", []requestCase{
   276  			{
   277  				testRequest{http.MethodGet, "http://anyserver:8080/"},
   278  				expectation{http.StatusOK, []byte("<html><body><title>SPA_01</title>{\"default\":\"true\"}</body></html>\n"), nil, "spa"},
   279  			},
   280  			{
   281  				testRequest{http.MethodGet, "http://anyserver:8080/foo"},
   282  				expectation{http.StatusOK, []byte("<html><body><title>SPA_01</title>{\"default\":\"true\"}</body></html>\n"), nil, "spa"},
   283  			},
   284  		}},
   285  		{"api/01_couper.hcl", []requestCase{
   286  			{
   287  				testRequest{http.MethodGet, "http://anyserver:8080/"},
   288  				expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""},
   289  			},
   290  			{
   291  				testRequest{http.MethodGet, "http://anyserver:8080/v1"},
   292  				expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"},
   293  			},
   294  			{
   295  				testRequest{http.MethodGet, "http://anyserver:8080/v1/"},
   296  				expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"},
   297  			},
   298  			{
   299  				testRequest{http.MethodGet, "http://anyserver:8080/v1/not-found"},
   300  				expectation{http.StatusNotFound, []byte(`{"message": "route not found error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, ""},
   301  			},
   302  			{
   303  				testRequest{http.MethodGet, "http://anyserver:8080/v1/connect-error/"}, // in this case proxyconnect fails
   304  				expectation{http.StatusBadGateway, []byte(`{"message": "backend error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, "api"},
   305  			},
   306  			{
   307  				testRequest{http.MethodGet, "http://anyserver:8080/v1x"},
   308  				expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""},
   309  			},
   310  		}},
   311  		{"api/02_couper.hcl", []requestCase{
   312  			{
   313  				testRequest{http.MethodGet, "http://anyserver:8080/"},
   314  				expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""},
   315  			},
   316  			{
   317  				testRequest{http.MethodGet, "http://anyserver:8080/v2/"},
   318  				expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"},
   319  			},
   320  			{
   321  				testRequest{http.MethodGet, "http://couper.io:9898/v2/"},
   322  				expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"},
   323  			},
   324  			{
   325  				testRequest{http.MethodGet, "http://example.com:9898/v3/"},
   326  				expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"},
   327  			},
   328  			{
   329  				testRequest{http.MethodGet, "http://anyserver:8080/v2/not-found"},
   330  				expectation{http.StatusNotFound, []byte(`{"message": "route not found error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, ""},
   331  			},
   332  			{
   333  				testRequest{http.MethodGet, "http://couper.io:9898/v2/not-found"},
   334  				expectation{http.StatusNotFound, []byte(`{"message": "route not found error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, ""},
   335  			},
   336  			{
   337  				testRequest{http.MethodGet, "http://example.com:9898/v3/not-found"},
   338  				expectation{http.StatusNotFound, []byte(`{"message": "route not found error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, ""},
   339  			},
   340  		}},
   341  		{"vhosts/01_couper.hcl", []requestCase{
   342  			{
   343  				testRequest{http.MethodGet, "http://anyserver:8080/notfound"},
   344  				expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""},
   345  			},
   346  			{
   347  				testRequest{http.MethodGet, "http://anyserver:8080/"},
   348  				expectation{http.StatusOK, []byte("<html><body><title>FS_01</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"},
   349  			},
   350  			{
   351  				testRequest{http.MethodGet, "http://anyserver:8080/spa1"},
   352  				expectation{http.StatusOK, []byte("<html><body><title>SPA_01</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "spa"},
   353  			},
   354  			{
   355  				testRequest{http.MethodGet, "http://example.com:8080/"},
   356  				expectation{http.StatusOK, []byte("<html><body><title>FS_01</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"},
   357  			},
   358  			{
   359  				testRequest{http.MethodGet, "http://example.org:9876/"},
   360  				expectation{http.StatusOK, []byte("<html><body><title>FS_01</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"},
   361  			},
   362  			{
   363  				testRequest{http.MethodGet, "http://couper.io:8080/"},
   364  				expectation{http.StatusOK, []byte("<html><body><title>FS_02</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"},
   365  			},
   366  			{
   367  				testRequest{http.MethodGet, "http://couper.io:8080/spa2"},
   368  				expectation{http.StatusOK, []byte("<html><body><title>SPA_02</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "spa"},
   369  			},
   370  			{
   371  				testRequest{http.MethodGet, "http://example.net:9876/"},
   372  				expectation{http.StatusOK, []byte("<html><body><title>FS_02</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"},
   373  			},
   374  			{
   375  				testRequest{http.MethodGet, "http://v-server3.com:8080/"},
   376  				expectation{http.StatusOK, []byte("<html><body><title>FS_03</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"},
   377  			},
   378  			{
   379  				testRequest{http.MethodGet, "http://v-server3.com:8080/spa2"},
   380  				expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""},
   381  			},
   382  		}},
   383  		{"endpoint_eval/16_couper.hcl", []requestCase{
   384  			{
   385  				testRequest{http.MethodGet, "http://anyserver:8080/"},
   386  				expectation{http.StatusInternalServerError, []byte("<html>configuration error</html>\n"), http.Header{"Couper-Error": {"configuration error"}}, ""},
   387  			},
   388  		}},
   389  	} {
   390  		confPath := path.Join("testdata/integration", testcase.fileName)
   391  		t.Logf("#%.2d: Create Couper: %q", i+1, confPath)
   392  
   393  		for _, rc := range testcase.requests {
   394  			t.Run(testcase.fileName+" "+rc.req.method+"|"+rc.req.url, func(subT *testing.T) {
   395  				helper := test.New(subT)
   396  				shutdown, logHook := newCouper(confPath, helper)
   397  				defer shutdown()
   398  
   399  				logHook.Reset()
   400  
   401  				req, err := http.NewRequest(rc.req.method, rc.req.url, nil)
   402  				helper.Must(err)
   403  
   404  				res, err := client.Do(req)
   405  				helper.Must(err)
   406  
   407  				resBytes, err := io.ReadAll(res.Body)
   408  				helper.Must(err)
   409  
   410  				_ = res.Body.Close()
   411  
   412  				if res.StatusCode != rc.exp.status {
   413  					subT.Errorf("Expected statusCode %d, got %d", rc.exp.status, res.StatusCode)
   414  					subT.Logf("Failed: %s|%s", testcase.fileName, rc.req.url)
   415  				}
   416  
   417  				for k, v := range rc.exp.header {
   418  					if !reflect.DeepEqual(res.Header[k], v) {
   419  						subT.Errorf("Exptected headers:\nWant:\t%#v\nGot:\t%#v\n", v, res.Header[k])
   420  					}
   421  				}
   422  
   423  				if rc.exp.body != nil && !bytes.Equal(resBytes, rc.exp.body) {
   424  					subT.Errorf("Expected same body content:\nWant:\t%q\nGot:\t%q\n", string(rc.exp.body), string(resBytes))
   425  				}
   426  
   427  				entry := logHook.LastEntry()
   428  
   429  				if entry == nil || entry.Data["type"] != "couper_access" {
   430  					subT.Error("Expected a log entry, got nothing")
   431  					return
   432  				}
   433  				if handler, ok := entry.Data["handler"]; rc.exp.handlerName != "" && (!ok || handler != rc.exp.handlerName) {
   434  					subT.Errorf("Expected handler %q within logs, got:\n%#v", rc.exp.handlerName, entry.Data)
   435  				}
   436  			})
   437  		}
   438  	}
   439  }
   440  
   441  func TestHTTPServer_HostHeader(t *testing.T) {
   442  	helper := test.New(t)
   443  
   444  	client := newClient()
   445  
   446  	confPath := path.Join("testdata/integration", "files/02_couper.hcl")
   447  	shutdown, _ := newCouper(confPath, helper)
   448  	defer shutdown()
   449  
   450  	req, err := http.NewRequest(http.MethodGet, "http://example.com:9898/b", nil)
   451  	helper.Must(err)
   452  
   453  	req.Host = "Example.com."
   454  	res, err := client.Do(req)
   455  	helper.Must(err)
   456  
   457  	resBytes, err := io.ReadAll(res.Body)
   458  	helper.Must(err)
   459  
   460  	_ = res.Body.Close()
   461  
   462  	if string(resBytes) != `<html lang="en">index B</html>` {
   463  		t.Errorf("%s", resBytes)
   464  	}
   465  }
   466  
   467  func TestHTTPServer_HostHeader2(t *testing.T) {
   468  	helper := test.New(t)
   469  
   470  	client := newClient()
   471  
   472  	confPath := path.Join("testdata/integration", "api/03_couper.hcl")
   473  	shutdown, logHook := newCouper(confPath, helper)
   474  	defer shutdown()
   475  
   476  	req, err := http.NewRequest(http.MethodGet, "http://couper.io:9898/v3/def", nil)
   477  	helper.Must(err)
   478  
   479  	req.Host = "couper.io"
   480  	res, err := client.Do(req)
   481  	helper.Must(err)
   482  
   483  	resBytes, err := io.ReadAll(res.Body)
   484  	helper.Must(err)
   485  
   486  	_ = res.Body.Close()
   487  
   488  	if string(resBytes) != "<html>route not found error</html>\n" {
   489  		t.Errorf("%s", resBytes)
   490  	}
   491  
   492  	entry := logHook.LastEntry()
   493  	if entry == nil {
   494  		t.Error("Expected a log entry, got nothing")
   495  	} else if entry.Data["server"] != "multi-api-host1" {
   496  		t.Errorf("Expected 'multi-api-host1', got: %s", entry.Data["server"])
   497  	}
   498  }
   499  
   500  func TestHTTPServer_EnvVars(t *testing.T) {
   501  	helper := test.New(t)
   502  	client := newClient()
   503  
   504  	env.SetTestOsEnviron(func() []string {
   505  		return []string{"BAP1=pass1"}
   506  	})
   507  	defer env.SetTestOsEnviron(os.Environ)
   508  
   509  	shutdown, hook := newCouper("testdata/integration/env/01_couper.hcl", test.New(t))
   510  	defer shutdown()
   511  
   512  	hook.Reset()
   513  
   514  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080", nil)
   515  	helper.Must(err)
   516  
   517  	res, err := client.Do(req)
   518  	helper.Must(err)
   519  
   520  	if res.StatusCode != http.StatusUnauthorized {
   521  		t.Errorf("expected 401, got %d", res.StatusCode)
   522  	}
   523  }
   524  
   525  func TestHTTPServer_DefaultEnvVars(t *testing.T) {
   526  	helper := test.New(t)
   527  	client := newClient()
   528  
   529  	env.SetTestOsEnviron(func() []string {
   530  		return []string{"VALUE_4=value4"}
   531  	})
   532  	defer env.SetTestOsEnviron(os.Environ)
   533  
   534  	shutdown, hook := newCouper("testdata/integration/env/02_couper.hcl", test.New(t))
   535  	defer shutdown()
   536  
   537  	hook.Reset()
   538  
   539  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080", nil)
   540  	helper.Must(err)
   541  
   542  	res, err := client.Do(req)
   543  	helper.Must(err)
   544  
   545  	if res.StatusCode != http.StatusOK {
   546  		t.Errorf("expected 200, got %d", res.StatusCode)
   547  	}
   548  
   549  	b, err := io.ReadAll(res.Body)
   550  	helper.Must(err)
   551  
   552  	var result []string
   553  	helper.Must(json.Unmarshal(b, &result))
   554  
   555  	if diff := cmp.Diff(result, []string{"value1", "", "default_value_3", "value4", "value5"}); diff != "" {
   556  		t.Error(diff)
   557  	}
   558  }
   559  
   560  func TestHTTPServer_XFHHeader(t *testing.T) {
   561  	client := newClient()
   562  
   563  	env.SetTestOsEnviron(func() []string {
   564  		return []string{"COUPER_XFH=true"}
   565  	})
   566  	defer env.SetTestOsEnviron(os.Environ)
   567  
   568  	confPath := path.Join("testdata/integration", "files/02_couper.hcl")
   569  	shutdown, logHook := newCouper(confPath, test.New(t))
   570  	defer shutdown()
   571  
   572  	helper := test.New(t)
   573  	logHook.Reset()
   574  
   575  	req, err := http.NewRequest(http.MethodGet, "http://example.com:9898/b", nil)
   576  	helper.Must(err)
   577  
   578  	req.Host = "example.com"
   579  	req.Header.Set("X-Forwarded-Host", "example.com.")
   580  	res, err := client.Do(req)
   581  	helper.Must(err)
   582  
   583  	resBytes, err := io.ReadAll(res.Body)
   584  	helper.Must(err)
   585  
   586  	_ = res.Body.Close()
   587  
   588  	if string(resBytes) != `<html lang="en">index B</html>` {
   589  		t.Errorf("%s", resBytes)
   590  	}
   591  
   592  	entry := logHook.LastEntry()
   593  	if entry == nil {
   594  		t.Error("Expected a log entry, got nothing")
   595  	} else if entry.Data["server"] != "multi-files-host2" {
   596  		t.Errorf("Expected 'multi-files-host2', got: %s", entry.Data["server"])
   597  	} else if entry.Data["url"] != "http://example.com:9898/b" {
   598  		t.Errorf("Expected 'http://example.com:9898/b', got: %s", entry.Data["url"])
   599  	}
   600  }
   601  
   602  func TestHTTPServer_ProxyFromEnv(t *testing.T) {
   603  	helper := test.New(t)
   604  
   605  	seen := make(chan struct{})
   606  	origin := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
   607  		rw.WriteHeader(http.StatusNoContent)
   608  		go func() {
   609  			seen <- struct{}{}
   610  		}()
   611  	}))
   612  	ln, err := net.Listen("tcp4", testProxyAddr[7:])
   613  	helper.Must(err)
   614  	origin.Listener = ln
   615  	origin.Start()
   616  	defer func() {
   617  		origin.Close()
   618  		ln.Close()
   619  		time.Sleep(time.Second)
   620  	}()
   621  
   622  	confPath := path.Join("testdata/integration", "api/01_couper.hcl")
   623  	shutdown, _ := newCouper(confPath, test.New(t))
   624  	defer shutdown()
   625  
   626  	req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/v1/proxy", nil)
   627  	helper.Must(err)
   628  
   629  	_, err = newClient().Do(req)
   630  	helper.Must(err)
   631  
   632  	timer := time.NewTimer(time.Second)
   633  	select {
   634  	case <-timer.C:
   635  		t.Error("Missing proxy call")
   636  	case <-seen:
   637  	}
   638  }
   639  
   640  func TestHTTPServer_Gzip(t *testing.T) {
   641  	client := newClient()
   642  
   643  	confPath := path.Join("testdata/integration", "files/03_gzip.hcl")
   644  	shutdown, _ := newCouper(confPath, test.New(t))
   645  	defer shutdown()
   646  
   647  	type testCase struct {
   648  		name                 string
   649  		headerAcceptEncoding string
   650  		path                 string
   651  		expectGzipResponse   bool
   652  	}
   653  
   654  	for _, tc := range []testCase{
   655  		{"with mixed header AE gzip", "br, gzip", "/index.html", true},
   656  		{"with header AE gzip", "gzip", "/index.html", true},
   657  		{"with header AE and without gzip", "deflate", "/index.html", false},
   658  		{"with header AE and space", " ", "/index.html", false},
   659  	} {
   660  		t.Run(tc.name, func(subT *testing.T) {
   661  			helper := test.New(subT)
   662  
   663  			req, err := http.NewRequest(http.MethodGet, "http://example.org:9898"+tc.path, nil)
   664  			helper.Must(err)
   665  
   666  			if tc.headerAcceptEncoding != "" {
   667  				req.Header.Set("Accept-Encoding", tc.headerAcceptEncoding)
   668  			}
   669  
   670  			res, err := client.Do(req)
   671  			helper.Must(err)
   672  
   673  			var body io.Reader
   674  			body = res.Body
   675  
   676  			if !tc.expectGzipResponse {
   677  				if val := res.Header.Get("Content-Encoding"); val != "" {
   678  					subT.Errorf("Expected no header with key Content-Encoding, got value: %s", val)
   679  				}
   680  			} else {
   681  				if ce := res.Header.Get("Content-Encoding"); ce != "gzip" {
   682  					subT.Errorf("Expected Content-Encoding header value: %q, got: %q", "gzip", ce)
   683  				}
   684  
   685  				body, err = gzip.NewReader(res.Body)
   686  				helper.Must(err)
   687  			}
   688  
   689  			if vr := res.Header.Get("Vary"); vr != "Accept-Encoding" {
   690  				subT.Errorf("Expected Accept-Encoding header value %q, got: %q", "Vary", vr)
   691  			}
   692  
   693  			resBytes, err := io.ReadAll(body)
   694  			helper.Must(err)
   695  
   696  			srcBytes, err := os.ReadFile(filepath.Join(testWorkingDir, "testdata/integration/files/htdocs_c_gzip"+tc.path))
   697  			helper.Must(err)
   698  
   699  			if !bytes.Equal(resBytes, srcBytes) {
   700  				subT.Errorf("Want:\n%s\nGot:\n%s", string(srcBytes), string(resBytes))
   701  			}
   702  		})
   703  	}
   704  }
   705  
   706  func TestHTTPServer_QueryParams(t *testing.T) {
   707  	client := newClient()
   708  
   709  	const confPath = "testdata/integration/endpoint_eval/"
   710  
   711  	type expectation struct {
   712  		Query url.Values
   713  		Path  string
   714  	}
   715  
   716  	type testCase struct {
   717  		file  string
   718  		query string
   719  		exp   expectation
   720  	}
   721  
   722  	for _, tc := range []testCase{
   723  		{"04_couper.hcl", "a=b%20c&aeb_del=1&ae_del=1&CaseIns=1&caseIns=1&def_del=1&xyz=123", expectation{
   724  			Query: url.Values{
   725  				"a":           []string{"b c"},
   726  				"ae_a_and_b":  []string{"A&B", "A&B"},
   727  				"ae_empty":    []string{"", ""},
   728  				"ae_multi":    []string{"str1", "str2", "str3", "str4"},
   729  				"ae_string":   []string{"str", "str"},
   730  				"ae":          []string{"ae", "ae"},
   731  				"aeb_a_and_b": []string{"A&B", "A&B"},
   732  				"aeb_empty":   []string{"", ""},
   733  				"aeb_multi":   []string{"str1", "str2", "str3", "str4"},
   734  				"aeb_string":  []string{"str", "str"},
   735  				"aeb":         []string{"aeb", "aeb"},
   736  				"caseIns":     []string{"1"},
   737  				"def_del":     []string{"1"},
   738  				"xxx":         []string{"aaa", "bbb"},
   739  			},
   740  			Path: "/",
   741  		}},
   742  		{"05_couper.hcl", "", expectation{
   743  			Query: url.Values{
   744  				"ae":  []string{"ae"},
   745  				"def": []string{"def"},
   746  			},
   747  			Path: "/xxx",
   748  		}},
   749  		{"06_couper.hcl", "", expectation{
   750  			Query: url.Values{
   751  				"ae":  []string{"ae"},
   752  				"def": []string{"def"},
   753  			},
   754  			Path: "/zzz",
   755  		}},
   756  		{"07_couper.hcl", "", expectation{
   757  			Query: url.Values{
   758  				"ae":  []string{"ae"},
   759  				"def": []string{"def"},
   760  			},
   761  			Path: "/xxx",
   762  		}},
   763  		{"09_couper.hcl", "", expectation{
   764  			Query: url.Values{
   765  				"test": []string{"pest"},
   766  			},
   767  			Path: "/",
   768  		}},
   769  	} {
   770  		t.Run("_"+tc.query, func(subT *testing.T) {
   771  			helper := test.New(subT)
   772  
   773  			shutdown, _ := newCouper(path.Join(confPath, tc.file), helper)
   774  			defer shutdown()
   775  
   776  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080?"+tc.query, nil)
   777  			helper.Must(err)
   778  
   779  			req.Header.Set("ae", "ae")
   780  			req.Header.Set("aeb", "aeb")
   781  			req.Header.Set("def", "def")
   782  			req.Header.Set("xyz", "xyz")
   783  
   784  			res, err := client.Do(req)
   785  			helper.Must(err)
   786  
   787  			resBytes, err := io.ReadAll(res.Body)
   788  			helper.Must(err)
   789  
   790  			_ = res.Body.Close()
   791  
   792  			var jsonResult expectation
   793  			err = json.Unmarshal(resBytes, &jsonResult)
   794  			if err != nil {
   795  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
   796  			}
   797  
   798  			if !reflect.DeepEqual(jsonResult, tc.exp) {
   799  				subT.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes))
   800  			}
   801  		})
   802  	}
   803  }
   804  
   805  func TestHTTPServer_PathPrefix(t *testing.T) {
   806  	client := newClient()
   807  
   808  	type expectation struct {
   809  		Path string
   810  	}
   811  
   812  	type testCase struct {
   813  		path string
   814  		exp  expectation
   815  	}
   816  
   817  	for _, tc := range []testCase{
   818  		{"/v1", expectation{
   819  			Path: "/xxx/xxx/v1",
   820  		}},
   821  		{"/v1/vvv/foo", expectation{
   822  			Path: "/xxx/xxx/api/foo",
   823  		}},
   824  		{"/v2/yyy", expectation{
   825  			Path: "/v2/yyy",
   826  		}},
   827  		{"/v3/zzz", expectation{
   828  			Path: "/zzz/v3/zzz",
   829  		}},
   830  	} {
   831  		t.Run("_"+tc.path, func(subT *testing.T) {
   832  			helper := test.New(subT)
   833  
   834  			shutdown, _ := newCouper("testdata/integration/api/06_couper.hcl", helper)
   835  			defer shutdown()
   836  
   837  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
   838  			helper.Must(err)
   839  
   840  			// Test dynamic values in conf
   841  			if strings.HasPrefix(tc.exp.Path, "/xxx") {
   842  				req.Header.Set("X-Val", "xxx")
   843  			}
   844  
   845  			res, err := client.Do(req)
   846  			helper.Must(err)
   847  
   848  			resBytes, err := io.ReadAll(res.Body)
   849  			helper.Must(err)
   850  
   851  			_ = res.Body.Close()
   852  
   853  			var jsonResult expectation
   854  			err = json.Unmarshal(resBytes, &jsonResult)
   855  			if err != nil {
   856  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
   857  			}
   858  
   859  			if !reflect.DeepEqual(jsonResult, tc.exp) {
   860  				subT.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes))
   861  			}
   862  		})
   863  	}
   864  }
   865  
   866  func TestHTTPServer_BackendLogPath(t *testing.T) {
   867  	client := newClient()
   868  	helper := test.New(t)
   869  
   870  	shutdown, hook := newCouper("testdata/integration/api/07_couper.hcl", helper)
   871  	defer shutdown()
   872  
   873  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil)
   874  	helper.Must(err)
   875  
   876  	hook.Reset()
   877  	_, err = client.Do(req)
   878  	helper.Must(err)
   879  
   880  	if p := hook.AllEntries()[0].Data["request"].(logging.Fields)["path"]; p != "/path?query" {
   881  		t.Errorf("Unexpected path given: %s", p)
   882  	}
   883  }
   884  
   885  func TestHTTPServer_BackendLogRequestProto(t *testing.T) {
   886  	client := newClient()
   887  	helper := test.New(t)
   888  
   889  	shutdown, hook := newCouper("testdata/integration/api/15_couper.hcl", helper)
   890  	defer shutdown()
   891  
   892  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil)
   893  	helper.Must(err)
   894  
   895  	hook.Reset()
   896  	_, err = client.Do(req)
   897  	helper.Must(err)
   898  
   899  	var backendLogsSeen int
   900  	for _, entry := range hook.AllEntries() {
   901  		if entry.Data["type"] == "couper_access" {
   902  			continue
   903  		}
   904  
   905  		backendLogsSeen++
   906  
   907  		if p := entry.Data["request"].(logging.Fields)["proto"]; p != "http" {
   908  			t.Errorf("want proto http, got: %q", p)
   909  		}
   910  	}
   911  
   912  	if backendLogsSeen != 2 {
   913  		t.Error("expected two backend request logs")
   914  	}
   915  }
   916  
   917  func TestHTTPServer_PathInvalidFragment(t *testing.T) {
   918  	client := newClient()
   919  	helper := test.New(t)
   920  
   921  	shutdown, hook := newCouper("testdata/integration/api/09_couper.hcl", helper)
   922  	defer shutdown()
   923  
   924  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil)
   925  	helper.Must(err)
   926  
   927  	hook.Reset()
   928  	_, err = client.Do(req)
   929  	helper.Must(err)
   930  
   931  	if m := hook.AllEntries()[0].Message; m != "configuration error: path attribute: invalid fragment found in \"/path#xxx\"" {
   932  		t.Errorf("Unexpected message given: %s", m)
   933  	}
   934  }
   935  
   936  func TestHTTPServer_PathInvalidQuery(t *testing.T) {
   937  	client := newClient()
   938  	helper := test.New(t)
   939  
   940  	shutdown, hook := newCouper("testdata/integration/api/10_couper.hcl", helper)
   941  	defer shutdown()
   942  
   943  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil)
   944  	helper.Must(err)
   945  
   946  	hook.Reset()
   947  	_, err = client.Do(req)
   948  	helper.Must(err)
   949  
   950  	if m := hook.AllEntries()[0].Message; m != "configuration error: path attribute: invalid query string found in \"/path?xxx\"" {
   951  		t.Errorf("Unexpected message given: %s", m)
   952  	}
   953  }
   954  
   955  func TestHTTPServer_PathPrefixInvalidFragment(t *testing.T) {
   956  	client := newClient()
   957  	helper := test.New(t)
   958  
   959  	shutdown, hook := newCouper("testdata/integration/api/11_couper.hcl", helper)
   960  	defer shutdown()
   961  
   962  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil)
   963  	helper.Must(err)
   964  
   965  	hook.Reset()
   966  	_, err = client.Do(req)
   967  	helper.Must(err)
   968  
   969  	if m := hook.AllEntries()[0].Message; m != "configuration error: path_prefix attribute: invalid fragment found in \"/path#xxx\"" {
   970  		t.Errorf("Unexpected message given: %s", m)
   971  	}
   972  }
   973  
   974  func TestHTTPServer_PathPrefixInvalidQuery(t *testing.T) {
   975  	client := newClient()
   976  	helper := test.New(t)
   977  
   978  	shutdown, hook := newCouper("testdata/integration/api/12_couper.hcl", helper)
   979  	defer shutdown()
   980  
   981  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil)
   982  	helper.Must(err)
   983  
   984  	hook.Reset()
   985  	_, err = client.Do(req)
   986  	helper.Must(err)
   987  
   988  	if m := hook.AllEntries()[0].Message; m != "configuration error: path_prefix attribute: invalid query string found in \"/path?xxx\"" {
   989  		t.Errorf("Unexpected message given: %s", m)
   990  	}
   991  }
   992  
   993  func TestHTTPServer_RequestHeaders(t *testing.T) {
   994  	client := newClient()
   995  
   996  	const confPath = "testdata/integration/endpoint_eval/"
   997  
   998  	type expectation struct {
   999  		Headers http.Header
  1000  	}
  1001  
  1002  	type testCase struct {
  1003  		file  string
  1004  		query string
  1005  		exp   expectation
  1006  	}
  1007  
  1008  	for _, tc := range []testCase{
  1009  		{"12_couper.hcl", "ae=ae&aeb=aeb&def=def&xyz=xyz", expectation{
  1010  			Headers: http.Header{
  1011  				"Aeb":         []string{"aeb", "aeb"},
  1012  				"Aeb_a_and_b": []string{"A&B", "A&B"},
  1013  				"Aeb_empty":   []string{"", ""},
  1014  				"Aeb_multi":   []string{"str1", "str2", "str3", "str4"},
  1015  				"Aeb_string":  []string{"str", "str"},
  1016  				"Xxx":         []string{"aaa", "bbb"},
  1017  			},
  1018  		}},
  1019  	} {
  1020  		t.Run("_"+tc.query, func(subT *testing.T) {
  1021  			helper := test.New(subT)
  1022  			shutdown, _ := newCouper(path.Join(confPath, tc.file), helper)
  1023  			defer shutdown()
  1024  
  1025  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080?"+tc.query, nil)
  1026  			helper.Must(err)
  1027  
  1028  			res, err := client.Do(req)
  1029  			helper.Must(err)
  1030  
  1031  			if r1 := res.Header.Get("Remove-Me-1"); r1 != "r1" {
  1032  				subT.Errorf("Missing or invalid header Remove-Me-1: %s", r1)
  1033  			}
  1034  			if r2 := res.Header.Get("Remove-Me-2"); r2 != "" {
  1035  				subT.Errorf("Unexpected header %s", r2)
  1036  			}
  1037  
  1038  			if s2 := res.Header.Get("Set-Me-2"); s2 != "s2" {
  1039  				subT.Errorf("Missing or invalid header Set-Me-2: %s", s2)
  1040  			}
  1041  
  1042  			if a2 := res.Header.Get("Add-Me-2"); a2 != "a2" {
  1043  				subT.Errorf("Missing or invalid header Add-Me-2: %s", a2)
  1044  			}
  1045  
  1046  			resBytes, err := io.ReadAll(res.Body)
  1047  			helper.Must(err)
  1048  
  1049  			_ = res.Body.Close()
  1050  
  1051  			var jsonResult expectation
  1052  			err = json.Unmarshal(resBytes, &jsonResult)
  1053  			if err != nil {
  1054  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  1055  			}
  1056  
  1057  			jsonResult.Headers.Del("User-Agent")
  1058  			jsonResult.Headers.Del("X-Forwarded-For")
  1059  			jsonResult.Headers.Del("Couper-Request-Id")
  1060  
  1061  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  1062  				subT.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes))
  1063  			}
  1064  		})
  1065  	}
  1066  }
  1067  
  1068  func TestHTTPServer_LogFields(t *testing.T) {
  1069  	client := newClient()
  1070  	conf := "testdata/integration/endpoint_eval/10_couper.hcl"
  1071  
  1072  	helper := test.New(t)
  1073  	shutdown, logHook := newCouper(conf, helper)
  1074  	defer shutdown()
  1075  
  1076  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080", nil)
  1077  	helper.Must(err)
  1078  
  1079  	res, err := client.Do(req)
  1080  	helper.Must(err)
  1081  
  1082  	entries := logHook.AllEntries()
  1083  	if l := len(entries); l != 2 {
  1084  		t.Fatalf("Unexpected number of log lines: %d", l)
  1085  	}
  1086  
  1087  	resBytes, err := io.ReadAll(res.Body)
  1088  	helper.Must(err)
  1089  	helper.Must(res.Body.Close())
  1090  
  1091  	backendLog := entries[0]
  1092  	accessLog := entries[1]
  1093  
  1094  	if tp, ok := backendLog.Data["type"]; !ok || tp != "couper_backend" {
  1095  		t.Fatalf("Unexpected log type: %s", tp)
  1096  	}
  1097  	if tp, ok := accessLog.Data["type"]; !ok || tp != "couper_access" {
  1098  		t.Fatalf("Unexpected log type: %s", tp)
  1099  	}
  1100  
  1101  	if u, ok := backendLog.Data["url"]; !ok || u == "" {
  1102  		t.Fatalf("Unexpected URL: %s", u)
  1103  	}
  1104  	if u, ok := accessLog.Data["url"]; !ok || u == "" {
  1105  		t.Fatalf("Unexpected URL: %s", u)
  1106  	}
  1107  
  1108  	if b, ok := backendLog.Data["backend"]; !ok || b != "anything" {
  1109  		t.Fatalf("Unexpected backend name: %s", b)
  1110  	}
  1111  	if e, ok := accessLog.Data["endpoint"]; !ok || e != "/" {
  1112  		t.Fatalf("Unexpected endpoint: %s", e)
  1113  	}
  1114  
  1115  	if b, ok := accessLog.Data["response"].(logging.Fields)["bytes"]; !ok || b != len(resBytes) {
  1116  		t.Fatalf("Unexpected number of bytes: %d\npayload: %s", b, string(resBytes))
  1117  	}
  1118  }
  1119  
  1120  func TestHTTPServer_QueryEncoding(t *testing.T) {
  1121  	client := newClient()
  1122  
  1123  	conf := "testdata/integration/endpoint_eval/10_couper.hcl"
  1124  
  1125  	type expectation struct {
  1126  		RawQuery string
  1127  	}
  1128  
  1129  	helper := test.New(t)
  1130  	shutdown, _ := newCouper(conf, helper)
  1131  	defer shutdown()
  1132  
  1133  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080?a=a%20a&x=x+x", nil)
  1134  	helper.Must(err)
  1135  
  1136  	res, err := client.Do(req)
  1137  	helper.Must(err)
  1138  
  1139  	resBytes, err := io.ReadAll(res.Body)
  1140  	helper.Must(err)
  1141  
  1142  	_ = res.Body.Close()
  1143  
  1144  	var jsonResult expectation
  1145  	err = json.Unmarshal(resBytes, &jsonResult)
  1146  	if err != nil {
  1147  		t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  1148  	}
  1149  
  1150  	exp := expectation{RawQuery: "a=a%20a&space=a%20b%2Bc&x=x%2Bx"}
  1151  	if !reflect.DeepEqual(jsonResult, exp) {
  1152  		t.Errorf("\nwant: \n%#v\ngot: \n%#v", exp, jsonResult)
  1153  	}
  1154  }
  1155  
  1156  func TestHTTPServer_Backends(t *testing.T) {
  1157  	client := newClient()
  1158  
  1159  	configPath := "testdata/integration/config/02_couper.hcl"
  1160  
  1161  	helper := test.New(t)
  1162  	shutdown, _ := newCouper(configPath, helper)
  1163  	defer shutdown()
  1164  
  1165  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil)
  1166  	helper.Must(err)
  1167  
  1168  	res, err := client.Do(req)
  1169  	helper.Must(err)
  1170  
  1171  	exp := []string{"1", "4"}
  1172  	if !reflect.DeepEqual(res.Header.Values("Foo"), exp) {
  1173  		t.Errorf("\nwant: \n%#v\ngot: \n%#v", exp, res.Header.Values("Foo"))
  1174  	}
  1175  }
  1176  
  1177  func TestHTTPServer_Backends_Reference(t *testing.T) {
  1178  	client := newClient()
  1179  
  1180  	configPath := "testdata/integration/config/04_couper.hcl"
  1181  
  1182  	helper := test.New(t)
  1183  	shutdown, _ := newCouper(configPath, helper)
  1184  	defer shutdown()
  1185  
  1186  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil)
  1187  	helper.Must(err)
  1188  
  1189  	res, err := client.Do(req)
  1190  	helper.Must(err)
  1191  
  1192  	if res.Header.Get("proxy") != "a" || res.Header.Get("request") != "b" {
  1193  		t.Errorf("Expected proxy:a and request:b header values, got: %v", res.Header)
  1194  	}
  1195  }
  1196  
  1197  func TestHTTPServer_Backends_Reference_BasicAuth(t *testing.T) {
  1198  	client := newClient()
  1199  
  1200  	configPath := "testdata/integration/config/13_couper.hcl"
  1201  
  1202  	helper := test.New(t)
  1203  	shutdown, _ := newCouper(configPath, helper)
  1204  	defer shutdown()
  1205  
  1206  	type testcase struct {
  1207  		path     string
  1208  		wantAuth bool
  1209  	}
  1210  
  1211  	for _, tc := range []testcase{
  1212  		{"/", false},
  1213  		{"/granted", true},
  1214  	} {
  1215  		req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil)
  1216  		helper.Must(err)
  1217  
  1218  		res, err := client.Do(req)
  1219  		helper.Must(err)
  1220  
  1221  		b, err := io.ReadAll(res.Body)
  1222  		helper.Must(err)
  1223  
  1224  		helper.Must(res.Body.Close())
  1225  
  1226  		type result struct {
  1227  			Headers http.Header
  1228  		}
  1229  		r := result{}
  1230  		helper.Must(json.Unmarshal(b, &r))
  1231  
  1232  		if tc.wantAuth && !strings.HasPrefix(r.Headers.Get("Authorization"), "Basic ") {
  1233  			t.Error("expected Authorization header value")
  1234  		}
  1235  	}
  1236  }
  1237  
  1238  func TestHTTPServer_Backends_Reference_PathPrefix(t *testing.T) {
  1239  	client := newClient()
  1240  
  1241  	configPath := "testdata/integration/config/12_couper.hcl"
  1242  
  1243  	helper := test.New(t)
  1244  	shutdown, _ := newCouper(configPath, helper)
  1245  	defer shutdown()
  1246  
  1247  	type testcase struct {
  1248  		path       string
  1249  		wantPath   string
  1250  		wantStatus int
  1251  	}
  1252  
  1253  	for _, tc := range []testcase{
  1254  		{"/", "/anything", http.StatusOK},
  1255  		{"/prefixed", "/my-prefix/anything", http.StatusNotFound},
  1256  	} {
  1257  		req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil)
  1258  		helper.Must(err)
  1259  
  1260  		res, err := client.Do(req)
  1261  		helper.Must(err)
  1262  
  1263  		type result struct {
  1264  			Path string
  1265  		}
  1266  
  1267  		b, err := io.ReadAll(res.Body)
  1268  		helper.Must(err)
  1269  
  1270  		helper.Must(res.Body.Close())
  1271  
  1272  		r := result{}
  1273  		helper.Must(json.Unmarshal(b, &r))
  1274  
  1275  		if res.StatusCode != tc.wantStatus {
  1276  			t.Errorf("expected status: %d, got %d", tc.wantStatus, res.StatusCode)
  1277  		}
  1278  
  1279  		if r.Path != tc.wantPath {
  1280  			t.Errorf("expected path: %q, got: %q", tc.wantPath, r.Path)
  1281  		}
  1282  	}
  1283  }
  1284  
  1285  func TestHTTPServer_OriginVsURL(t *testing.T) {
  1286  	client := newClient()
  1287  
  1288  	configPath := "testdata/integration/url/"
  1289  
  1290  	type expectation struct {
  1291  		Path  string
  1292  		Query url.Values
  1293  	}
  1294  
  1295  	type testCase struct {
  1296  		file string
  1297  		exp  expectation
  1298  	}
  1299  
  1300  	for _, tc := range []testCase{
  1301  		{"01_couper.hcl", expectation{
  1302  			Path: "/anything",
  1303  			Query: url.Values{
  1304  				"x": []string{"y"},
  1305  			},
  1306  		}},
  1307  		{"02_couper.hcl", expectation{
  1308  			Path: "/anything",
  1309  			Query: url.Values{
  1310  				"a": []string{"A"},
  1311  			},
  1312  		}},
  1313  		{"03_couper.hcl", expectation{
  1314  			Path: "/anything",
  1315  			Query: url.Values{
  1316  				"a": []string{"A"},
  1317  				"x": []string{"y"},
  1318  			},
  1319  		}},
  1320  		{"04_couper.hcl", expectation{
  1321  			Path: "/anything",
  1322  			Query: url.Values{
  1323  				"a": []string{"A"},
  1324  				"x": []string{"y"},
  1325  			},
  1326  		}},
  1327  		{"05_couper.hcl", expectation{
  1328  			Path: "/anything",
  1329  			Query: url.Values{
  1330  				"a": []string{"A"},
  1331  				"x": []string{"y"},
  1332  			},
  1333  		}},
  1334  		{"06_couper.hcl", expectation{
  1335  			Path: "/anything",
  1336  			Query: url.Values{
  1337  				"a": []string{"A"},
  1338  				"x": []string{"y"},
  1339  			},
  1340  		}},
  1341  	} {
  1342  		t.Run("File "+tc.file, func(subT *testing.T) {
  1343  			helper := test.New(subT)
  1344  
  1345  			shutdown, _ := newCouper(path.Join(configPath, tc.file), helper)
  1346  			defer shutdown()
  1347  
  1348  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080", nil)
  1349  			helper.Must(err)
  1350  
  1351  			res, err := client.Do(req)
  1352  			helper.Must(err)
  1353  
  1354  			resBytes, err := io.ReadAll(res.Body)
  1355  			helper.Must(err)
  1356  			res.Body.Close()
  1357  
  1358  			var jsonResult expectation
  1359  			err = json.Unmarshal(resBytes, &jsonResult)
  1360  			if err != nil {
  1361  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  1362  			}
  1363  
  1364  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  1365  				subT.Errorf("\nwant: \n%#v\ngot: \n%#v", tc.exp, jsonResult)
  1366  			}
  1367  		})
  1368  	}
  1369  }
  1370  
  1371  func TestHTTPServer_TrailingSlash(t *testing.T) {
  1372  	client := newClient()
  1373  
  1374  	conf := "testdata/integration/endpoint_eval/11_couper.hcl"
  1375  
  1376  	type expectation struct {
  1377  		Path string
  1378  	}
  1379  
  1380  	type testCase struct {
  1381  		path string
  1382  		exp  expectation
  1383  	}
  1384  
  1385  	for _, tc := range []testCase{
  1386  		{"/path", expectation{
  1387  			Path: "/path",
  1388  		}},
  1389  		{"/path/", expectation{
  1390  			Path: "/path/",
  1391  		}},
  1392  	} {
  1393  		t.Run("TrailingSlash "+tc.path, func(subT *testing.T) {
  1394  			helper := test.New(subT)
  1395  			shutdown, _ := newCouper(conf, helper)
  1396  			defer shutdown()
  1397  
  1398  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
  1399  			helper.Must(err)
  1400  
  1401  			res, err := client.Do(req)
  1402  			helper.Must(err)
  1403  
  1404  			resBytes, err := io.ReadAll(res.Body)
  1405  			helper.Must(err)
  1406  
  1407  			_ = res.Body.Close()
  1408  
  1409  			var jsonResult expectation
  1410  			err = json.Unmarshal(resBytes, &jsonResult)
  1411  			if err != nil {
  1412  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  1413  			}
  1414  
  1415  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  1416  				subT.Errorf("\nwant: \n%#v\ngot: \n%#v", tc.exp, jsonResult)
  1417  			}
  1418  		})
  1419  	}
  1420  }
  1421  
  1422  func TestHTTPServer_DynamicRequest(t *testing.T) {
  1423  	client := newClient()
  1424  
  1425  	configFile := "testdata/integration/endpoint_eval/13_couper.hcl"
  1426  	shutdown, _ := newCouper(configFile, test.New(t))
  1427  	defer shutdown()
  1428  
  1429  	type expectation struct {
  1430  		Body    string
  1431  		Headers http.Header
  1432  		Method  string
  1433  		Path    string
  1434  		Query   url.Values
  1435  	}
  1436  
  1437  	type testCase struct {
  1438  		exp expectation
  1439  	}
  1440  
  1441  	for _, tc := range []testCase{
  1442  		{expectation{
  1443  			Body:   "body",
  1444  			Method: "PUT",
  1445  			Path:   "/anything",
  1446  			Query: url.Values{
  1447  				"q": []string{"query"},
  1448  			},
  1449  			Headers: http.Header{
  1450  				"Content-Length": []string{"4"},
  1451  				"Content-Type":   []string{"text/plain"},
  1452  				"Test":           []string{"header"},
  1453  			},
  1454  		}},
  1455  	} {
  1456  		t.Run("Dynamic request", func(subT *testing.T) {
  1457  			helper := test.New(subT)
  1458  
  1459  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?method=put", nil)
  1460  			helper.Must(err)
  1461  
  1462  			req.Header.Set("Body", "body")
  1463  			req.Header.Set("Query", "query")
  1464  			req.Header.Set("Test", "header")
  1465  
  1466  			res, err := client.Do(req)
  1467  			helper.Must(err)
  1468  
  1469  			resBytes, err := io.ReadAll(res.Body)
  1470  			helper.Must(err)
  1471  			res.Body.Close()
  1472  
  1473  			var jsonResult expectation
  1474  			err = json.Unmarshal(resBytes, &jsonResult)
  1475  			if err != nil {
  1476  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  1477  			}
  1478  
  1479  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  1480  				subT.Errorf("\nwant: \n%#v\ngot: \n%#v", tc.exp, jsonResult)
  1481  			}
  1482  		})
  1483  	}
  1484  }
  1485  
  1486  func TestHTTPServer_request_bodies(t *testing.T) {
  1487  	client := newClient()
  1488  
  1489  	configFile := "testdata/integration/endpoint_eval/14_couper.hcl"
  1490  	shutdown, _ := newCouper(configFile, test.New(t))
  1491  	defer shutdown()
  1492  
  1493  	type expectation struct {
  1494  		Body    string
  1495  		Args    url.Values
  1496  		Headers http.Header
  1497  		Method  string
  1498  	}
  1499  
  1500  	type testCase struct {
  1501  		path              string
  1502  		clientPayload     string
  1503  		clientContentType string
  1504  		exp               expectation
  1505  	}
  1506  
  1507  	for _, tc := range []testCase{
  1508  		{
  1509  			"/request/body",
  1510  			"",
  1511  			"",
  1512  			expectation{
  1513  				Body:   "foo",
  1514  				Args:   url.Values{},
  1515  				Method: "POST",
  1516  				Headers: http.Header{
  1517  					"Content-Length": []string{"3"},
  1518  					"Content-Type":   []string{"text/plain"},
  1519  				},
  1520  			},
  1521  		},
  1522  		{
  1523  			"/request/body/ct",
  1524  			"",
  1525  			"",
  1526  			expectation{
  1527  				Body:   "foo",
  1528  				Args:   url.Values{},
  1529  				Method: "POST",
  1530  				Headers: http.Header{
  1531  					"Content-Length": []string{"3"},
  1532  					"Content-Type":   []string{"application/foo"},
  1533  				},
  1534  			},
  1535  		},
  1536  		{
  1537  			"/request/json_body/null",
  1538  			"",
  1539  			"",
  1540  			expectation{
  1541  				Body:   "null",
  1542  				Args:   url.Values{},
  1543  				Method: "POST",
  1544  				Headers: http.Header{
  1545  					"Content-Length": []string{"4"},
  1546  					"Content-Type":   []string{"application/json"},
  1547  				},
  1548  			},
  1549  		},
  1550  		{
  1551  			"/request/json_body/boolean",
  1552  			"",
  1553  			"",
  1554  			expectation{
  1555  				Body:   "true",
  1556  				Args:   url.Values{},
  1557  				Method: "POST",
  1558  				Headers: http.Header{
  1559  					"Content-Length": []string{"4"},
  1560  					"Content-Type":   []string{"application/json"},
  1561  				},
  1562  			},
  1563  		},
  1564  		{
  1565  			"/request/json_body/boolean/ct",
  1566  			"",
  1567  			"",
  1568  			expectation{
  1569  				Body:   "true",
  1570  				Args:   url.Values{},
  1571  				Method: "POST",
  1572  				Headers: http.Header{
  1573  					"Content-Length": []string{"4"},
  1574  					"Content-Type":   []string{"application/foo+json"},
  1575  				},
  1576  			},
  1577  		},
  1578  		{
  1579  			"/request/json_body/number",
  1580  			"",
  1581  			"",
  1582  			expectation{
  1583  				Body:   "1.2",
  1584  				Args:   url.Values{},
  1585  				Method: "POST",
  1586  				Headers: http.Header{
  1587  					"Content-Length": []string{"3"},
  1588  					"Content-Type":   []string{"application/json"},
  1589  				},
  1590  			},
  1591  		},
  1592  		{
  1593  			"/request/json_body/string",
  1594  			"",
  1595  			"",
  1596  			expectation{
  1597  				Body:   `"föö"`,
  1598  				Args:   url.Values{},
  1599  				Method: "POST",
  1600  				Headers: http.Header{
  1601  					"Content-Length": []string{"7"},
  1602  					"Content-Type":   []string{"application/json"},
  1603  				},
  1604  			},
  1605  		},
  1606  		{
  1607  			"/request/json_body/object",
  1608  			"",
  1609  			"",
  1610  			expectation{
  1611  				Body:   `{"url":"http://...?foo&bar"}`,
  1612  				Args:   url.Values{},
  1613  				Method: "POST",
  1614  				Headers: http.Header{
  1615  					"Content-Length": []string{"28"},
  1616  					"Content-Type":   []string{"application/json"},
  1617  				},
  1618  			},
  1619  		},
  1620  		{
  1621  			"/request/json_body/object/html",
  1622  			"",
  1623  			"",
  1624  			expectation{
  1625  				Body:   `{"foo":"<p>bar</p>"}`,
  1626  				Args:   url.Values{},
  1627  				Method: "POST",
  1628  				Headers: http.Header{
  1629  					"Content-Length": []string{"20"},
  1630  					"Content-Type":   []string{"application/json"},
  1631  				},
  1632  			},
  1633  		},
  1634  		{
  1635  			"/request/json_body/array",
  1636  			"",
  1637  			"",
  1638  			expectation{
  1639  				Body:   "[0,1,2]",
  1640  				Args:   url.Values{},
  1641  				Method: "POST",
  1642  				Headers: http.Header{
  1643  					"Content-Length": []string{"7"},
  1644  					"Content-Type":   []string{"application/json"},
  1645  				},
  1646  			},
  1647  		},
  1648  		{
  1649  			"/request/json_body/dyn",
  1650  			"true",
  1651  			"application/json",
  1652  			expectation{
  1653  				Body:   "true",
  1654  				Args:   url.Values{},
  1655  				Method: "POST",
  1656  				Headers: http.Header{
  1657  					"Content-Length": []string{"4"},
  1658  					"Content-Type":   []string{"application/json"},
  1659  				},
  1660  			},
  1661  		},
  1662  		{
  1663  			"/request/json_body/dyn",
  1664  			"1.23",
  1665  			"application/json",
  1666  			expectation{
  1667  				Body:   "1.23",
  1668  				Args:   url.Values{},
  1669  				Method: "POST",
  1670  				Headers: http.Header{
  1671  					"Content-Length": []string{"4"},
  1672  					"Content-Type":   []string{"application/json"},
  1673  				},
  1674  			},
  1675  		},
  1676  		{
  1677  			"/request/json_body/dyn",
  1678  			"\"ab\"",
  1679  			"application/json",
  1680  			expectation{
  1681  				Body:   "\"ab\"",
  1682  				Args:   url.Values{},
  1683  				Method: "POST",
  1684  				Headers: http.Header{
  1685  					"Content-Length": []string{"4"},
  1686  					"Content-Type":   []string{"application/json"},
  1687  				},
  1688  			},
  1689  		},
  1690  		{
  1691  			"/request/json_body/dyn",
  1692  			"{\"a\":3,\"b\":[]}",
  1693  			"application/json",
  1694  			expectation{
  1695  				Body:   "{\"a\":3,\"b\":[]}",
  1696  				Args:   url.Values{},
  1697  				Method: "POST",
  1698  				Headers: http.Header{
  1699  					"Content-Length": []string{"14"},
  1700  					"Content-Type":   []string{"application/json"},
  1701  				},
  1702  			},
  1703  		},
  1704  		{
  1705  			"/request/json_body/dyn",
  1706  			"[0,1]",
  1707  			"application/json",
  1708  			expectation{
  1709  				Body:   "[0,1]",
  1710  				Args:   url.Values{},
  1711  				Method: "POST",
  1712  				Headers: http.Header{
  1713  					"Content-Length": []string{"5"},
  1714  					"Content-Type":   []string{"application/json"},
  1715  				},
  1716  			},
  1717  		},
  1718  		{
  1719  			"/request/form_body",
  1720  			"",
  1721  			"",
  1722  			expectation{
  1723  				Body: "",
  1724  				Args: url.Values{
  1725  					"foo": []string{"ab c"},
  1726  					"bar": []string{",:/"},
  1727  				},
  1728  				Method: "POST",
  1729  				Headers: http.Header{
  1730  					"Content-Length": []string{"22"},
  1731  					"Content-Type":   []string{"application/x-www-form-urlencoded"},
  1732  				},
  1733  			},
  1734  		},
  1735  		{
  1736  			"/request/form_body/ct",
  1737  			"",
  1738  			"",
  1739  			expectation{
  1740  				Body:   "bar=%2C%3A%2F&foo=ab+c",
  1741  				Args:   url.Values{},
  1742  				Method: "POST",
  1743  				Headers: http.Header{
  1744  					"Content-Length": []string{"22"},
  1745  					"Content-Type":   []string{"application/my-form-urlencoded"},
  1746  				},
  1747  			},
  1748  		},
  1749  		{
  1750  			"/request/form_body/dyn",
  1751  			"bar=%2C&foo=a",
  1752  			"application/x-www-form-urlencoded",
  1753  			expectation{
  1754  				Body: "",
  1755  				Args: url.Values{
  1756  					"foo": []string{"a"},
  1757  					"bar": []string{","},
  1758  				},
  1759  				Method: "POST",
  1760  				Headers: http.Header{
  1761  					"Content-Length": []string{"13"},
  1762  					"Content-Type":   []string{"application/x-www-form-urlencoded"},
  1763  				},
  1764  			},
  1765  		},
  1766  	} {
  1767  		t.Run(tc.path, func(subT *testing.T) {
  1768  			helper := test.New(subT)
  1769  
  1770  			req, err := http.NewRequest(http.MethodPost, "http://example.com:8080"+tc.path, strings.NewReader(tc.clientPayload))
  1771  			helper.Must(err)
  1772  
  1773  			if tc.clientContentType != "" {
  1774  				req.Header.Set("Content-Type", tc.clientContentType)
  1775  			}
  1776  
  1777  			res, err := client.Do(req)
  1778  			helper.Must(err)
  1779  
  1780  			resBytes, err := io.ReadAll(res.Body)
  1781  			helper.Must(err)
  1782  			res.Body.Close()
  1783  
  1784  			var jsonResult expectation
  1785  			err = json.Unmarshal(resBytes, &jsonResult)
  1786  			if err != nil {
  1787  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  1788  			}
  1789  
  1790  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  1791  				subT.Errorf("\nwant: \n%#v\ngot: \n%#v", tc.exp, jsonResult)
  1792  			}
  1793  		})
  1794  	}
  1795  }
  1796  
  1797  func TestHTTPServer_response_bodies(t *testing.T) {
  1798  	client := newClient()
  1799  
  1800  	configFile := "testdata/integration/endpoint_eval/14_couper.hcl"
  1801  	shutdown, _ := newCouper(configFile, test.New(t))
  1802  	defer shutdown()
  1803  
  1804  	type expectation struct {
  1805  		Body        string
  1806  		ContentType string
  1807  	}
  1808  
  1809  	type testCase struct {
  1810  		path string
  1811  		exp  expectation
  1812  	}
  1813  
  1814  	for _, tc := range []testCase{
  1815  		{
  1816  			"/response/body",
  1817  			expectation{
  1818  				Body:        "foo",
  1819  				ContentType: "text/plain",
  1820  			},
  1821  		},
  1822  		{
  1823  			"/response/body/ct",
  1824  			expectation{
  1825  				Body:        "foo",
  1826  				ContentType: "application/foo",
  1827  			},
  1828  		},
  1829  		{
  1830  			"/response/json_body/null",
  1831  			expectation{
  1832  				Body:        "null",
  1833  				ContentType: "application/json",
  1834  			},
  1835  		},
  1836  		{
  1837  			"/response/json_body/boolean",
  1838  			expectation{
  1839  				Body:        "true",
  1840  				ContentType: "application/json",
  1841  			},
  1842  		},
  1843  		{
  1844  			"/response/json_body/boolean/ct",
  1845  			expectation{
  1846  				Body:        "true",
  1847  				ContentType: "application/foo+json",
  1848  			},
  1849  		},
  1850  		{
  1851  			"/response/json_body/number",
  1852  			expectation{
  1853  				Body:        "1.2",
  1854  				ContentType: "application/json",
  1855  			},
  1856  		},
  1857  		{
  1858  			"/response/json_body/string",
  1859  			expectation{
  1860  				Body:        `"foo"`,
  1861  				ContentType: "application/json",
  1862  			},
  1863  		},
  1864  		{
  1865  			"/response/json_body/object",
  1866  			expectation{
  1867  				Body:        `{"foo":"bar"}`,
  1868  				ContentType: "application/json",
  1869  			},
  1870  		},
  1871  		{
  1872  			"/response/json_body/object/html",
  1873  			expectation{
  1874  				Body:        `{"foo":"<p>bar</p>"}`,
  1875  				ContentType: "application/json",
  1876  			},
  1877  		},
  1878  		{
  1879  			"/response/json_body/array",
  1880  			expectation{
  1881  				Body:        "[0,1,2]",
  1882  				ContentType: "application/json",
  1883  			},
  1884  		},
  1885  	} {
  1886  		t.Run(tc.path, func(subT *testing.T) {
  1887  			helper := test.New(subT)
  1888  
  1889  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
  1890  			helper.Must(err)
  1891  
  1892  			res, err := client.Do(req)
  1893  			helper.Must(err)
  1894  
  1895  			resBytes, err := io.ReadAll(res.Body)
  1896  			helper.Must(err)
  1897  			res.Body.Close()
  1898  
  1899  			if string(resBytes) != tc.exp.Body {
  1900  				subT.Errorf("%s: want: %s, got:%s", tc.path, tc.exp.Body, string(resBytes))
  1901  			}
  1902  
  1903  			if ct := res.Header.Get("Content-Type"); ct != tc.exp.ContentType {
  1904  				subT.Errorf("%s: want: %s, got:%s", tc.path, tc.exp.ContentType, ct)
  1905  			}
  1906  		})
  1907  	}
  1908  }
  1909  
  1910  func TestHTTPServer_Endpoint_Evaluation(t *testing.T) {
  1911  	client := newClient()
  1912  
  1913  	confPath := path.Join("testdata/integration/endpoint_eval/01_couper.hcl")
  1914  	shutdown, _ := newCouper(confPath, test.New(t))
  1915  	defer shutdown()
  1916  
  1917  	type expectation struct {
  1918  		Host, Origin, Path string
  1919  	}
  1920  
  1921  	type testCase struct {
  1922  		reqPath string
  1923  		exp     expectation
  1924  	}
  1925  
  1926  	// first traffic pins the origin (transport conf)
  1927  	for _, tc := range []testCase{
  1928  		{"/my-waffik/my.host.de/" + testBackend.Addr()[7:], expectation{
  1929  			Host:   "my.host.de",
  1930  			Origin: testBackend.Addr()[7:],
  1931  			Path:   "/anything",
  1932  		}},
  1933  		{"/my-respo/my.host.com/" + testBackend.Addr()[7:], expectation{
  1934  			Host:   "my.host.de",
  1935  			Origin: testBackend.Addr()[7:],
  1936  			Path:   "/anything",
  1937  		}},
  1938  	} {
  1939  		t.Run("_"+tc.reqPath, func(subT *testing.T) {
  1940  			helper := test.New(subT)
  1941  
  1942  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.reqPath, nil)
  1943  			helper.Must(err)
  1944  
  1945  			res, err := client.Do(req)
  1946  			helper.Must(err)
  1947  
  1948  			resBytes, err := io.ReadAll(res.Body)
  1949  			helper.Must(err)
  1950  
  1951  			_ = res.Body.Close()
  1952  
  1953  			var jsonResult expectation
  1954  			err = json.Unmarshal(resBytes, &jsonResult)
  1955  			if err != nil {
  1956  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  1957  			}
  1958  
  1959  			jsonResult.Origin = res.Header.Get("X-Origin")
  1960  
  1961  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  1962  				subT.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes))
  1963  			}
  1964  		})
  1965  	}
  1966  }
  1967  
  1968  func TestHTTPServer_Endpoint_Response_FormQuery_Evaluation(t *testing.T) {
  1969  	client := newClient()
  1970  
  1971  	confPath := path.Join("testdata/integration/endpoint_eval/15_couper.hcl")
  1972  	shutdown, _ := newCouper(confPath, test.New(t))
  1973  	defer shutdown()
  1974  
  1975  	helper := test.New(t)
  1976  
  1977  	req, err := http.NewRequest(http.MethodPost, "http://example.com:8080/req?foo=bar", strings.NewReader("s=abc123"))
  1978  	helper.Must(err)
  1979  	req.Header.Set("User-Agent", "")
  1980  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  1981  
  1982  	res, err := client.Do(req)
  1983  	helper.Must(err)
  1984  
  1985  	resBytes, err := io.ReadAll(res.Body)
  1986  	helper.Must(err)
  1987  
  1988  	_ = res.Body.Close()
  1989  
  1990  	type Expectation struct {
  1991  		FormBody url.Values  `json:"form_body"`
  1992  		Headers  test.Header `json:"headers"`
  1993  		Method   string      `json:"method"`
  1994  		Query    url.Values  `json:"query"`
  1995  		URL      string      `json:"url"`
  1996  	}
  1997  
  1998  	var jsonResult Expectation
  1999  	err = json.Unmarshal(resBytes, &jsonResult)
  2000  	if err != nil {
  2001  		t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  2002  	}
  2003  
  2004  	delete(jsonResult.Headers, "couper-request-id")
  2005  
  2006  	exp := Expectation{
  2007  		Method: http.MethodPost,
  2008  		FormBody: map[string][]string{
  2009  			"s": {"abc123"},
  2010  		},
  2011  		Headers: map[string]string{
  2012  			"content-length": "8",
  2013  			"content-type":   "application/x-www-form-urlencoded",
  2014  		},
  2015  		Query: map[string][]string{
  2016  			"foo": {"bar"},
  2017  		},
  2018  		URL: "http://example.com:8080/req?foo=bar",
  2019  	}
  2020  	if !reflect.DeepEqual(jsonResult, exp) {
  2021  		t.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", exp, jsonResult, string(resBytes))
  2022  	}
  2023  }
  2024  
  2025  func TestHTTPServer_Endpoint_Response_JSONBody_Evaluation(t *testing.T) {
  2026  	client := newClient()
  2027  
  2028  	confPath := path.Join("testdata/integration/endpoint_eval/15_couper.hcl")
  2029  	shutdown, _ := newCouper(confPath, test.New(t))
  2030  	defer shutdown()
  2031  
  2032  	helper := test.New(t)
  2033  
  2034  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/req?foo=bar", strings.NewReader(`{"data": true}`))
  2035  	helper.Must(err)
  2036  	req.Header.Set("User-Agent", "")
  2037  	req.Header.Set("Content-Type", "application/json")
  2038  
  2039  	res, err := client.Do(req)
  2040  	helper.Must(err)
  2041  
  2042  	resBytes, err := io.ReadAll(res.Body)
  2043  	helper.Must(err)
  2044  
  2045  	_ = res.Body.Close()
  2046  
  2047  	type Expectation struct {
  2048  		JSONBody map[string]interface{} `json:"json_body"`
  2049  		Headers  test.Header            `json:"headers"`
  2050  		Method   string                 `json:"method"`
  2051  		Query    url.Values             `json:"query"`
  2052  		URL      string                 `json:"url"`
  2053  	}
  2054  
  2055  	var jsonResult Expectation
  2056  	err = json.Unmarshal(resBytes, &jsonResult)
  2057  	if err != nil {
  2058  		t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  2059  	}
  2060  
  2061  	delete(jsonResult.Headers, "couper-request-id")
  2062  
  2063  	exp := Expectation{
  2064  		Method: http.MethodGet,
  2065  		JSONBody: map[string]interface{}{
  2066  			"data": true,
  2067  		},
  2068  		Headers: map[string]string{
  2069  			"content-length": "14",
  2070  			"content-type":   "application/json",
  2071  		},
  2072  		Query: map[string][]string{
  2073  			"foo": {"bar"},
  2074  		},
  2075  		URL: "http://example.com:8080/req?foo=bar",
  2076  	}
  2077  	if !reflect.DeepEqual(jsonResult, exp) {
  2078  		t.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", exp, jsonResult, string(resBytes))
  2079  	}
  2080  }
  2081  
  2082  func TestHTTPServer_Endpoint_Response_JSONBody_Array_Evaluation(t *testing.T) {
  2083  	client := newClient()
  2084  
  2085  	confPath := path.Join("testdata/integration/endpoint_eval/15_couper.hcl")
  2086  	shutdown, _ := newCouper(confPath, test.New(t))
  2087  	defer shutdown()
  2088  
  2089  	helper := test.New(t)
  2090  
  2091  	content := `[1, 2, {"data": true}]`
  2092  
  2093  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/req?foo=bar", strings.NewReader(content))
  2094  	helper.Must(err)
  2095  	req.Header.Set("User-Agent", "")
  2096  	req.Header.Set("Content-Type", "application/json")
  2097  
  2098  	res, err := client.Do(req)
  2099  	helper.Must(err)
  2100  
  2101  	resBytes, err := io.ReadAll(res.Body)
  2102  	helper.Must(err)
  2103  
  2104  	_ = res.Body.Close()
  2105  
  2106  	type Expectation struct {
  2107  		JSONBody interface{} `json:"json_body"`
  2108  		Headers  test.Header `json:"headers"`
  2109  		Method   string      `json:"method"`
  2110  		Query    url.Values  `json:"query"`
  2111  		URL      string      `json:"url"`
  2112  	}
  2113  
  2114  	var jsonResult Expectation
  2115  	err = json.Unmarshal(resBytes, &jsonResult)
  2116  	if err != nil {
  2117  		t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  2118  	}
  2119  
  2120  	delete(jsonResult.Headers, "couper-request-id")
  2121  
  2122  	exp := Expectation{
  2123  		Method: http.MethodGet,
  2124  		JSONBody: []interface{}{
  2125  			1,
  2126  			2,
  2127  			map[string]interface{}{
  2128  				"data": true,
  2129  			},
  2130  		},
  2131  		Headers: map[string]string{
  2132  			"content-length": strconv.Itoa(len(content)),
  2133  			"content-type":   "application/json",
  2134  		},
  2135  		Query: map[string][]string{
  2136  			"foo": {"bar"},
  2137  		},
  2138  		URL: "http://example.com:8080/req?foo=bar",
  2139  	}
  2140  
  2141  	if fmt.Sprint(jsonResult) != fmt.Sprint(exp) {
  2142  		t.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", exp, jsonResult, string(resBytes))
  2143  	}
  2144  }
  2145  
  2146  func TestHTTPServer_AcceptingForwardedURL(t *testing.T) {
  2147  	client := newClient()
  2148  
  2149  	confPath := path.Join("testdata/settings/05_couper.hcl")
  2150  	shutdown, hook := newCouper(confPath, test.New(t))
  2151  	defer shutdown()
  2152  
  2153  	type expectation struct {
  2154  		Protocol string `json:"protocol"`
  2155  		Host     string `json:"host"`
  2156  		Port     int    `json:"port"`
  2157  		Origin   string `json:"origin"`
  2158  		URL      string `json:"url"`
  2159  	}
  2160  
  2161  	type testCase struct {
  2162  		name             string
  2163  		header           http.Header
  2164  		exp              expectation
  2165  		wantAccessLogURL string
  2166  	}
  2167  
  2168  	for _, tc := range []testCase{
  2169  		{
  2170  			"no proto, host, or port",
  2171  			http.Header{},
  2172  			expectation{
  2173  				Protocol: "http",
  2174  				Host:     "localhost",
  2175  				Port:     8080,
  2176  				Origin:   "http://localhost:8080",
  2177  				URL:      "http://localhost:8080/path",
  2178  			},
  2179  			"http://localhost:8080/path",
  2180  		},
  2181  		{
  2182  			"port, no proto, no host",
  2183  			http.Header{
  2184  				"X-Forwarded-Port": []string{"8081"},
  2185  			},
  2186  			expectation{
  2187  				Protocol: "http",
  2188  				Host:     "localhost",
  2189  				Port:     8081,
  2190  				Origin:   "http://localhost:8081",
  2191  				URL:      "http://localhost:8081/path",
  2192  			},
  2193  			"http://localhost:8081/path",
  2194  		},
  2195  		{
  2196  			"proto, no host, no port",
  2197  			http.Header{
  2198  				"X-Forwarded-Proto": []string{"https"},
  2199  			},
  2200  			expectation{
  2201  				Protocol: "https",
  2202  				Host:     "localhost",
  2203  				Port:     443,
  2204  				Origin:   "https://localhost",
  2205  				URL:      "https://localhost/path",
  2206  			},
  2207  			"https://localhost/path",
  2208  		},
  2209  		{
  2210  			"proto, host, no port",
  2211  			http.Header{
  2212  				"X-Forwarded-Proto": []string{"https"},
  2213  				"X-Forwarded-Host":  []string{"www.example.com"},
  2214  			},
  2215  			expectation{
  2216  				Protocol: "https",
  2217  				Host:     "www.example.com",
  2218  				Port:     443,
  2219  				Origin:   "https://www.example.com",
  2220  				URL:      "https://www.example.com/path",
  2221  			},
  2222  			"https://www.example.com/path",
  2223  		},
  2224  		{
  2225  			"proto, host with port, no port",
  2226  			http.Header{
  2227  				"X-Forwarded-Proto": []string{"https"},
  2228  				"X-Forwarded-Host":  []string{"www.example.com:8443"},
  2229  			},
  2230  			expectation{
  2231  				Protocol: "https",
  2232  				Host:     "www.example.com",
  2233  				Port:     8443,
  2234  				Origin:   "https://www.example.com:8443",
  2235  				URL:      "https://www.example.com:8443/path",
  2236  			},
  2237  			"https://www.example.com:8443/path",
  2238  		},
  2239  		{
  2240  			"proto, port, no host",
  2241  			http.Header{
  2242  				"X-Forwarded-Proto": []string{"https"},
  2243  				"X-Forwarded-Port":  []string{"8443"},
  2244  			},
  2245  			expectation{
  2246  				Protocol: "https",
  2247  				Host:     "localhost",
  2248  				Port:     8443,
  2249  				Origin:   "https://localhost:8443",
  2250  				URL:      "https://localhost:8443/path",
  2251  			},
  2252  			"https://localhost:8443/path",
  2253  		},
  2254  		{
  2255  			"host, port, no proto",
  2256  			http.Header{
  2257  				"X-Forwarded-Host": []string{"www.example.com"},
  2258  				"X-Forwarded-Port": []string{"8081"},
  2259  			},
  2260  			expectation{
  2261  				Protocol: "http",
  2262  				Host:     "www.example.com",
  2263  				Port:     8081,
  2264  				Origin:   "http://www.example.com:8081",
  2265  				URL:      "http://www.example.com:8081/path",
  2266  			},
  2267  			"http://www.example.com:8081/path",
  2268  		},
  2269  		{
  2270  			"host with port, port, no proto",
  2271  			http.Header{
  2272  				"X-Forwarded-Host": []string{"www.example.com:8081"},
  2273  				"X-Forwarded-Port": []string{"8081"},
  2274  			},
  2275  			expectation{
  2276  				Protocol: "http",
  2277  				Host:     "www.example.com",
  2278  				Port:     8081,
  2279  				Origin:   "http://www.example.com:8081",
  2280  				URL:      "http://www.example.com:8081/path",
  2281  			},
  2282  			"http://www.example.com:8081/path",
  2283  		},
  2284  		{
  2285  			"host with port, different port, no proto",
  2286  			http.Header{
  2287  				"X-Forwarded-Host": []string{"www.example.com:8081"},
  2288  				"X-Forwarded-Port": []string{"8082"},
  2289  			},
  2290  			expectation{
  2291  				Protocol: "http",
  2292  				Host:     "www.example.com",
  2293  				Port:     8082,
  2294  				Origin:   "http://www.example.com:8082",
  2295  				URL:      "http://www.example.com:8082/path",
  2296  			},
  2297  			"http://www.example.com:8082/path",
  2298  		},
  2299  		{
  2300  			"host, no port, no proto",
  2301  			http.Header{
  2302  				"X-Forwarded-Host": []string{"www.example.com"},
  2303  			},
  2304  			expectation{
  2305  				Protocol: "http",
  2306  				Host:     "www.example.com",
  2307  				Port:     8080,
  2308  				Origin:   "http://www.example.com:8080",
  2309  				URL:      "http://www.example.com:8080/path",
  2310  			},
  2311  			"http://www.example.com:8080/path",
  2312  		},
  2313  		{
  2314  			"host with port, no proto, no port",
  2315  			http.Header{
  2316  				"X-Forwarded-Host": []string{"www.example.com:8081"},
  2317  			},
  2318  			expectation{
  2319  				Protocol: "http",
  2320  				Host:     "www.example.com",
  2321  				Port:     8081,
  2322  				Origin:   "http://www.example.com:8081",
  2323  				URL:      "http://www.example.com:8081/path",
  2324  			},
  2325  			"http://www.example.com:8081/path",
  2326  		},
  2327  		{
  2328  			"proto, host, port",
  2329  			http.Header{
  2330  				"X-Forwarded-Proto": []string{"https"},
  2331  				"X-Forwarded-Host":  []string{"www.example.com"},
  2332  				"X-Forwarded-Port":  []string{"8443"},
  2333  			},
  2334  			expectation{
  2335  				Protocol: "https",
  2336  				Host:     "www.example.com",
  2337  				Port:     8443,
  2338  				Origin:   "https://www.example.com:8443",
  2339  				URL:      "https://www.example.com:8443/path",
  2340  			},
  2341  			"https://www.example.com:8443/path",
  2342  		},
  2343  		{
  2344  			"proto, host with port, port",
  2345  			http.Header{
  2346  				"X-Forwarded-Proto": []string{"https"},
  2347  				"X-Forwarded-Host":  []string{"www.example.com:8443"},
  2348  				"X-Forwarded-Port":  []string{"8443"},
  2349  			},
  2350  			expectation{
  2351  				Protocol: "https",
  2352  				Host:     "www.example.com",
  2353  				Port:     8443,
  2354  				Origin:   "https://www.example.com:8443",
  2355  				URL:      "https://www.example.com:8443/path",
  2356  			},
  2357  			"https://www.example.com:8443/path",
  2358  		},
  2359  		{
  2360  			"proto, host with port, different port",
  2361  			http.Header{
  2362  				"X-Forwarded-Proto": []string{"https"},
  2363  				"X-Forwarded-Host":  []string{"www.example.com:8443"},
  2364  				"X-Forwarded-Port":  []string{"9443"},
  2365  			},
  2366  			expectation{
  2367  				Protocol: "https",
  2368  				Host:     "www.example.com",
  2369  				Port:     9443,
  2370  				Origin:   "https://www.example.com:9443",
  2371  				URL:      "https://www.example.com:9443/path",
  2372  			},
  2373  			"https://www.example.com:9443/path",
  2374  		},
  2375  	} {
  2376  		t.Run(tc.name, func(subT *testing.T) {
  2377  			helper := test.New(subT)
  2378  			hook.Reset()
  2379  
  2380  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/path", nil)
  2381  			helper.Must(err)
  2382  			for k, v := range tc.header {
  2383  				req.Header.Set(k, v[0])
  2384  			}
  2385  
  2386  			res, err := client.Do(req)
  2387  			helper.Must(err)
  2388  
  2389  			resBytes, err := io.ReadAll(res.Body)
  2390  			helper.Must(err)
  2391  
  2392  			_ = res.Body.Close()
  2393  
  2394  			var jsonResult expectation
  2395  			err = json.Unmarshal(resBytes, &jsonResult)
  2396  			if err != nil {
  2397  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  2398  			}
  2399  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  2400  				subT.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", tc.exp, jsonResult, string(resBytes))
  2401  			}
  2402  
  2403  			logURL := getAccessLogURL(hook)
  2404  			if logURL != tc.wantAccessLogURL {
  2405  				subT.Errorf("Expected URL: %q, actual: %q", tc.wantAccessLogURL, logURL)
  2406  			}
  2407  		})
  2408  	}
  2409  }
  2410  
  2411  func TestHTTPServer_XFH_AcceptingForwardedURL(t *testing.T) {
  2412  	client := newClient()
  2413  
  2414  	confPath := path.Join("testdata/settings/06_couper.hcl")
  2415  	shutdown, hook := newCouper(confPath, test.New(t))
  2416  	defer shutdown()
  2417  
  2418  	type expectation struct {
  2419  		Protocol string `json:"protocol"`
  2420  		Host     string `json:"host"`
  2421  		Port     int    `json:"port"`
  2422  		Origin   string `json:"origin"`
  2423  		URL      string `json:"url"`
  2424  	}
  2425  
  2426  	type testCase struct {
  2427  		name             string
  2428  		header           http.Header
  2429  		exp              expectation
  2430  		wantAccessLogURL string
  2431  	}
  2432  
  2433  	for _, tc := range []testCase{
  2434  		{
  2435  			"no proto, host, or port",
  2436  			http.Header{},
  2437  			expectation{
  2438  				Protocol: "http",
  2439  				Host:     "localhost",
  2440  				Port:     8080,
  2441  				Origin:   "http://localhost:8080",
  2442  				URL:      "http://localhost:8080/path",
  2443  			},
  2444  			"http://localhost:8080/path",
  2445  		},
  2446  		{
  2447  			"port, no proto, no host",
  2448  			http.Header{
  2449  				"X-Forwarded-Port": []string{"8081"},
  2450  			},
  2451  			expectation{
  2452  				Protocol: "http",
  2453  				Host:     "localhost",
  2454  				Port:     8081,
  2455  				Origin:   "http://localhost:8081",
  2456  				URL:      "http://localhost:8081/path",
  2457  			},
  2458  			"http://localhost:8081/path",
  2459  		},
  2460  		{
  2461  			"proto, no host, no port",
  2462  			http.Header{
  2463  				"X-Forwarded-Proto": []string{"https"},
  2464  			},
  2465  			expectation{
  2466  				Protocol: "https",
  2467  				Host:     "localhost",
  2468  				Port:     443,
  2469  				Origin:   "https://localhost",
  2470  				URL:      "https://localhost/path",
  2471  			},
  2472  			"https://localhost/path",
  2473  		},
  2474  		{
  2475  			"proto, host, no port",
  2476  			http.Header{
  2477  				"X-Forwarded-Proto": []string{"https"},
  2478  				"X-Forwarded-Host":  []string{"www.example.com"},
  2479  			},
  2480  			expectation{
  2481  				Protocol: "https",
  2482  				Host:     "www.example.com",
  2483  				Port:     443,
  2484  				Origin:   "https://www.example.com",
  2485  				URL:      "https://www.example.com/path",
  2486  			},
  2487  			"https://www.example.com/path",
  2488  		},
  2489  		{
  2490  			"proto, host with port, no port",
  2491  			http.Header{
  2492  				"X-Forwarded-Proto": []string{"https"},
  2493  				"X-Forwarded-Host":  []string{"www.example.com:8443"},
  2494  			},
  2495  			expectation{
  2496  				Protocol: "https",
  2497  				Host:     "www.example.com",
  2498  				Port:     443,
  2499  				Origin:   "https://www.example.com",
  2500  				URL:      "https://www.example.com/path",
  2501  			},
  2502  			"https://www.example.com/path",
  2503  		},
  2504  		{
  2505  			"proto, port, no host",
  2506  			http.Header{
  2507  				"X-Forwarded-Proto": []string{"https"},
  2508  				"X-Forwarded-Port":  []string{"8443"},
  2509  			},
  2510  			expectation{
  2511  				Protocol: "https",
  2512  				Host:     "localhost",
  2513  				Port:     8443,
  2514  				Origin:   "https://localhost:8443",
  2515  				URL:      "https://localhost:8443/path",
  2516  			},
  2517  			"https://localhost:8443/path",
  2518  		},
  2519  		{
  2520  			"host, port, no proto",
  2521  			http.Header{
  2522  				"X-Forwarded-Host": []string{"www.example.com"},
  2523  				"X-Forwarded-Port": []string{"8081"},
  2524  			},
  2525  			expectation{
  2526  				Protocol: "http",
  2527  				Host:     "www.example.com",
  2528  				Port:     8081,
  2529  				Origin:   "http://www.example.com:8081",
  2530  				URL:      "http://www.example.com:8081/path",
  2531  			},
  2532  			"http://www.example.com:8081/path",
  2533  		},
  2534  		{
  2535  			"host with port, port, no proto",
  2536  			http.Header{
  2537  				"X-Forwarded-Host": []string{"www.example.com:8081"},
  2538  				"X-Forwarded-Port": []string{"8081"},
  2539  			},
  2540  			expectation{
  2541  				Protocol: "http",
  2542  				Host:     "www.example.com",
  2543  				Port:     8081,
  2544  				Origin:   "http://www.example.com:8081",
  2545  				URL:      "http://www.example.com:8081/path",
  2546  			},
  2547  			"http://www.example.com:8081/path",
  2548  		},
  2549  		{
  2550  			"host with port, different port, no proto",
  2551  			http.Header{
  2552  				"X-Forwarded-Host": []string{"www.example.com:8081"},
  2553  				"X-Forwarded-Port": []string{"8082"},
  2554  			},
  2555  			expectation{
  2556  				Protocol: "http",
  2557  				Host:     "www.example.com",
  2558  				Port:     8082,
  2559  				Origin:   "http://www.example.com:8082",
  2560  				URL:      "http://www.example.com:8082/path",
  2561  			},
  2562  			"http://www.example.com:8082/path",
  2563  		},
  2564  		{
  2565  			"host, no port, no proto",
  2566  			http.Header{
  2567  				"X-Forwarded-Host": []string{"www.example.com"},
  2568  			},
  2569  			expectation{
  2570  				Protocol: "http",
  2571  				Host:     "www.example.com",
  2572  				Port:     8080,
  2573  				Origin:   "http://www.example.com:8080",
  2574  				URL:      "http://www.example.com:8080/path",
  2575  			},
  2576  			"http://www.example.com:8080/path",
  2577  		},
  2578  		{
  2579  			"host with port, no proto, no port",
  2580  			http.Header{
  2581  				"X-Forwarded-Host": []string{"www.example.com:8081"},
  2582  			},
  2583  			expectation{
  2584  				Protocol: "http",
  2585  				Host:     "www.example.com",
  2586  				Port:     8080,
  2587  				Origin:   "http://www.example.com:8080",
  2588  				URL:      "http://www.example.com:8080/path",
  2589  			},
  2590  			"http://www.example.com:8080/path",
  2591  		},
  2592  		{
  2593  			"proto, host, port",
  2594  			http.Header{
  2595  				"X-Forwarded-Proto": []string{"https"},
  2596  				"X-Forwarded-Host":  []string{"www.example.com"},
  2597  				"X-Forwarded-Port":  []string{"8443"},
  2598  			},
  2599  			expectation{
  2600  				Protocol: "https",
  2601  				Host:     "www.example.com",
  2602  				Port:     8443,
  2603  				Origin:   "https://www.example.com:8443",
  2604  				URL:      "https://www.example.com:8443/path",
  2605  			},
  2606  			"https://www.example.com:8443/path",
  2607  		},
  2608  		{
  2609  			"proto, host with port, port",
  2610  			http.Header{
  2611  				"X-Forwarded-Proto": []string{"https"},
  2612  				"X-Forwarded-Host":  []string{"www.example.com:8443"},
  2613  				"X-Forwarded-Port":  []string{"8443"},
  2614  			},
  2615  			expectation{
  2616  				Protocol: "https",
  2617  				Host:     "www.example.com",
  2618  				Port:     8443,
  2619  				Origin:   "https://www.example.com:8443",
  2620  				URL:      "https://www.example.com:8443/path",
  2621  			},
  2622  			"https://www.example.com:8443/path",
  2623  		},
  2624  		{
  2625  			"proto, host with port, different port",
  2626  			http.Header{
  2627  				"X-Forwarded-Proto": []string{"https"},
  2628  				"X-Forwarded-Host":  []string{"www.example.com:8443"},
  2629  				"X-Forwarded-Port":  []string{"9443"},
  2630  			},
  2631  			expectation{
  2632  				Protocol: "https",
  2633  				Host:     "www.example.com",
  2634  				Port:     9443,
  2635  				Origin:   "https://www.example.com:9443",
  2636  				URL:      "https://www.example.com:9443/path",
  2637  			},
  2638  			"https://www.example.com:9443/path",
  2639  		},
  2640  	} {
  2641  		t.Run(tc.name, func(subT *testing.T) {
  2642  			helper := test.New(subT)
  2643  			hook.Reset()
  2644  
  2645  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/path", nil)
  2646  			helper.Must(err)
  2647  			for k, v := range tc.header {
  2648  				req.Header.Set(k, v[0])
  2649  			}
  2650  
  2651  			res, err := client.Do(req)
  2652  			helper.Must(err)
  2653  
  2654  			resBytes, err := io.ReadAll(res.Body)
  2655  			helper.Must(err)
  2656  
  2657  			_ = res.Body.Close()
  2658  
  2659  			var jsonResult expectation
  2660  			err = json.Unmarshal(resBytes, &jsonResult)
  2661  			if err != nil {
  2662  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  2663  			}
  2664  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  2665  				subT.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", tc.exp, jsonResult, string(resBytes))
  2666  			}
  2667  
  2668  			logURL := getAccessLogURL(hook)
  2669  			if logURL != tc.wantAccessLogURL {
  2670  				subT.Errorf("Expected URL: %q, actual: %q", tc.wantAccessLogURL, logURL)
  2671  			}
  2672  		})
  2673  	}
  2674  }
  2675  
  2676  func TestHTTPServer_BackendProbes(t *testing.T) {
  2677  	helper := test.New(t)
  2678  	client := newClient()
  2679  
  2680  	confPath := path.Join("testdata/integration/config/14_couper.hcl")
  2681  	shutdown, _ := newCouper(confPath, helper)
  2682  	defer shutdown()
  2683  
  2684  	type testCase struct {
  2685  		name   string
  2686  		path   string
  2687  		expect string
  2688  	}
  2689  
  2690  	time.Sleep(2 * time.Second)
  2691  	healthyJSON := `{"error":"","healthy":true,"state":"healthy"}`
  2692  
  2693  	for _, tc := range []testCase{
  2694  		{
  2695  			"unknown backend",
  2696  			"/unknown",
  2697  			`null`,
  2698  		},
  2699  		{
  2700  			"healthy backend",
  2701  			"/healthy/default",
  2702  			healthyJSON,
  2703  		},
  2704  		{
  2705  			"healthy backend w/ expected_status",
  2706  			"/healthy/expected_status",
  2707  			healthyJSON,
  2708  		},
  2709  		{
  2710  			"healthy backend w/ expected_text",
  2711  			"/healthy/expected_text",
  2712  			healthyJSON,
  2713  		},
  2714  		{
  2715  			"healthy backend w/ path",
  2716  			"/healthy/path",
  2717  			healthyJSON,
  2718  		},
  2719  		{
  2720  			"healthy backend w/ headers",
  2721  			"/healthy/headers",
  2722  			healthyJSON,
  2723  		},
  2724  		{
  2725  			"healthy backend w/ fallback ua header",
  2726  			"/healthy/ua-header",
  2727  			healthyJSON,
  2728  		},
  2729  		{
  2730  			"healthy backend: check does not follow Location",
  2731  			"/healthy/no_follow_redirect",
  2732  			healthyJSON,
  2733  		},
  2734  		{
  2735  			"unhealthy backend: timeout",
  2736  			"/unhealthy/timeout",
  2737  			`{"error":"backend error: connecting to unhealthy_timeout '1.2.3.4' failed: i/o timeout","healthy":false,"state":"unhealthy"}`,
  2738  		},
  2739  		{
  2740  			"unhealthy backend: unexpected status code",
  2741  			"/unhealthy/bad_status",
  2742  			`{"error":"unexpected status code: 404","healthy":false,"state":"unhealthy"}`,
  2743  		},
  2744  		{
  2745  			"unhealthy backend w/ expected_status: unexpected status code",
  2746  			"/unhealthy/bad_expected_status",
  2747  			`{"error":"unexpected status code: 200","healthy":false,"state":"unhealthy"}`,
  2748  		},
  2749  		{
  2750  			"unhealthy backend w/ expected_text: unexpected text",
  2751  			"/unhealthy/bad_expected_text",
  2752  			`{"error":"unexpected text","healthy":false,"state":"unhealthy"}`,
  2753  		},
  2754  		{
  2755  			"unhealthy backend: unexpected status code",
  2756  			"/unhealthy/bad_status",
  2757  			`{"error":"unexpected status code: 404","healthy":false,"state":"unhealthy"}`,
  2758  		},
  2759  		{
  2760  			"unhealthy backend w/ path: unexpected status code",
  2761  			"/unhealthy/bad_path",
  2762  			`{"error":"unexpected status code: 404","healthy":false,"state":"unhealthy"}`,
  2763  		},
  2764  		{
  2765  			"unhealthy backend w/ headers: unexpected text",
  2766  			"/unhealthy/headers",
  2767  			`{"error":"unexpected text","healthy":false,"state":"unhealthy"}`,
  2768  		},
  2769  		{
  2770  			"unhealthy backend: does not follow location",
  2771  			"/unhealthy/no_follow_redirect",
  2772  			`{"error":"unexpected status code: 302","healthy":false,"state":"unhealthy"}`,
  2773  		},
  2774  		{
  2775  			"backend error: timeout but threshold not reached",
  2776  			"/failing",
  2777  			`{"error":"backend error: connecting to failing '1.2.3.4' failed: i/o timeout","healthy":true,"state":"failing"}`,
  2778  		},
  2779  	} {
  2780  		t.Run(tc.name, func(subT *testing.T) {
  2781  			h := test.New(subT)
  2782  
  2783  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil)
  2784  			h.Must(err)
  2785  
  2786  			res, err := client.Do(req)
  2787  			h.Must(err)
  2788  
  2789  			b, _ := io.ReadAll(res.Body)
  2790  			body := string(b)
  2791  			h.Must(res.Body.Close())
  2792  
  2793  			if body != tc.expect {
  2794  				t.Errorf("%s: Unexpected states:\n\tWant: %s\n\tGot:  %s", tc.name, tc.expect, body)
  2795  			}
  2796  		})
  2797  	}
  2798  }
  2799  
  2800  func TestHTTPServer_backend_requests_variables(t *testing.T) {
  2801  	client := newClient()
  2802  
  2803  	ResourceOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  2804  		rw.WriteHeader(http.StatusNoContent)
  2805  	}))
  2806  	defer ResourceOrigin.Close()
  2807  
  2808  	confPath := path.Join("testdata/integration/endpoint_eval/18_couper.hcl")
  2809  	shutdown, hook, err := newCouperWithTemplate(confPath, test.New(t), map[string]interface{}{"rsOrigin": ResourceOrigin.URL})
  2810  	if err != nil {
  2811  		t.Fatal(err)
  2812  	}
  2813  	defer shutdown()
  2814  
  2815  	type expectation struct {
  2816  		Method   string                 `json:"method"`
  2817  		Protocol string                 `json:"protocol"`
  2818  		Host     string                 `json:"host"`
  2819  		Port     int64                  `json:"port"`
  2820  		Path     string                 `json:"path"`
  2821  		Query    map[string][]string    `json:"query"`
  2822  		Origin   string                 `json:"origin"`
  2823  		URL      string                 `json:"url"`
  2824  		Body     string                 `json:"body"`
  2825  		JSONBody map[string]interface{} `json:"json_body"`
  2826  		FormBody map[string][]string    `json:"form_body"`
  2827  	}
  2828  
  2829  	type testCase struct {
  2830  		name   string
  2831  		relURL string
  2832  		header http.Header
  2833  		body   io.Reader
  2834  		exp    expectation
  2835  	}
  2836  
  2837  	helper := test.New(t)
  2838  	resourceOrigin, perr := url.Parse(ResourceOrigin.URL)
  2839  	helper.Must(perr)
  2840  
  2841  	port, _ := strconv.ParseInt(resourceOrigin.Port(), 10, 64)
  2842  
  2843  	for _, tc := range []testCase{
  2844  		{
  2845  			"body",
  2846  			"/body",
  2847  			http.Header{},
  2848  			strings.NewReader(`abcd1234`),
  2849  			expectation{
  2850  				Method:   http.MethodPost,
  2851  				Protocol: resourceOrigin.Scheme,
  2852  				Host:     resourceOrigin.Hostname(),
  2853  				Port:     port,
  2854  				Path:     "/resource",
  2855  				Query:    map[string][]string{"foo": {"bar"}},
  2856  				Origin:   ResourceOrigin.URL,
  2857  				URL:      ResourceOrigin.URL + "/resource?foo=bar",
  2858  				Body:     "abcd1234",
  2859  				JSONBody: map[string]interface{}{},
  2860  				FormBody: map[string][]string{},
  2861  			},
  2862  		},
  2863  		{
  2864  			"json_body",
  2865  			"/json_body",
  2866  			http.Header{"Content-Type": []string{"application/json"}},
  2867  			strings.NewReader(`{"s":"abcd1234"}`),
  2868  			expectation{
  2869  				Method:   http.MethodPost,
  2870  				Protocol: resourceOrigin.Scheme,
  2871  				Host:     resourceOrigin.Hostname(),
  2872  				Port:     port,
  2873  				Path:     "/resource",
  2874  				Query:    map[string][]string{"foo": {"bar"}},
  2875  				Origin:   ResourceOrigin.URL,
  2876  				URL:      ResourceOrigin.URL + "/resource?foo=bar",
  2877  				Body:     `{"s":"abcd1234"}`,
  2878  				JSONBody: map[string]interface{}{"s": "abcd1234"},
  2879  				FormBody: map[string][]string{},
  2880  			},
  2881  		},
  2882  		{
  2883  			"form_body",
  2884  			"/form_body",
  2885  			http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}},
  2886  			strings.NewReader(`s=abcd1234`),
  2887  			expectation{
  2888  				Method:   http.MethodPost,
  2889  				Protocol: resourceOrigin.Scheme,
  2890  				Host:     resourceOrigin.Hostname(),
  2891  				Port:     port,
  2892  				Path:     "/resource",
  2893  				Query:    map[string][]string{"foo": {"bar"}},
  2894  				Origin:   ResourceOrigin.URL,
  2895  				URL:      ResourceOrigin.URL + "/resource?foo=bar",
  2896  				Body:     `s=abcd1234`,
  2897  				JSONBody: map[string]interface{}{},
  2898  				FormBody: map[string][]string{"s": {"abcd1234"}},
  2899  			},
  2900  		},
  2901  	} {
  2902  		t.Run(tc.name, func(subT *testing.T) {
  2903  			h := test.New(subT)
  2904  			hook.Reset()
  2905  
  2906  			req, err := http.NewRequest(http.MethodPost, "http://localhost:8080"+tc.relURL, tc.body)
  2907  			h.Must(err)
  2908  
  2909  			for k, v := range tc.header {
  2910  				req.Header.Set(k, v[0])
  2911  			}
  2912  
  2913  			res, err := client.Do(req)
  2914  			h.Must(err)
  2915  
  2916  			resBytes, err := io.ReadAll(res.Body)
  2917  			h.Must(err)
  2918  
  2919  			_ = res.Body.Close()
  2920  
  2921  			var jsonResult expectation
  2922  			err = json.Unmarshal(resBytes, &jsonResult)
  2923  			if err != nil {
  2924  				subT.Errorf("%s: unmarshal json: %v: got:\n%s", tc.name, err, string(resBytes))
  2925  			}
  2926  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  2927  				subT.Errorf("%s\nwant:\t%#v\ngot:\t%#v\npayload: %s", tc.name, tc.exp, jsonResult, string(resBytes))
  2928  			}
  2929  		})
  2930  	}
  2931  }
  2932  
  2933  func TestHTTPServer_request_variables(t *testing.T) {
  2934  	client := newClient()
  2935  
  2936  	confPath := path.Join("testdata/integration/endpoint_eval/19_couper.hcl")
  2937  	shutdown, hook := newCouper(confPath, test.New(t))
  2938  	defer shutdown()
  2939  
  2940  	type expectation struct {
  2941  		Method   string                 `json:"method"`
  2942  		Protocol string                 `json:"protocol"`
  2943  		Host     string                 `json:"host"`
  2944  		Port     int64                  `json:"port"`
  2945  		Path     string                 `json:"path"`
  2946  		Query    map[string][]string    `json:"query"`
  2947  		Origin   string                 `json:"origin"`
  2948  		URL      string                 `json:"url"`
  2949  		Body     string                 `json:"body"`
  2950  		JSONBody map[string]interface{} `json:"json_body"`
  2951  		FormBody map[string][]string    `json:"form_body"`
  2952  	}
  2953  
  2954  	type testCase struct {
  2955  		name   string
  2956  		relURL string
  2957  		header http.Header
  2958  		body   io.Reader
  2959  		exp    expectation
  2960  	}
  2961  
  2962  	for _, tc := range []testCase{
  2963  		{
  2964  			"body",
  2965  			"/body?foo=bar",
  2966  			http.Header{},
  2967  			strings.NewReader(`abcd1234`),
  2968  			expectation{
  2969  				Method:   "POST",
  2970  				Protocol: "http",
  2971  				Host:     "localhost",
  2972  				Port:     8080,
  2973  				Path:     "/body",
  2974  				Query:    map[string][]string{"foo": {"bar"}},
  2975  				Origin:   "http://localhost:8080",
  2976  				URL:      "http://localhost:8080/body?foo=bar",
  2977  				Body:     "abcd1234",
  2978  				JSONBody: map[string]interface{}{},
  2979  				FormBody: map[string][]string{},
  2980  			},
  2981  		},
  2982  		{
  2983  			"json_body",
  2984  			"/json_body?foo=bar",
  2985  			http.Header{"Content-Type": []string{"application/json"}},
  2986  			strings.NewReader(`{"s":"abcd1234"}`),
  2987  			expectation{
  2988  				Method:   "POST",
  2989  				Protocol: "http",
  2990  				Host:     "localhost",
  2991  				Port:     8080,
  2992  				Path:     "/json_body",
  2993  				Query:    map[string][]string{"foo": {"bar"}},
  2994  				Origin:   "http://localhost:8080",
  2995  				URL:      "http://localhost:8080/json_body?foo=bar",
  2996  				Body:     `{"s":"abcd1234"}`,
  2997  				JSONBody: map[string]interface{}{"s": "abcd1234"},
  2998  				FormBody: map[string][]string{},
  2999  			},
  3000  		},
  3001  		{
  3002  			"form_body",
  3003  			"/form_body?foo=bar",
  3004  			http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}},
  3005  			strings.NewReader(`s=abcd1234`),
  3006  			expectation{
  3007  				Method:   "POST",
  3008  				Protocol: "http",
  3009  				Host:     "localhost",
  3010  				Port:     8080,
  3011  				Path:     "/form_body",
  3012  				Query:    map[string][]string{"foo": {"bar"}},
  3013  				Origin:   "http://localhost:8080",
  3014  				URL:      "http://localhost:8080/form_body?foo=bar",
  3015  				Body:     `s=abcd1234`,
  3016  				JSONBody: map[string]interface{}{},
  3017  				FormBody: map[string][]string{"s": {"abcd1234"}},
  3018  			},
  3019  		},
  3020  	} {
  3021  		t.Run(tc.name, func(subT *testing.T) {
  3022  			helper := test.New(subT)
  3023  			hook.Reset()
  3024  
  3025  			req, err := http.NewRequest(http.MethodPost, "http://localhost:8080"+tc.relURL, tc.body)
  3026  			helper.Must(err)
  3027  
  3028  			for k, v := range tc.header {
  3029  				req.Header.Set(k, v[0])
  3030  			}
  3031  
  3032  			res, err := client.Do(req)
  3033  			helper.Must(err)
  3034  
  3035  			resBytes, err := io.ReadAll(res.Body)
  3036  			helper.Must(err)
  3037  
  3038  			_ = res.Body.Close()
  3039  
  3040  			var jsonResult expectation
  3041  			err = json.Unmarshal(resBytes, &jsonResult)
  3042  			if err != nil {
  3043  				subT.Errorf("%s: unmarshal json: %v: got:\n%s", tc.name, err, string(resBytes))
  3044  			}
  3045  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  3046  				subT.Errorf("%s\nwant:\t%#v\ngot:\t%#v\npayload: %s", tc.name, tc.exp, jsonResult, string(resBytes))
  3047  			}
  3048  		})
  3049  	}
  3050  }
  3051  
  3052  func TestOpenAPIValidateConcurrentRequests(t *testing.T) {
  3053  	helper := test.New(t)
  3054  	client := newClient()
  3055  
  3056  	shutdown, _ := newCouper("testdata/integration/validation/01_couper.hcl", helper)
  3057  	defer shutdown()
  3058  
  3059  	req1, err := http.NewRequest(http.MethodGet, "http://example.com:8080/anything", nil)
  3060  	helper.Must(err)
  3061  	req2, err := http.NewRequest(http.MethodGet, "http://example.com:8080/pdf", nil)
  3062  	helper.Must(err)
  3063  
  3064  	var res1, res2 *http.Response
  3065  	var err1, err2 error
  3066  	waitCh := make(chan struct{})
  3067  	wg := sync.WaitGroup{}
  3068  	wg.Add(2)
  3069  	go func() {
  3070  		defer wg.Done()
  3071  		<-waitCh // blocks
  3072  		res1, err1 = client.Do(req1)
  3073  	}()
  3074  	go func() {
  3075  		defer wg.Done()
  3076  		<-waitCh // blocks
  3077  		res2, err2 = client.Do(req2)
  3078  	}()
  3079  
  3080  	close(waitCh) // triggers reqs
  3081  	wg.Wait()
  3082  
  3083  	helper.Must(err1)
  3084  	helper.Must(err2)
  3085  
  3086  	if res1.StatusCode != 200 {
  3087  		t.Errorf("Expected status %d for response1; got: %d", 200, res1.StatusCode)
  3088  	}
  3089  	if res2.StatusCode != 502 {
  3090  		t.Errorf("Expected status %d for response2; got: %d", 502, res2.StatusCode)
  3091  	}
  3092  }
  3093  
  3094  func TestOpenAPIValidateRequestResponseBuffer(t *testing.T) {
  3095  	helper := test.New(t)
  3096  
  3097  	content := `{ "prop": true }`
  3098  	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  3099  		b, err := io.ReadAll(req.Body)
  3100  		helper.Must(err)
  3101  		if string(b) != content {
  3102  			t.Errorf("origin: expected same content")
  3103  		}
  3104  		rw.Header().Set("Content-Type", "application/json")
  3105  		_, err = rw.Write([]byte(content))
  3106  		helper.Must(err)
  3107  	}))
  3108  	defer origin.Close()
  3109  
  3110  	shutdown, _ := newCouper("testdata/integration/validation/02_couper.hcl", helper)
  3111  	defer shutdown()
  3112  
  3113  	req, err := http.NewRequest(http.MethodPost, "http://localhost:8080/buffer", bytes.NewBufferString(content))
  3114  	helper.Must(err)
  3115  
  3116  	req.Header.Set("Content-Type", "application/json")
  3117  	req.Header.Set("Origin", origin.URL)
  3118  
  3119  	res, err := test.NewHTTPClient().Do(req)
  3120  	helper.Must(err)
  3121  
  3122  	if res.StatusCode != http.StatusOK {
  3123  		t.Errorf("Expected StatusOK, got: %s", res.Status)
  3124  	}
  3125  
  3126  	b, err := io.ReadAll(res.Body)
  3127  	helper.Must(err)
  3128  
  3129  	helper.Must(res.Body.Close())
  3130  
  3131  	if string(b) != content {
  3132  		t.Error("expected same body content")
  3133  	}
  3134  }
  3135  
  3136  func TestConfigBodyContent(t *testing.T) {
  3137  	helper := test.New(t)
  3138  	client := newClient()
  3139  
  3140  	expiredOrigin, selfSigned := test.NewExpiredBackend()
  3141  	defer expiredOrigin.Close()
  3142  
  3143  	expiredCert, err := os.CreateTemp(os.TempDir(), "expired.pem")
  3144  	helper.Must(err)
  3145  
  3146  	_, err = expiredCert.Write(selfSigned.CACertificate.Certificate)
  3147  	helper.Must(err)
  3148  	helper.Must(expiredCert.Close())
  3149  
  3150  	defer os.RemoveAll(expiredCert.Name())
  3151  
  3152  	shutdown, _, err := newCouperWithTemplate("testdata/integration/config/01_couper.hcl", helper, map[string]interface{}{
  3153  		"expiredOrigin": expiredOrigin.Addr(),
  3154  		"caFile":        expiredCert.Name(),
  3155  	})
  3156  	helper.Must(err)
  3157  	defer shutdown()
  3158  
  3159  	// default port changed in config
  3160  	req, err := http.NewRequest(http.MethodGet, "http://time.out:8090/", nil)
  3161  	helper.Must(err)
  3162  
  3163  	// 2s timeout in config
  3164  	ctx, cancel := context.WithDeadline(req.Context(), time.Now().Add(time.Second*10))
  3165  	defer cancel()
  3166  	*req = *req.Clone(ctx)
  3167  	defer func() {
  3168  		if e := ctx.Err(); e != nil {
  3169  			t.Error("Expected used config timeout instead of deadline timer")
  3170  		}
  3171  	}()
  3172  
  3173  	_, err = client.Do(req)
  3174  	helper.Must(err)
  3175  
  3176  	// disabled cert check in config
  3177  	req, err = http.NewRequest(http.MethodGet, "http://time.out:8090/expired/", nil)
  3178  	helper.Must(err)
  3179  
  3180  	res, err := client.Do(req)
  3181  	helper.Must(err)
  3182  	if res.StatusCode != http.StatusOK {
  3183  		t.Errorf("Expected status OK with disabled certificate validation, got: %q", res.Status)
  3184  	}
  3185  }
  3186  
  3187  func TestConfigBodyContentBackends(t *testing.T) {
  3188  	client := newClient()
  3189  
  3190  	shutdown, _ := newCouper("testdata/integration/config/02_couper.hcl", test.New(t))
  3191  	defer shutdown()
  3192  
  3193  	type testCase struct {
  3194  		path   string
  3195  		header http.Header
  3196  		query  url.Values
  3197  	}
  3198  
  3199  	for _, tc := range []testCase{
  3200  		{"/anything", http.Header{"Foo": []string{"4"}}, url.Values{"bar": []string{"3", "4"}}},
  3201  		{"/get", http.Header{"Foo": []string{"1", "3"}}, url.Values{"bar": []string{"1", "4"}}},
  3202  	} {
  3203  		t.Run(tc.path[1:], func(subT *testing.T) {
  3204  			helper := test.New(subT)
  3205  			req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil)
  3206  			helper.Must(err)
  3207  
  3208  			res, err := client.Do(req)
  3209  			helper.Must(err)
  3210  
  3211  			if res.StatusCode != http.StatusOK {
  3212  				subT.Errorf("%q: expected Status OK, got: %d", tc.path, res.StatusCode)
  3213  			}
  3214  
  3215  			b, err := io.ReadAll(res.Body)
  3216  			helper.Must(err)
  3217  
  3218  			type payload struct {
  3219  				Query url.Values
  3220  			}
  3221  			var p payload
  3222  			helper.Must(json.Unmarshal(b, &p))
  3223  
  3224  			for k, v := range tc.header {
  3225  				if !reflect.DeepEqual(res.Header[k], v) {
  3226  					subT.Errorf("Expected Header %q value: %v, got: %v", k, v, res.Header[k])
  3227  				}
  3228  			}
  3229  
  3230  			for k, v := range tc.query {
  3231  				if !reflect.DeepEqual(p.Query[k], v) {
  3232  					subT.Errorf("Expected Query %q value: %v, got: %v", k, v, p.Query[k])
  3233  				}
  3234  			}
  3235  		})
  3236  	}
  3237  }
  3238  
  3239  func TestConfigBodyContentAccessControl(t *testing.T) {
  3240  	client := newClient()
  3241  
  3242  	shutdown, hook := newCouper("testdata/integration/config/03_couper.hcl", test.New(t))
  3243  	defer shutdown()
  3244  
  3245  	type testCase struct {
  3246  		path       string
  3247  		header     http.Header
  3248  		status     int
  3249  		ct         string
  3250  		wantErrLog string
  3251  	}
  3252  
  3253  	for _, tc := range []testCase{
  3254  		{"/v1", http.Header{"Auth": []string{"ba1"}}, http.StatusOK, "application/json", ""},
  3255  		{"/v2", http.Header{"Authorization": []string{"Basic OmFzZGY="}, "Auth": []string{"ba1", "ba2"}}, http.StatusOK, "application/json", ""}, // minimum ':'
  3256  		{"/v2", http.Header{}, http.StatusUnauthorized, "application/json", "access control error: ba1: credentials required"},
  3257  		{"/v3", http.Header{}, http.StatusOK, "application/json", ""},
  3258  		{"/status", http.Header{}, http.StatusOK, "application/json", ""},
  3259  		{"/superadmin", http.Header{"Authorization": []string{"Basic OmFzZGY="}, "Auth": []string{"ba1", "ba4"}}, http.StatusOK, "application/json", ""},
  3260  		{"/superadmin", http.Header{}, http.StatusUnauthorized, "application/json", "access control error: ba1: credentials required"},
  3261  		{"/ba5", http.Header{"Authorization": []string{"Basic VVNSOlBXRA=="}, "X-Ba-User": []string{"USR"}}, http.StatusOK, "application/json", ""},
  3262  		{"/v4", http.Header{}, http.StatusUnauthorized, "text/html", "access control error: ba1: credentials required"},
  3263  	} {
  3264  		t.Run(tc.path[1:], func(subT *testing.T) {
  3265  			helper := test.New(subT)
  3266  			hook.Reset()
  3267  
  3268  			req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil)
  3269  			helper.Must(err)
  3270  
  3271  			if val := tc.header.Get("Authorization"); val != "" {
  3272  				req.Header.Set("Authorization", val)
  3273  			}
  3274  
  3275  			res, err := client.Do(req)
  3276  			helper.Must(err)
  3277  			// t.Errorf(">>> %#v", res.Header)
  3278  
  3279  			message := getFirstAccessLogMessage(hook)
  3280  			if tc.wantErrLog == "" {
  3281  				if message != "" {
  3282  					subT.Errorf("Expected error log: %q, actual: %#v", tc.wantErrLog, message)
  3283  				}
  3284  			} else {
  3285  				if message != tc.wantErrLog {
  3286  					subT.Errorf("Expected error log message: %q, actual: %#v", tc.wantErrLog, message)
  3287  				}
  3288  			}
  3289  
  3290  			if res.StatusCode != tc.status {
  3291  				subT.Fatalf("%q: expected Status %d, got: %d", tc.path, tc.status, res.StatusCode)
  3292  			}
  3293  
  3294  			if ct := res.Header.Get("Content-Type"); ct != tc.ct {
  3295  				subT.Fatalf("%q: expected content-type: %q, got: %q", tc.path, tc.ct, ct)
  3296  			}
  3297  
  3298  			if tc.ct == "text/html" {
  3299  				return
  3300  			}
  3301  
  3302  			b, err := io.ReadAll(res.Body)
  3303  			helper.Must(err)
  3304  
  3305  			type payload struct {
  3306  				Headers http.Header
  3307  			}
  3308  			var p payload
  3309  			helper.Must(json.Unmarshal(b, &p))
  3310  
  3311  			for k, v := range tc.header {
  3312  				if _, ok := p.Headers[k]; !ok {
  3313  					subT.Errorf("Expected header %q, got nothing", k)
  3314  					break
  3315  				}
  3316  				if !reflect.DeepEqual(p.Headers[k], v) {
  3317  					subT.Errorf("Expected header %q value: %v, got: %v", k, v, p.Headers[k])
  3318  				}
  3319  			}
  3320  		})
  3321  	}
  3322  }
  3323  
  3324  func TestAPICatchAll(t *testing.T) {
  3325  	client := newClient()
  3326  
  3327  	shutdown, hook := newCouper("testdata/integration/config/03_couper.hcl", test.New(t))
  3328  	defer shutdown()
  3329  
  3330  	type testCase struct {
  3331  		name       string
  3332  		path       string
  3333  		method     string
  3334  		header     http.Header
  3335  		status     int
  3336  		wantErrLog string
  3337  	}
  3338  
  3339  	for _, tc := range []testCase{
  3340  		{"exists, authorized", "/v5/exists", http.MethodGet, http.Header{"Authorization": []string{"Basic OmFzZGY="}}, http.StatusOK, ""},
  3341  		{"exists, unauthorized", "/v5/exists", http.MethodGet, http.Header{}, http.StatusUnauthorized, "access control error: ba1: credentials required"},
  3342  		{"exists, CORS pre-flight", "/v5/exists", http.MethodOptions, http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}}, http.StatusNoContent, ""},
  3343  		{"not-exist, authorized", "/v5/not-exist", http.MethodGet, http.Header{"Authorization": []string{"Basic OmFzZGY="}}, http.StatusNotFound, "route not found error"},
  3344  		{"not-exist, unauthorized", "/v5/not-exist", http.MethodGet, http.Header{}, http.StatusUnauthorized, "access control error: ba1: credentials required"},
  3345  		{"not-exist, non-standard method, authorized", "/v5/not-exist", "BREW", http.Header{"Authorization": []string{"Basic OmFzZGY="}}, http.StatusMethodNotAllowed, "method not allowed error"},
  3346  		{"not-exist, non-standard method, unauthorized", "/v5/not-exist", "BREW", http.Header{}, http.StatusUnauthorized, "access control error: ba1: credentials required"},
  3347  		{"not-exist, CORS pre-flight", "/v5/not-exist", http.MethodOptions, http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}}, http.StatusNoContent, ""},
  3348  	} {
  3349  		t.Run(tc.name, func(subT *testing.T) {
  3350  			helper := test.New(subT)
  3351  			hook.Reset()
  3352  
  3353  			req, err := http.NewRequest(tc.method, "http://back.end:8080"+tc.path, nil)
  3354  			helper.Must(err)
  3355  
  3356  			req.Header = tc.header
  3357  
  3358  			res, err := client.Do(req)
  3359  			helper.Must(err)
  3360  
  3361  			message := getFirstAccessLogMessage(hook)
  3362  			if tc.wantErrLog == "" {
  3363  				if message != "" {
  3364  					subT.Errorf("Expected error log: %q, actual: %#v", tc.wantErrLog, message)
  3365  				}
  3366  			} else {
  3367  				if message != tc.wantErrLog {
  3368  					subT.Errorf("Expected error log message: %q, actual: %#v", tc.wantErrLog, message)
  3369  				}
  3370  			}
  3371  
  3372  			if res.StatusCode != tc.status {
  3373  				subT.Fatalf("%q: expected Status %d, got: %d", tc.path, tc.status, res.StatusCode)
  3374  			}
  3375  		})
  3376  	}
  3377  }
  3378  
  3379  func Test_LoadAccessControl(t *testing.T) {
  3380  	// Tests the config load with ACs and "error_handler" blocks...
  3381  	backend := test.NewBackend()
  3382  	defer backend.Close()
  3383  
  3384  	shutdown, _, err := newCouperWithTemplate("testdata/integration/config/07_couper.hcl", test.New(t), map[string]interface{}{
  3385  		"asOrigin": backend.Addr(),
  3386  	})
  3387  	if err != nil {
  3388  		t.Fatal(err)
  3389  	}
  3390  
  3391  	test.WaitForOpenPort(8080)
  3392  	shutdown()
  3393  }
  3394  
  3395  func TestJWTAccessControl(t *testing.T) {
  3396  	client := newClient()
  3397  
  3398  	shutdown, hook := newCouper("testdata/integration/config/03_couper.hcl", test.New(t))
  3399  	defer shutdown()
  3400  
  3401  	type testCase struct {
  3402  		name       string
  3403  		path       string
  3404  		header     http.Header
  3405  		body       string
  3406  		status     int
  3407  		expPerm    string
  3408  		wantErrLog string
  3409  	}
  3410  
  3411  	tokenRequest, reqerr := http.NewRequest(http.MethodGet, "http://back.end:8080/jwt/create?type=ECDSAToken", nil)
  3412  	if reqerr != nil {
  3413  		t.Fatal(reqerr)
  3414  	}
  3415  	tokenResponse, resperr := client.Do(tokenRequest)
  3416  	if reqerr != nil {
  3417  		t.Fatal(resperr)
  3418  	}
  3419  	bytes, _ := io.ReadAll(tokenResponse.Body)
  3420  	localToken := string(bytes)
  3421  
  3422  	// RSA tokens created with server/testdata/integration/files/pkcs8.key
  3423  	// ECDSA tokens created with server/testdata/integration/files/ecdsa.key
  3424  	rsaToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTB9.AZ0gZVqPe9TjjjJO0GnlTvERBXhPyxW_gTn050rCoEkseFRlp4TYry7WTQ7J4HNrH3btfxaEQLtTv7KooVLXQyMDujQbKU6cyuYH6MZXaM0Co3Bhu0awoX-2GVk997-7kMZx2yvwIR5ypd1CERIbNs5QcQaI4sqx_8oGrjO5ZmOWRqSpi4Mb8gJEVVccxurPu65gPFq9esVWwTf4cMQ3GGzijatnGDbRWs_igVGf8IAfmiROSVd17fShQtfthOFd19TGUswVAleOftC7-DDeJgAK8Un5xOHGRjv3ypK_6ZLRonhswaGXxovE0kLq4ZSzumQY2hOFE6x_BbrR1WKtGw"
  3425  	hmacToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGUiOiJmb28gYmFyIiwiaWF0IjoxNTE2MjM5MDIyfQ.7wz7Z7IajfEpwYayfshag6tQVS0e0zZJyjAhuFC0L-E"
  3426  	ecdsaToken := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImVzMjU2In0.eyJzdWIiOjEyMzQ1Njc4OTB9.jXsNtPUXxBi8Bz2i2Maj9lzbB1ebQDmz8TU6GSs6G0yzq9YguXm_HQuwsg4ZTbPER3bpXH_cxz9eEZHUBXfWzw"
  3427  	ecdsaToken2 := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImVzMjU2LWNydi14LXkifQ.eyJzdWIiOjEyMzQ1Njc4OTB9.4-uNC6KGkSY1YYAmGoR-naUu2-Rxo6HSzEkecb7Ua9FVkif0X2gC55DpPU06_HH-yfK-dFozLwzuV2AT6ouOIg"
  3428  
  3429  	for _, tc := range []testCase{
  3430  		{"no token", "/jwt", http.Header{}, "", http.StatusUnauthorized, "", "access control error: JWTToken: token required"},
  3431  		{"expired token", "/jwt", http.Header{"Authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjEyMzQ1Njc4OSwic2NvcGUiOlsiZm9vIiwiYmFyIl19.W2ziH_V33JkOA5ttQhzWN96RqxFydmx7GHY6G__U9HM"}}, "", http.StatusUnauthorized, "", "access control error: JWTToken: Token is expired"},
  3432  		{"valid token", "/jwt", http.Header{"Authorization": []string{"Bearer " + hmacToken}}, "", http.StatusOK, `["foo","bar"]`, ""},
  3433  		{"RSA JWT", "/jwt/rsa", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""},
  3434  		{"RSA JWT PKCS1", "/jwt/rsa/pkcs1", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""},
  3435  		{"RSA JWT PKCS8", "/jwt/rsa/pkcs8", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""},
  3436  		{"RSA JWT bad algorithm", "/jwt/rsa/bad", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusUnauthorized, "", "access control error: RSATokenWrongAlgorithm: signing method RS256 is invalid"},
  3437  		{"local RSA JWKS without kid", "/jwks/rsa", http.Header{"Authorization": []string{"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEyMzQ1Njc4OTB9.V9skZUql-mHqwOzVdzamqAOWSx8fjEA-6py0nfxLRSl7h1bQvqUCWMZUAkMJK6RuJ3y5YAr8ZBXZsh4rwABp_3hitQitMXnV6nr5qfzVDE9-mdS4--Bj46-JlkHacNcK24qlnn_EXGJlzCj6VFgjObSy6geaTY9iDVF6EzjZkxc1H75XRlNYAMu-0KCGfKdte0qASeBKrWnoFNEpnXZ_jhqRRNVkaSBj7_HPXD6oPqKBQf6Jh6fGgdz6q4KNL-t-Qa2_eKc8tkrYNdTdxco-ufmmLiUQ_MzRAqowHb2LdsFJP9rN2QT8MGjRXqGvkCd0EsLfqAeCPkTXs1kN8LGlvw"}}, "", http.StatusUnauthorized, "", `access control error: JWKS: no matching RS256 JWK for kid ""`},
  3438  		{"local RSA JWKS with unsupported kid", "/jwks/rsa", http.Header{"Authorization": []string{"Bearer eyJraWQiOiJyczI1Ni11bnN1cHBvcnRlZCIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTB9.wx1MkMgJhh6gnOvvrnnkRpEUDe-0KpKWw9ZIfDVHtGkuL46AktBgfbaW1ttB78wWrIW9OPfpLqKwkPizwfShoXKF9qN-6TlhPSWIUh0_kBHEj7H4u45YZXH1Ha-r9kGzly1PmLx7gzxUqRpqYnwo0TzZSEr_a8rpfWaC0ZJl3CKARormeF3tzW_ARHnGUqck4VjPfX50Ot6B5nool6qmsCQLLmDECIKBDzZicqdeWH7JPvRZx45R5ZHJRQpD3Z2iqVIF177Wj1C8q75Gxj2PXziIVKplmIUrKN-elYj3kBtJkDFneb384FPLuzsQZOR6HQmKXG2nA1WOfsblJSz3FA"}}, "", http.StatusUnauthorized, "", `access control error: JWKS: no matching RS256 JWK for kid "rs256-unsupported"`},
  3439  		{"local RSA JWKS with non-parsable cert", "/jwks/rsa", http.Header{"Authorization": []string{"Bearer eyJraWQiOiJyczI1Ni13cm9uZy1jZXJ0IiwiYWxnIjoiUlMyNTYiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOjEyMzQ1Njc4OTB9.n--6mjzfnPKbaYAquBK3v6gsbmvEofSprk3jwWGSKPdDt2VpVOe8ZNtGhJj_3f1h86-wg-gEQT5GhJmsI47X9MJ70j74dqhXUF6w4782OljstP955whuSM9hJAIvUw_WV1sqtkiESA-CZiNJIBydL5YzV2nO3gfEYdy9EdMJ2ykGLRBajRxhShxsfaZykFKvvWpy1LbUc-gfRZ4q8Hs9B7b_9RGdbpRwBtwiqPPzhjC5O86vk7ZoiG9Gq7pg52yEkLqdN4a5QkfP8nNeTTMAsqPQL1-1TAC7rIGekoUtoINRR-cewPpZ_E7JVxXvBVvPe3gX_2NzGtXkLg5QDt6RzQ"}}, "", http.StatusUnauthorized, "", `access control error: JWKS: no matching RS256 JWK for kid "rs256-wrong-cert"`},
  3440  		{"local RSA JWKS not found", "/jwks/rsa/not_found", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusUnauthorized, "", `access control error: JWKS_not_found: received no valid JWKs data: <nil>, status code 404`},
  3441  		{"local RSA JWKS", "/jwks/rsa", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""},
  3442  		{"local RSA JWKS with scope", "/jwks/rsa/scope", http.Header{"Authorization": []string{"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTAsInNjb3BlIjpbImZvbyIsImJhciJdfQ.IFqIF_9ELXl3A-oy52G0Sg5f34ah3araOxFboskEw110nXdb_-UuxCnG0naFVFje7xvNrGbJgVAbBRX1v1I_to4BR8RzvIh2hi5IgBmqclIYsYbVWlEhsvjBhFR2b90Rz0APUdfgHp-nvgLB13jxm8f4TRr4ZDnvUQdZp3vI5PMj9optEmlZvexkNLDQLrBvoGCfVHodZyPQMLNVKp0TXWksPT-bw0E7Lq1GeYe2eU0GwHx8fugo2-v44dfCp0RXYYG6bI_Z-U3KZpvdj05n2_UDgTJFFm4c5i9UjILvlO73QJpMNi5eBjerm2alTisSCoiCtfgIgVsM8yHoomgarg"}}, "", http.StatusOK, `["foo","bar"]`, ""},
  3443  		{"remote RSA JWKS x5c", "/jwks/rsa/remote", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""},
  3444  		{"remote RSA JWKS x5c w/ backend", "/jwks/rsa/backend", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""},
  3445  		{"remote RSA JWKS x5c w/ backendref", "/jwks/rsa/backendref", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""},
  3446  		{"remote RSA JWKS n, e", "/jwks/rsa/remote", http.Header{"Authorization": []string{"Bearer eyJraWQiOiJyczI1Ni1uZSIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTB9.aGOhlWQIZvnwoEZGDBYhkkEduIVa59G57x88L3fiLc1MuWbYS84nHEZnlPDuVJ3_BxdXr6-nZ8gpk1C9vfamDzkbvzbdcJ2FzmvAONm1II3_u5OTc6ZtpREDx9ohlIvkcOcalOUhQLqU5r2uik2bGSVV3vFDbqxQeuNzh49i3VgdtwoaryNYSzbg_Ki8dHiaFrWH-r2WCU08utqpFmNdr8oNw4Y5AYJdUW2aItxDbwJ6YLBJN0_6EApbXsNqiaNXkLws3cxMvczGKODyGGVCPENa-VmTQ41HxsXB-_rMmcnMw3_MjyIueWcjeP8BNvLYt1bKFWdU0NcYCkXvEqE4-g"}}, "", http.StatusOK, "", ""},
  3447  		{"token_value query", "/jwt/token_value_query?token=" + hmacToken, http.Header{}, "", http.StatusOK, `["foo","bar"]`, ""},
  3448  		{"token_value body", "/jwt/token_value_body", http.Header{"Content-Type": {"application/json"}}, `{"token":"` + hmacToken + `"}`, http.StatusOK, `["foo","bar"]`, ""},
  3449  		{"ECDSA JWT", "/jwt/ecdsa", http.Header{"Authorization": []string{"Bearer " + ecdsaToken}}, "", http.StatusOK, "", ""},
  3450  		{"ECDSA local JWT", "/jwt/ecdsa", http.Header{"Authorization": []string{"Bearer " + localToken}}, "", http.StatusOK, "", ""},
  3451  		{"ECDSA JWT PKCS8", "/jwt/ecdsa8", http.Header{"Authorization": []string{"Bearer " + ecdsaToken}}, "", http.StatusOK, "", ""},
  3452  		{"ECDSA JWT bad algorithm", "/jwt/ecdsa/bad", http.Header{"Authorization": []string{"Bearer " + ecdsaToken}}, "", http.StatusUnauthorized, "", "access control error: ECDSATokenWrongAlgorithm: signing method ES256 is invalid"},
  3453  		{"ECDSA JWKS with certificate: kid=es256", "/jwks/ecdsa", http.Header{"Authorization": []string{"Bearer " + ecdsaToken}}, "", http.StatusOK, "", ""},
  3454  		{"ECDSA JWKS with crv/x/y: kid=es256-crv-x-y", "/jwks/ecdsa", http.Header{"Authorization": []string{"Bearer " + ecdsaToken2}}, "", http.StatusOK, "", ""},
  3455  	} {
  3456  		t.Run(tc.name, func(subT *testing.T) {
  3457  			helper := test.New(subT)
  3458  			hook.Reset()
  3459  
  3460  			req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, strings.NewReader(tc.body))
  3461  			helper.Must(err)
  3462  
  3463  			req.Header = tc.header
  3464  
  3465  			res, err := client.Do(req)
  3466  			helper.Must(err)
  3467  
  3468  			message := getFirstAccessLogMessage(hook)
  3469  			if res.StatusCode != tc.status {
  3470  				subT.Errorf("expected Status %d, got: %d (%s)", tc.status, res.StatusCode, message)
  3471  				return
  3472  			}
  3473  
  3474  			if tc.wantErrLog == "" {
  3475  				if message != "" {
  3476  					subT.Errorf("Expected error log: %q, actual: %#v", tc.wantErrLog, message)
  3477  				}
  3478  			} else {
  3479  				if !strings.HasPrefix(message, tc.wantErrLog) {
  3480  					subT.Errorf("Expected error log message: '%s', actual: '%s'", tc.wantErrLog, message)
  3481  				}
  3482  			}
  3483  
  3484  			if res.StatusCode != http.StatusOK {
  3485  				return
  3486  			}
  3487  
  3488  			expSub := "1234567890"
  3489  			if sub := res.Header.Get("X-Jwt-Sub"); sub != expSub {
  3490  				subT.Errorf("expected sub: %q, actual: %q", expSub, sub)
  3491  				return
  3492  			}
  3493  
  3494  			if grantedPermissions := res.Header.Get("X-Granted-Permissions"); grantedPermissions != tc.expPerm {
  3495  				subT.Errorf("expected granted permissions: %q, actual: %q", tc.expPerm, grantedPermissions)
  3496  				return
  3497  			}
  3498  		})
  3499  	}
  3500  }
  3501  
  3502  func TestJWT_DefaultErrorHandler(t *testing.T) {
  3503  	client := newClient()
  3504  
  3505  	shutdown, hook := newCouper("testdata/integration/config/03_couper.hcl", test.New(t))
  3506  	defer shutdown()
  3507  
  3508  	type testCase struct {
  3509  		name        string
  3510  		path        string
  3511  		header      http.Header
  3512  		status      int
  3513  		wantErrType string
  3514  		wantWwwAuth string
  3515  	}
  3516  
  3517  	validToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGUiOiJmb28gYmFyIiwiaWF0IjoxNTE2MjM5MDIyfQ.7wz7Z7IajfEpwYayfshag6tQVS0e0zZJyjAhuFC0L-E"
  3518  	expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjEyMzQ1Njc4OSwic2NvcGUiOlsiZm9vIiwiYmFyIl19.W2ziH_V33JkOA5ttQhzWN96RqxFydmx7GHY6G__U9HM"
  3519  	invalidToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGUiOiJmb28gYmFyIiwiaWF0IjoxNTE2MjM5MDIyfQ.7wz7Z7IajfEpwYayfshag6tQVS0e0"
  3520  
  3521  	for _, tc := range []testCase{
  3522  		{"valid token", "/jwt", http.Header{"Authorization": []string{"Bearer " + validToken}}, http.StatusOK, "", ""},
  3523  		{"no token", "/jwt", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", `Bearer`},
  3524  		{"expired token", "/jwt", http.Header{"Authorization": []string{"Bearer " + expiredToken}}, http.StatusUnauthorized, "jwt_token_expired", `Bearer error="invalid_token", error_description="The access token expired"`},
  3525  		{"invalid token", "/jwt", http.Header{"Authorization": []string{"Bearer " + invalidToken}}, http.StatusUnauthorized, "jwt_token_invalid", `Bearer error="invalid_token"`},
  3526  
  3527  		{"valid token in header", "/jwt/header", http.Header{"X-Token": []string{validToken}}, http.StatusOK, "", ""},
  3528  		{"no token in header", "/jwt/header", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", ""},
  3529  		{"expired token in header", "/jwt/header", http.Header{"X-Token": []string{expiredToken}}, http.StatusUnauthorized, "jwt_token_expired", ""},
  3530  		{"invalid token in header", "/jwt/header", http.Header{"X-Token": []string{invalidToken}}, http.StatusUnauthorized, "jwt_token_invalid", ""},
  3531  
  3532  		{"valid token in authorization header", "/jwt/header/auth", http.Header{"Authorization": []string{"Bearer " + validToken}}, http.StatusOK, "", ""},
  3533  		{"no token in authorization header", "/jwt/header/auth", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", `Bearer`},
  3534  		{"expired token in authorization header", "/jwt/header/auth", http.Header{"Authorization": []string{"Bearer " + expiredToken}}, http.StatusUnauthorized, "jwt_token_expired", `Bearer error="invalid_token", error_description="The access token expired"`},
  3535  		{"invalid token in authorization header", "/jwt/header/auth", http.Header{"Authorization": []string{"Bearer " + invalidToken}}, http.StatusUnauthorized, "jwt_token_invalid", `Bearer error="invalid_token"`},
  3536  
  3537  		{"valid token in cookie", "/jwt/cookie", http.Header{"Cookie": []string{"tok=" + validToken}}, http.StatusOK, "", ""},
  3538  		{"no token in cookie", "/jwt/cookie", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", ""},
  3539  		{"expired token in cookie", "/jwt/cookie", http.Header{"Cookie": []string{"tok=" + expiredToken}}, http.StatusUnauthorized, "jwt_token_expired", ""},
  3540  		{"invalid token in cookie", "/jwt/cookie", http.Header{"Cookie": []string{"tok=" + invalidToken}}, http.StatusUnauthorized, "jwt_token_invalid", ""},
  3541  
  3542  		{"valid token from token_value", "/jwt/tokenValue?tok=" + validToken, http.Header{}, http.StatusOK, "", ""},
  3543  		{"no token from token_value", "/jwt/tokenValue", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", ""},
  3544  		{"expired token from token_value", "/jwt/tokenValue?tok=" + expiredToken, http.Header{}, http.StatusUnauthorized, "jwt_token_expired", ""},
  3545  		{"invalid token from token_value", "/jwt/tokenValue?tok=" + invalidToken, http.Header{}, http.StatusUnauthorized, "jwt_token_invalid", ""},
  3546  	} {
  3547  		t.Run(tc.name, func(subT *testing.T) {
  3548  			helper := test.New(subT)
  3549  			hook.Reset()
  3550  
  3551  			req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil)
  3552  			helper.Must(err)
  3553  
  3554  			req.Header = tc.header
  3555  
  3556  			res, err := client.Do(req)
  3557  			helper.Must(err)
  3558  
  3559  			if res.StatusCode != tc.status {
  3560  				subT.Errorf("expected Status %d, got: %d", tc.status, res.StatusCode)
  3561  				return
  3562  			}
  3563  
  3564  			errorType := getAccessLogErrorType(hook)
  3565  			if errorType != tc.wantErrType {
  3566  				subT.Errorf("Expected error type: %q, actual: %q", tc.wantErrType, errorType)
  3567  			}
  3568  
  3569  			wwwAuth := res.Header.Get("WWW-Authenticate")
  3570  			if wwwAuth != tc.wantWwwAuth {
  3571  				subT.Errorf("Expected www-authenticate: %q, actual: %q", tc.wantWwwAuth, wwwAuth)
  3572  			}
  3573  		})
  3574  	}
  3575  }
  3576  
  3577  func TestJWKsMaxStale(t *testing.T) {
  3578  	helper := test.New(t)
  3579  	client := newClient()
  3580  
  3581  	config := `
  3582  	  server {
  3583  	    endpoint "/" {
  3584  	    access_control = ["stale"]
  3585  	      response {
  3586  	        body = "hi"
  3587  	      }
  3588  	    }
  3589  	  }
  3590  	  definitions {
  3591  	    jwt "stale" {
  3592  	      jwks_url = "${env.COUPER_TEST_BACKEND_ADDR}/jwks.json"
  3593  	      jwks_ttl = "3s"
  3594  	      jwks_max_stale = "2s"
  3595  	      backend {
  3596  	        origin = env.COUPER_TEST_BACKEND_ADDR
  3597  	        set_request_headers = {
  3598  	          Self-Destruct: ` + fmt.Sprint(time.Now().Add(2*time.Second).Unix()) + `
  3599  	        }
  3600  	      }
  3601  	    }
  3602  	  }
  3603  	`
  3604  
  3605  	shutdown, hook, err := newCouperWithBytes([]byte(config), helper)
  3606  	defer shutdown()
  3607  	helper.Must(err)
  3608  
  3609  	req, err := http.NewRequest(http.MethodGet, "http://back.end:8080/", nil)
  3610  	helper.Must(err)
  3611  
  3612  	rsaToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTB9.AZ0gZVqPe9TjjjJO0GnlTvERBXhPyxW_gTn050rCoEkseFRlp4TYry7WTQ7J4HNrH3btfxaEQLtTv7KooVLXQyMDujQbKU6cyuYH6MZXaM0Co3Bhu0awoX-2GVk997-7kMZx2yvwIR5ypd1CERIbNs5QcQaI4sqx_8oGrjO5ZmOWRqSpi4Mb8gJEVVccxurPu65gPFq9esVWwTf4cMQ3GGzijatnGDbRWs_igVGf8IAfmiROSVd17fShQtfthOFd19TGUswVAleOftC7-DDeJgAK8Un5xOHGRjv3ypK_6ZLRonhswaGXxovE0kLq4ZSzumQY2hOFE6x_BbrR1WKtGw"
  3613  
  3614  	req.Header = http.Header{"Authorization": []string{"Bearer " + rsaToken}}
  3615  
  3616  	res, err := client.Do(req)
  3617  	helper.Must(err)
  3618  	if res.StatusCode != http.StatusOK {
  3619  		message := getFirstAccessLogMessage(hook)
  3620  		t.Fatalf("expected status %d, got: %d (%s)", http.StatusOK, res.StatusCode, message)
  3621  	}
  3622  
  3623  	time.Sleep(3 * time.Second)
  3624  	// TTL 3s, backend is already failing, responds with stale JWKS
  3625  
  3626  	res, err = client.Do(req)
  3627  	helper.Must(err)
  3628  	if res.StatusCode != http.StatusOK {
  3629  		message := getFirstAccessLogMessage(hook)
  3630  		t.Fatalf("expected status %d, got: %d (%s)", http.StatusOK, res.StatusCode, message)
  3631  	}
  3632  
  3633  	time.Sleep(3 * time.Second)
  3634  	// stale time (2s) exhausted -> 403
  3635  	res, err = client.Do(req)
  3636  	helper.Must(err)
  3637  
  3638  	time.Sleep(time.Second)
  3639  	message := getFirstAccessLogMessage(hook)
  3640  	if res.StatusCode != http.StatusUnauthorized {
  3641  		t.Fatalf("expected status %d, got: %d (%s)", http.StatusUnauthorized, res.StatusCode, message)
  3642  	}
  3643  
  3644  	expectedMessage := "access control error: stale: received no valid JWKs data: <nil>, status code 500"
  3645  	if message != expectedMessage {
  3646  		t.Fatalf("expected message %q, got: %q", expectedMessage, message)
  3647  	}
  3648  }
  3649  
  3650  func TestJWTAccessControlSourceConfig(t *testing.T) {
  3651  	helper := test.New(t)
  3652  	couperConfig, err := configload.LoadFile("testdata/integration/config/05_couper.hcl", "")
  3653  	helper.Must(err)
  3654  
  3655  	log, _ := logrustest.NewNullLogger()
  3656  	ctx := context.TODO()
  3657  
  3658  	expectedMsg := "configuration error: invalid-source: token source is invalid"
  3659  
  3660  	err = command.NewRun(ctx).Execute(nil, couperConfig, log.WithContext(ctx))
  3661  	logErr, _ := err.(errors.GoError)
  3662  	if logErr == nil {
  3663  		t.Error("logErr should not be nil")
  3664  	} else if logErr.LogError() != expectedMsg {
  3665  		t.Errorf("\nwant:\t%s\ngot:\t%v", expectedMsg, logErr.LogError())
  3666  	}
  3667  }
  3668  
  3669  func TestJWTAccessControl_round(t *testing.T) {
  3670  	pid := "asdf"
  3671  	client := newClient()
  3672  
  3673  	shutdown, hook := newCouper("testdata/integration/config/08_couper.hcl", test.New(t))
  3674  	defer shutdown()
  3675  
  3676  	type testCase struct {
  3677  		name      string
  3678  		path      string
  3679  		expGroups []interface{}
  3680  	}
  3681  
  3682  	for _, tc := range []testCase{
  3683  		{"separate jwt_signing_profile/jwt", "/separate", []interface{}{"g1", "g2"}},
  3684  		{"self-signed jwt", "/self-signed", []interface{}{}},
  3685  	} {
  3686  		t.Run(tc.path, func(subT *testing.T) {
  3687  			helper := test.New(subT)
  3688  			hook.Reset()
  3689  
  3690  			req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://back.end:8080%s/%s/create-jwt", tc.path, pid), nil)
  3691  			helper.Must(err)
  3692  
  3693  			res, err := client.Do(req)
  3694  			helper.Must(err)
  3695  
  3696  			if res.StatusCode != http.StatusOK {
  3697  				subT.Fatalf("%q: token request: unexpected status: %d", tc.name, res.StatusCode)
  3698  			}
  3699  
  3700  			token := res.Header.Get("X-Jwt")
  3701  
  3702  			req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://back.end:8080%s/%s/jwt", tc.path, pid), nil)
  3703  			helper.Must(err)
  3704  			req.Header.Set("Authorization", "Bearer "+token)
  3705  
  3706  			res, err = client.Do(req)
  3707  			helper.Must(err)
  3708  
  3709  			if res.StatusCode != http.StatusOK {
  3710  				subT.Fatalf("%q: resource request: unexpected status: %d", tc.name, res.StatusCode)
  3711  			}
  3712  
  3713  			decoder := json.NewDecoder(res.Body)
  3714  			var claims map[string]interface{}
  3715  			err = decoder.Decode(&claims)
  3716  			helper.Must(err)
  3717  
  3718  			if _, ok := claims["exp"]; !ok {
  3719  				subT.Fatalf("%q: missing exp claim: %#v", tc.name, claims)
  3720  			}
  3721  			issclaim, ok := claims["iss"]
  3722  			if !ok {
  3723  				subT.Fatalf("%q: missing iss claim: %#v", tc.name, claims)
  3724  			}
  3725  			if issclaim != "the_issuer" {
  3726  				subT.Fatalf("%q: unexpected iss claim: %q", tc.name, issclaim)
  3727  			}
  3728  			pidclaim, ok := claims["pid"]
  3729  			if !ok {
  3730  				subT.Fatalf("%q: missing pid claim: %#v", tc.name, claims)
  3731  			}
  3732  			if pidclaim != pid {
  3733  				subT.Fatalf("%q: unexpected pid claim: %q", tc.name, pidclaim)
  3734  			}
  3735  			groupsclaim, ok := claims["groups"]
  3736  			if !ok {
  3737  				subT.Fatalf("%q: missing groups claim: %#v", tc.name, claims)
  3738  			}
  3739  			groupsclaimArray, ok := groupsclaim.([]interface{})
  3740  			if !ok {
  3741  				subT.Fatalf("%q: groups must be array: %#v", tc.name, groupsclaim)
  3742  			}
  3743  			if !cmp.Equal(tc.expGroups, groupsclaimArray) {
  3744  				subT.Errorf(cmp.Diff(tc.expGroups, groupsclaimArray))
  3745  			}
  3746  		})
  3747  	}
  3748  }
  3749  
  3750  func TestJWT_CacheControl_private(t *testing.T) {
  3751  	token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.qSLnmYgnkcOjxlOjFhUHQpCfTQ5elzKY3Mq6gRVT4iI"
  3752  	client := newClient()
  3753  
  3754  	shutdown, hook := newCouper("testdata/integration/config/10_couper.hcl", test.New(t))
  3755  	defer shutdown()
  3756  
  3757  	var noCC []string
  3758  
  3759  	type testCase struct {
  3760  		name      string
  3761  		path      string
  3762  		setToken  bool
  3763  		expStatus int
  3764  		expCC     []string
  3765  	}
  3766  
  3767  	for _, tc := range []testCase{
  3768  		{"no token; no cc from ep", "/cc-private/no-cc", false, 401, []string{"private"}},
  3769  		{"no token; cc public from ep", "/cc-private/cc-public", false, 401, []string{"private"}},
  3770  		{"no token; no cc from ep; disable", "/no-cc-private/no-cc", false, 401, noCC},
  3771  		{"no token; cc public from ep; disable", "/no-cc-private/cc-public", false, 401, noCC},
  3772  		{"token; no cc from ep", "/cc-private/no-cc", true, 204, []string{"private"}},
  3773  		{"token; cc public from ep", "/cc-private/cc-public", true, 204, []string{"private", "public"}},
  3774  		{"token; no public cc from ep; disable", "/no-cc-private/no-cc", true, 204, noCC},
  3775  		{"token; cc public from ep; disable", "/no-cc-private/cc-public", true, 204, []string{"public"}},
  3776  	} {
  3777  		t.Run(tc.name, func(subT *testing.T) {
  3778  			helper := test.New(subT)
  3779  			hook.Reset()
  3780  
  3781  			req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil)
  3782  			helper.Must(err)
  3783  			if tc.setToken {
  3784  				req.Header.Set("Authorization", "Bearer "+token)
  3785  			}
  3786  
  3787  			res, err := client.Do(req)
  3788  			helper.Must(err)
  3789  
  3790  			if res.StatusCode != tc.expStatus {
  3791  				subT.Errorf("expected Status %d, got: %d", tc.expStatus, res.StatusCode)
  3792  				return
  3793  			}
  3794  
  3795  			cc := res.Header.Values("Cache-Control")
  3796  			sort.Strings(cc)
  3797  
  3798  			if !cmp.Equal(tc.expCC, cc) {
  3799  				subT.Errorf("%s", cmp.Diff(tc.expCC, cc))
  3800  			}
  3801  		})
  3802  	}
  3803  }
  3804  
  3805  func getFirstAccessLogMessage(hook *logrustest.Hook) string {
  3806  	for _, entry := range hook.AllEntries() {
  3807  		if entry.Data["type"] == "couper_access" && entry.Message != "" {
  3808  			return entry.Message
  3809  		}
  3810  	}
  3811  
  3812  	return ""
  3813  }
  3814  
  3815  func Test_Permissions(t *testing.T) {
  3816  	h := test.New(t)
  3817  	client := newClient()
  3818  
  3819  	shutdown, hook := newCouper("testdata/integration/config/09_couper.hcl", test.New(t))
  3820  	defer shutdown()
  3821  
  3822  	tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
  3823  		"scp": "a",
  3824  		"rl":  "r1",
  3825  	})
  3826  	token, tokenErr := tok.SignedString([]byte("asdf"))
  3827  	h.Must(tokenErr)
  3828  
  3829  	type testCase struct {
  3830  		name         string
  3831  		method       string
  3832  		path         string
  3833  		authorize    bool
  3834  		status       int
  3835  		wantGranted  string
  3836  		wantRequired string
  3837  		wantErrLog   string
  3838  		wantErrType  string
  3839  	}
  3840  
  3841  	for _, tc := range []testCase{
  3842  		{"by scope: unauthorized", http.MethodGet, "/scope/foo", false, http.StatusUnauthorized, ``, ``, "access control error: scoped_jwt: token required", "jwt_token_missing"},
  3843  		{"by scope: no permission required by endpoint", http.MethodGet, "/scope/foo", true, http.StatusNoContent, `["a"]`, ``, "", ""},
  3844  		{"by scope: permission required by endpoint: insufficient permissions", http.MethodPost, "/scope/foo", true, http.StatusForbidden, ``, ``, `access control error: required permission "foo" not granted`, "insufficient_permissions"},
  3845  		{"by scope: method not permitted", http.MethodDelete, "/scope/foo", true, http.StatusMethodNotAllowed, ``, ``, "method not allowed error: method DELETE not allowed by required_permission", ""},
  3846  		{"by scope: permission required by endpoint via *: insufficient permissions", http.MethodGet, "/scope/bar", true, http.StatusForbidden, ``, `more`, `access control error: required permission "more" not granted`, "insufficient_permissions"},
  3847  		{"by scope: no permission required by endpoint", http.MethodDelete, "/scope/bar", true, http.StatusNoContent, `["a"]`, ``, "", ""},
  3848  		{"by scope: required permission expression", http.MethodGet, "/scope/path/a/path", true, http.StatusNoContent, `["a"]`, ``, "", ""},
  3849  		{"by scope: required permission object expression (GET)", http.MethodGet, "/scope/object/get", true, http.StatusNoContent, `["a"]`, ``, "", ""},
  3850  		{"by scope: required permission object expression (DELETE)", http.MethodDelete, "/scope/object/delete", true, http.StatusForbidden, ``, ``, `access control error: required permission "z" not granted`, "insufficient_permissions"},
  3851  		{"by scope: required permission bad expression", http.MethodGet, "/scope/bad/expression", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
  3852  		{"by scope: required permission bad type number", http.MethodGet, "/scope/bad/type/number", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
  3853  		{"by scope: required permission bad type boolean", http.MethodGet, "/scope/bad/type/boolean", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
  3854  		{"by scope: required permission bad type tuple", http.MethodGet, "/scope/bad/type/tuple", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
  3855  		{"by scope: required permission bad type null", http.MethodGet, "/scope/bad/type/null", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
  3856  		{"by scope: required permission by api only: insufficient permissions", http.MethodGet, "/scope/permission-from-api", true, http.StatusForbidden, ``, ``, `access control error: required permission "z" not granted`, "insufficient_permissions"},
  3857  		{"by role: unauthorized", http.MethodGet, "/role/foo", false, http.StatusUnauthorized, ``, ``, "access control error: roled_jwt: token required", "jwt_token_missing"},
  3858  		{"by role: sufficient permission", http.MethodGet, "/role/foo", true, http.StatusNoContent, `["a","b"]`, ``, "", ""},
  3859  		{"by role: permission required by endpoint: insufficient permissions", http.MethodPost, "/role/foo", true, http.StatusForbidden, ``, ``, `access control error: required permission "foo" not granted`, "insufficient_permissions"},
  3860  		{"by role: method not permitted", http.MethodDelete, "/role/foo", true, http.StatusMethodNotAllowed, ``, ``, "method not allowed error: method DELETE not allowed by required_permission", ""},
  3861  		{"by role: permission required by endpoint via *: insufficient permissions", http.MethodGet, "/role/bar", true, http.StatusForbidden, ``, `more`, `access control error: required permission "more" not granted`, "insufficient_permissions"},
  3862  		{"by role: no permission required by endpoint", http.MethodDelete, "/role/bar", true, http.StatusNoContent, `["a","b"]`, ``, "", ""},
  3863  		{"by scope/role, mapped from scope", http.MethodGet, "/scope_and_role/foo", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""},
  3864  		{"by scope/role, mapped scope mapped from role", http.MethodGet, "/scope_and_role/bar", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""},
  3865  		{"by scope/role, mapped from scope, map files", http.MethodGet, "/scope_and_role_files/foo", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""},
  3866  		{"by scope/role, mapped scope mapped from role, map files", http.MethodGet, "/scope_and_role_files/bar", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""},
  3867  	} {
  3868  		t.Run(fmt.Sprintf("%s_%s_%s", tc.name, tc.method, tc.path), func(subT *testing.T) {
  3869  			helper := test.New(subT)
  3870  			hook.Reset()
  3871  
  3872  			req, err := http.NewRequest(tc.method, "http://back.end:8080"+tc.path, nil)
  3873  			helper.Must(err)
  3874  
  3875  			if tc.authorize {
  3876  				req.Header.Set("Authorization", "Bearer "+token)
  3877  			}
  3878  
  3879  			res, err := client.Do(req)
  3880  			helper.Must(err)
  3881  
  3882  			if res.StatusCode != tc.status {
  3883  				subT.Fatalf("expected Status %d, got: %d", tc.status, res.StatusCode)
  3884  			}
  3885  
  3886  			granted := res.Header.Get("x-granted-permissions")
  3887  			if granted != tc.wantGranted {
  3888  				subT.Errorf("Expected granted permissions:\nWant:\t%q\nGot:\t%q", tc.wantGranted, granted)
  3889  			}
  3890  
  3891  			required := res.Header.Get("x-required-permission")
  3892  			if required != tc.wantRequired {
  3893  				subT.Errorf("Expected required permission:\nWant:\t%q\nGot:\t%q", tc.wantRequired, required)
  3894  			}
  3895  
  3896  			message := getFirstAccessLogMessage(hook)
  3897  			if !strings.HasPrefix(message, tc.wantErrLog) {
  3898  				subT.Errorf("Expected error log:\nWant:\t%q\nGot:\t%q", tc.wantErrLog, message)
  3899  			}
  3900  
  3901  			errorType := getAccessLogErrorType(hook)
  3902  			if errorType != tc.wantErrType {
  3903  				subT.Errorf("Expected error type: %q, actual: %q", tc.wantErrType, errorType)
  3904  			}
  3905  		})
  3906  	}
  3907  }
  3908  
  3909  func getAccessLogURL(hook *logrustest.Hook) string {
  3910  	for _, entry := range hook.AllEntries() {
  3911  		if entry.Data["type"] == "couper_access" && entry.Data["url"] != "" {
  3912  			if u, ok := entry.Data["url"].(string); ok {
  3913  				return u
  3914  			}
  3915  		}
  3916  	}
  3917  
  3918  	return ""
  3919  }
  3920  
  3921  func getAccessLogErrorType(hook *logrustest.Hook) string {
  3922  	for _, entry := range hook.AllEntries() {
  3923  		if entry.Data["type"] == "couper_access" && entry.Data["error_type"] != "" {
  3924  			if errorType, ok := entry.Data["error_type"].(string); ok {
  3925  				return errorType
  3926  			}
  3927  		}
  3928  	}
  3929  
  3930  	return ""
  3931  }
  3932  
  3933  func TestWrapperHiJack_WebsocketUpgrade(t *testing.T) {
  3934  	helper := test.New(t)
  3935  	shutdown, _ := newCouper("testdata/integration/api/04_couper.hcl", test.New(t))
  3936  	defer shutdown()
  3937  
  3938  	req, err := http.NewRequest(http.MethodGet, "http://connect.ws:8080/upgrade", nil)
  3939  	helper.Must(err)
  3940  	req.Close = false
  3941  
  3942  	req.Header.Set("Connection", "upgrade")
  3943  	req.Header.Set("Upgrade", "websocket")
  3944  
  3945  	conn, err := net.Dial("tcp", "127.0.0.1:8080")
  3946  	helper.Must(err)
  3947  	defer conn.Close()
  3948  
  3949  	helper.Must(req.Write(conn))
  3950  
  3951  	helper.Must(conn.SetDeadline(time.Time{}))
  3952  
  3953  	textConn := textproto.NewConn(conn)
  3954  	_, _, _ = textConn.ReadResponse(http.StatusSwitchingProtocols) // ignore short resp error
  3955  	header, err := textConn.ReadMIMEHeader()
  3956  	helper.Must(err)
  3957  
  3958  	expectedHeader := textproto.MIMEHeader{
  3959  		"Abc":               []string{"123"},
  3960  		"Connection":        []string{"Upgrade"},
  3961  		"Couper-Request-Id": header.Values("Couper-Request-Id"), // dynamic
  3962  		"Server":            []string{"couper.io"},
  3963  		"Upgrade":           []string{"websocket"},
  3964  	}
  3965  
  3966  	if !reflect.DeepEqual(expectedHeader, header) {
  3967  		t.Errorf("Want: %v, got: %v", expectedHeader, header)
  3968  	}
  3969  
  3970  	n, err := conn.Write([]byte("ping"))
  3971  	helper.Must(err)
  3972  
  3973  	if n != 4 {
  3974  		t.Errorf("Expected 4 written bytes for 'ping', got: %d", n)
  3975  	}
  3976  
  3977  	p := make([]byte, 4)
  3978  	_, err = conn.Read(p)
  3979  	helper.Must(err)
  3980  
  3981  	if !bytes.Equal(p, []byte("pong")) {
  3982  		t.Errorf("Expected pong answer, got: %q", string(p))
  3983  	}
  3984  }
  3985  
  3986  func TestWrapperHiJack_WebsocketUpgradeModifier(t *testing.T) {
  3987  	helper := test.New(t)
  3988  	shutdown, _ := newCouper("testdata/integration/api/13_couper.hcl", test.New(t))
  3989  	defer shutdown()
  3990  
  3991  	req, err := http.NewRequest(http.MethodGet, "http://connect.ws:8080/upgrade/ws", bytes.NewBufferString("ws-client-body"))
  3992  	helper.Must(err)
  3993  	req.Close = false
  3994  
  3995  	req.Header.Set("Connection", "upgrade")
  3996  	req.Header.Set("Upgrade", "websocket")
  3997  
  3998  	conn, err := net.Dial("tcp", "127.0.0.1:8080")
  3999  	helper.Must(err)
  4000  	defer conn.Close()
  4001  
  4002  	helper.Must(req.Write(conn))
  4003  
  4004  	helper.Must(conn.SetDeadline(time.Time{}))
  4005  
  4006  	textConn := textproto.NewConn(conn)
  4007  	_, _, _ = textConn.ReadResponse(http.StatusSwitchingProtocols) // ignore short resp error
  4008  	header, err := textConn.ReadMIMEHeader()
  4009  	helper.Must(err)
  4010  
  4011  	expectedHeader := textproto.MIMEHeader{
  4012  		"Abc":               {"123"},
  4013  		"Connection":        {"Upgrade"},
  4014  		"Couper-Request-Id": header.Values("Couper-Request-Id"), // dynamic
  4015  		"Echo":              {"ECHO"},
  4016  		"Server":            {"couper.io"},
  4017  		"Upgrade":           {"websocket"},
  4018  		"X-Body":            {"ws-client-body"},
  4019  		"X-Upgrade-Body":    {"ws-client-body"},
  4020  	}
  4021  
  4022  	if !reflect.DeepEqual(expectedHeader, header) {
  4023  		t.Errorf(cmp.Diff(expectedHeader, header))
  4024  	}
  4025  
  4026  	n, err := conn.Write([]byte("ping"))
  4027  	helper.Must(err)
  4028  
  4029  	if n != 4 {
  4030  		t.Errorf("Expected 4 written bytes for 'ping', got: %d", n)
  4031  	}
  4032  
  4033  	p := make([]byte, 4)
  4034  	_, err = conn.Read(p)
  4035  	helper.Must(err)
  4036  
  4037  	if !bytes.Equal(p, []byte("pong")) {
  4038  		t.Errorf("Expected pong answer, got: %q", string(p))
  4039  	}
  4040  }
  4041  
  4042  func TestWrapperHiJack_WebsocketUpgradeBodyBuffer(t *testing.T) {
  4043  	helper := test.New(t)
  4044  	shutdown, _ := newCouper("testdata/integration/api/13_couper.hcl", test.New(t))
  4045  	defer shutdown()
  4046  
  4047  	req, err := http.NewRequest(http.MethodGet, "http://connect.ws:8080/upgrade/small", bytes.NewBufferString("client-body"))
  4048  	helper.Must(err)
  4049  	req.Close = false
  4050  
  4051  	conn, err := net.Dial("tcp", "127.0.0.1:8080")
  4052  	helper.Must(err)
  4053  	defer conn.Close()
  4054  
  4055  	helper.Must(req.Write(conn))
  4056  
  4057  	helper.Must(conn.SetDeadline(time.Time{}))
  4058  
  4059  	res, err := http.ReadResponse(bufio.NewReader(conn), req)
  4060  	helper.Must(err)
  4061  
  4062  	if res.StatusCode != http.StatusOK {
  4063  		t.Errorf("Expected StatusOK, got: %s", res.Status)
  4064  	}
  4065  
  4066  	expectedHeader := http.Header{
  4067  		"Content-Length":    res.Header.Values("Content-Length"), // dynamic, could change for other tests, unrelated here
  4068  		"Content-Type":      {"text/plain; charset=utf-8"},
  4069  		"Couper-Request-Id": res.Header.Values("Couper-Request-Id"), // dynamic
  4070  		"Date":              res.Header.Values("Date"),              // dynamic
  4071  		"Server":            {"couper.io"},
  4072  		"X-Body":            {"client-body"},
  4073  		"X-Resp-Body":       {"1234567890"},
  4074  	}
  4075  
  4076  	if !reflect.DeepEqual(expectedHeader, res.Header) {
  4077  		t.Errorf(cmp.Diff(expectedHeader, res.Header))
  4078  	}
  4079  }
  4080  
  4081  func TestWrapperHiJack_WebsocketUpgradeTimeout(t *testing.T) {
  4082  	helper := test.New(t)
  4083  	shutdown, _ := newCouper("testdata/integration/api/14_couper.hcl", test.New(t))
  4084  	defer shutdown()
  4085  
  4086  	req, err := http.NewRequest(http.MethodGet, "http://connect.ws:8080/upgrade", nil)
  4087  	helper.Must(err)
  4088  	req.Close = false
  4089  
  4090  	req.Header.Set("Connection", "upgrade")
  4091  	req.Header.Set("Upgrade", "websocket")
  4092  
  4093  	conn, err := net.Dial("tcp", "127.0.0.1:8080")
  4094  	helper.Must(err)
  4095  	defer conn.Close()
  4096  
  4097  	helper.Must(req.Write(conn))
  4098  
  4099  	helper.Must(conn.SetDeadline(time.Time{}))
  4100  
  4101  	p := make([]byte, 77)
  4102  	_, err = conn.Read(p)
  4103  	helper.Must(err)
  4104  
  4105  	if !bytes.HasPrefix(p, []byte("HTTP/1.1 504 Gateway Timeout\r\n")) {
  4106  		t.Errorf("Expected 504 status and related headers, got:\n%q", string(p))
  4107  	}
  4108  }
  4109  
  4110  func TestAccessControl_Files_SPA(t *testing.T) {
  4111  	shutdown, _ := newCouper("testdata/file_serving/conf_ac.hcl", test.New(t))
  4112  	defer shutdown()
  4113  
  4114  	client := newClient()
  4115  
  4116  	type testCase struct {
  4117  		path      string
  4118  		password  string
  4119  		expStatus int
  4120  	}
  4121  
  4122  	for _, tc := range []testCase{
  4123  		{"/favicon.ico", "", http.StatusUnauthorized},
  4124  		{"/robots.txt", "", http.StatusUnauthorized},
  4125  		{"/app", "", http.StatusUnauthorized},
  4126  		{"/app/1", "", http.StatusUnauthorized},
  4127  		{"/favicon.ico", "hans", http.StatusNotFound},
  4128  		{"/robots.txt", "hans", http.StatusOK},
  4129  		{"/app", "hans", http.StatusOK},
  4130  		{"/app/1", "hans", http.StatusOK},
  4131  	} {
  4132  		t.Run(tc.path[1:], func(subT *testing.T) {
  4133  			helper := test.New(subT)
  4134  
  4135  			req, err := http.NewRequest(http.MethodGet, "http://protect.me:8080"+tc.path, nil)
  4136  			helper.Must(err)
  4137  
  4138  			if tc.password != "" {
  4139  				req.SetBasicAuth("", tc.password)
  4140  			}
  4141  
  4142  			res, err := client.Do(req)
  4143  			helper.Must(err)
  4144  
  4145  			if res.StatusCode != tc.expStatus {
  4146  				subT.Errorf("Expected status: %d, got: %d", tc.expStatus, res.StatusCode)
  4147  			}
  4148  		})
  4149  	}
  4150  }
  4151  
  4152  func TestHTTPServer_MultiAPI(t *testing.T) {
  4153  	client := newClient()
  4154  
  4155  	type expectation struct {
  4156  		Path string
  4157  	}
  4158  
  4159  	type testCase struct {
  4160  		path string
  4161  		exp  expectation
  4162  	}
  4163  
  4164  	shutdown, _ := newCouper("testdata/integration/api/05_couper.hcl", test.New(t))
  4165  	defer shutdown()
  4166  
  4167  	for _, tc := range []testCase{
  4168  		{"/v1/xxx", expectation{
  4169  			Path: "/v1/xxx",
  4170  		}},
  4171  		{"/v2/yyy", expectation{
  4172  			Path: "/v2/yyy",
  4173  		}},
  4174  		{"/v3/zzz", expectation{
  4175  			Path: "/v3/zzz",
  4176  		}},
  4177  	} {
  4178  		t.Run(tc.path, func(subT *testing.T) {
  4179  			helper := test.New(subT)
  4180  
  4181  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
  4182  			helper.Must(err)
  4183  
  4184  			res, err := client.Do(req)
  4185  			helper.Must(err)
  4186  
  4187  			resBytes, err := io.ReadAll(res.Body)
  4188  			helper.Must(err)
  4189  
  4190  			_ = res.Body.Close()
  4191  
  4192  			var jsonResult expectation
  4193  			err = json.Unmarshal(resBytes, &jsonResult)
  4194  			if err != nil {
  4195  				subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
  4196  			}
  4197  
  4198  			if !reflect.DeepEqual(jsonResult, tc.exp) {
  4199  				subT.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes))
  4200  			}
  4201  		})
  4202  	}
  4203  }
  4204  
  4205  func TestFunctions(t *testing.T) {
  4206  	client := newClient()
  4207  
  4208  	shutdown, _ := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t))
  4209  	defer shutdown()
  4210  
  4211  	type testCase struct {
  4212  		name   string
  4213  		path   string
  4214  		header map[string]string
  4215  		status int
  4216  	}
  4217  
  4218  	for _, tc := range []testCase{
  4219  		{"merge", "/v1/merge", map[string]string{"X-Merged-1": "{\"foo\":[1,2]}", "X-Merged-2": "{\"bar\":[3,4]}", "X-Merged-3": "[\"a\",\"b\"]"}, http.StatusOK},
  4220  		{"coalesce", "/v1/coalesce?q=a", map[string]string{"X-Coalesce-1": "/v1/coalesce", "X-Coalesce-2": "default", "X-Coalesce-3": "default", "X-Coalesce-4": "default"}, http.StatusOK},
  4221  		{"default", "/v1/default?q=a", map[string]string{
  4222  			"X-Default-1":  "/v1/default",
  4223  			"X-Default-2":  "default",
  4224  			"X-Default-3":  "default",
  4225  			"X-Default-4":  "default",
  4226  			"X-Default-5":  "prefix-default",
  4227  			"X-Default-6":  "default",
  4228  			"X-Default-7":  "default",
  4229  			"X-Default-8":  "default-8",
  4230  			"X-Default-9":  "",
  4231  			"X-Default-10": "",
  4232  			"X-Default-11": "0",
  4233  			"X-Default-12": "",
  4234  			"X-Default-13": `{"a":1}`,
  4235  			"X-Default-14": `{"a":1}`,
  4236  			"X-Default-15": `[1,2]`,
  4237  		}, http.StatusOK},
  4238  		{"contains", "/v1/contains", map[string]string{
  4239  			"X-Contains-1":  "yes",
  4240  			"X-Contains-2":  "no",
  4241  			"X-Contains-3":  "yes",
  4242  			"X-Contains-4":  "no",
  4243  			"X-Contains-5":  "yes",
  4244  			"X-Contains-6":  "no",
  4245  			"X-Contains-7":  "yes",
  4246  			"X-Contains-8":  "no",
  4247  			"X-Contains-9":  "yes",
  4248  			"X-Contains-10": "no",
  4249  			"X-Contains-11": "yes",
  4250  		}, http.StatusOK},
  4251  		{"length", "/v1/length", map[string]string{
  4252  			"X-Length-1": "2",
  4253  			"X-Length-2": "0",
  4254  			"X-Length-3": "5",
  4255  			"X-Length-4": "2",
  4256  		}, http.StatusOK},
  4257  		{"join", "/v1/join", map[string]string{
  4258  			"X-Join-1": "0-1-a-b-3-c-1.234-true-false",
  4259  			"X-Join-2": "||",
  4260  			"X-Join-3": "0-1-2-3-4",
  4261  		}, http.StatusOK},
  4262  		{"keys", "/v1/keys", map[string]string{
  4263  			"X-Keys-1": `["a","b","c"]`,
  4264  			"X-Keys-2": `[]`,
  4265  			"X-Keys-3": `["couper-request-id","user-agent"]`,
  4266  		}, http.StatusOK},
  4267  		{"set_intersection", "/v1/set_intersection", map[string]string{
  4268  			"X-Set_Intersection-1":  `[1,3]`,
  4269  			"X-Set_Intersection-2":  `[1,3]`,
  4270  			"X-Set_Intersection-3":  `[1,3]`,
  4271  			"X-Set_Intersection-4":  `[1,3]`,
  4272  			"X-Set_Intersection-5":  `[3]`,
  4273  			"X-Set_Intersection-6":  `[3]`,
  4274  			"X-Set_Intersection-7":  `[]`,
  4275  			"X-Set_Intersection-8":  `[]`,
  4276  			"X-Set_Intersection-9":  `[]`,
  4277  			"X-Set_Intersection-10": `[]`,
  4278  			"X-Set_Intersection-11": `[2.2]`,
  4279  			"X-Set_Intersection-12": `["b","d"]`,
  4280  			"X-Set_Intersection-13": `[true]`,
  4281  			"X-Set_Intersection-14": `[{"a":1}]`,
  4282  			"X-Set_Intersection-15": `[[1,2]]`,
  4283  		}, http.StatusOK},
  4284  		{"lookup", "/v1/lookup", map[string]string{
  4285  			"X-Lookup-1": "1",
  4286  			"X-Lookup-2": "default",
  4287  			"X-Lookup-3": "Go-http-client/1.1",
  4288  			"X-Lookup-4": "default",
  4289  		}, http.StatusOK},
  4290  		{"trim", "/v1/trim", map[string]string{
  4291  			"X-Trim": "foo \tbar",
  4292  		}, http.StatusOK},
  4293  	} {
  4294  		t.Run(tc.path[1:], func(subT *testing.T) {
  4295  			helper := test.New(subT)
  4296  
  4297  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
  4298  			helper.Must(err)
  4299  
  4300  			res, err := client.Do(req)
  4301  			helper.Must(err)
  4302  
  4303  			if res.StatusCode != tc.status {
  4304  				subT.Fatalf("%q: expected Status %d, got: %d", tc.name, tc.status, res.StatusCode)
  4305  			}
  4306  
  4307  			for k, v := range tc.header {
  4308  				if v1 := res.Header.Get(k); v1 != v {
  4309  					subT.Fatalf("%q: unexpected header value for %q: got: %q, want: %q", tc.name, k, v1, v)
  4310  				}
  4311  			}
  4312  		})
  4313  	}
  4314  }
  4315  
  4316  func TestFunction_to_number(t *testing.T) {
  4317  	client := newClient()
  4318  
  4319  	shutdown, _ := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t))
  4320  	defer shutdown()
  4321  
  4322  	helper := test.New(t)
  4323  
  4324  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/v1/to_number", nil)
  4325  	helper.Must(err)
  4326  
  4327  	res, err := client.Do(req)
  4328  	helper.Must(err)
  4329  
  4330  	if res.StatusCode != http.StatusOK {
  4331  		t.Fatalf("expected Status %d, got: %d", http.StatusOK, res.StatusCode)
  4332  	}
  4333  
  4334  	resBytes, err := io.ReadAll(res.Body)
  4335  	helper.Must(err)
  4336  	helper.Must(res.Body.Close())
  4337  
  4338  	exp := `{"float-2_34":2.34,"float-_3":0.3,"from-env":3.14159,"int":34,"int-3_":3,"int-3_0":3,"null":null}`
  4339  	if string(resBytes) != exp {
  4340  		t.Fatalf("Unexpected result\nwant: %s\n got:  %s", exp, string(resBytes))
  4341  	}
  4342  }
  4343  
  4344  func TestFunction_to_number_errors(t *testing.T) {
  4345  	client := newClient()
  4346  
  4347  	shutdown, logHook := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t))
  4348  	defer shutdown()
  4349  
  4350  	wd, werr := os.Getwd()
  4351  	if werr != nil {
  4352  		t.Fatal(werr)
  4353  	}
  4354  	wd = wd + "/testdata/integration/functions"
  4355  
  4356  	type testCase struct {
  4357  		name   string
  4358  		path   string
  4359  		expMsg string
  4360  	}
  4361  
  4362  	for _, tc := range []testCase{
  4363  		{"string", "/v1/to_number/string", wd + `/01_couper.hcl:65,23-28: Invalid function argument; Invalid value for "v" parameter: cannot convert "two" to number; given string must be a decimal representation of a number.`},
  4364  		{"bool", "/v1/to_number/bool", wd + `/01_couper.hcl:73,23-27: Invalid function argument; Invalid value for "v" parameter: cannot convert bool to number.`},
  4365  		{"tuple", "/v1/to_number/tuple", wd + `/01_couper.hcl:81,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert tuple to number.`},
  4366  		{"object", "/v1/to_number/object", wd + `/01_couper.hcl:89,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert object to number.`},
  4367  	} {
  4368  		t.Run(tc.path[1:], func(subT *testing.T) {
  4369  			helper := test.New(subT)
  4370  
  4371  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
  4372  			helper.Must(err)
  4373  
  4374  			res, err := client.Do(req)
  4375  			helper.Must(err)
  4376  
  4377  			if res.StatusCode != http.StatusInternalServerError {
  4378  				subT.Fatalf("%q: expected Status %d, got: %d", tc.name, http.StatusInternalServerError, res.StatusCode)
  4379  			}
  4380  			msg := logHook.LastEntry().Message
  4381  			if msg != tc.expMsg {
  4382  				subT.Fatalf("%q: expected log message\nwant: %q\ngot:  %q", tc.name, tc.expMsg, msg)
  4383  			}
  4384  		})
  4385  	}
  4386  }
  4387  
  4388  func TestFunction_length_errors(t *testing.T) {
  4389  	client := newClient()
  4390  
  4391  	shutdown, logHook := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t))
  4392  	defer shutdown()
  4393  
  4394  	wd, werr := os.Getwd()
  4395  	if werr != nil {
  4396  		t.Fatal(werr)
  4397  	}
  4398  	wd = wd + "/testdata/integration/functions"
  4399  
  4400  	type testCase struct {
  4401  		name   string
  4402  		path   string
  4403  		expMsg string
  4404  	}
  4405  
  4406  	for _, tc := range []testCase{
  4407  		{"object", "/v1/length/object", wd + `/01_couper.hcl:126,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
  4408  		{"string", "/v1/length/string", wd + `/01_couper.hcl:134,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`},
  4409  		{"null", "/v1/length/null", wd + `/01_couper.hcl:142,26-30: Invalid function argument; Invalid value for "collection" parameter: argument must not be null.`},
  4410  	} {
  4411  		t.Run(tc.path[1:], func(subT *testing.T) {
  4412  			helper := test.New(subT)
  4413  
  4414  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
  4415  			helper.Must(err)
  4416  
  4417  			res, err := client.Do(req)
  4418  			helper.Must(err)
  4419  
  4420  			if res.StatusCode != http.StatusInternalServerError {
  4421  				subT.Fatalf("%q: expected Status %d, got: %d", tc.name, http.StatusInternalServerError, res.StatusCode)
  4422  			}
  4423  			msg := logHook.LastEntry().Message
  4424  			if msg != tc.expMsg {
  4425  				subT.Fatalf("%q: expected log message\nwant: %q\ngot:  %q", tc.name, tc.expMsg, msg)
  4426  			}
  4427  		})
  4428  	}
  4429  }
  4430  
  4431  func TestFunction_lookup_errors(t *testing.T) {
  4432  	client := newClient()
  4433  
  4434  	shutdown, logHook := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t))
  4435  	defer shutdown()
  4436  
  4437  	wd, werr := os.Getwd()
  4438  	if werr != nil {
  4439  		t.Fatal(werr)
  4440  	}
  4441  	wd = wd + "/testdata/integration/functions"
  4442  
  4443  	type testCase struct {
  4444  		name   string
  4445  		path   string
  4446  		expMsg string
  4447  	}
  4448  
  4449  	for _, tc := range []testCase{
  4450  		{"null inputMap", "/v1/lookup/inputMap-null", wd + `/01_couper.hcl:203,26-30: Invalid function argument; Invalid value for "inputMap" parameter: argument must not be null.`},
  4451  	} {
  4452  		t.Run(tc.path[1:], func(subT *testing.T) {
  4453  			helper := test.New(subT)
  4454  
  4455  			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
  4456  			helper.Must(err)
  4457  
  4458  			res, err := client.Do(req)
  4459  			helper.Must(err)
  4460  
  4461  			if res.StatusCode != http.StatusInternalServerError {
  4462  				subT.Fatalf("%q: expected Status %d, got: %d", tc.name, http.StatusInternalServerError, res.StatusCode)
  4463  			}
  4464  			msg := logHook.LastEntry().Message
  4465  			if msg != tc.expMsg {
  4466  				subT.Fatalf("%q: expected log message\nwant: %q\ngot:  %q", tc.name, tc.expMsg, msg)
  4467  			}
  4468  		})
  4469  	}
  4470  }
  4471  
  4472  func TestEndpoint_Response(t *testing.T) {
  4473  	client := newClient()
  4474  	var redirSeen bool
  4475  	client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
  4476  		redirSeen = true
  4477  		return fmt.Errorf("do not follow")
  4478  	}
  4479  
  4480  	shutdown, logHook := newCouper("testdata/integration/endpoint_eval/17_couper.hcl", test.New(t))
  4481  	defer shutdown()
  4482  
  4483  	type testCase struct {
  4484  		path          string
  4485  		expStatusCode int
  4486  	}
  4487  
  4488  	for _, tc := range []testCase{
  4489  		{"/200", http.StatusOK},
  4490  		{"/200/this-is-my-resp-body", http.StatusOK},
  4491  		{"/204", http.StatusNoContent},
  4492  		{"/301", http.StatusMovedPermanently},
  4493  	} {
  4494  		t.Run(tc.path[1:], func(subT *testing.T) {
  4495  			helper := test.New(subT)
  4496  
  4497  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil)
  4498  			helper.Must(err)
  4499  
  4500  			res, err := client.Do(req)
  4501  			if tc.expStatusCode == http.StatusMovedPermanently {
  4502  				if !redirSeen {
  4503  					subT.Errorf("expected a redirect response")
  4504  				}
  4505  
  4506  				resp := logHook.LastEntry().Data["response"]
  4507  				fields := resp.(logging.Fields)
  4508  				headers := fields["headers"].(map[string]string)
  4509  				if headers["location"] != "https://couper.io/" {
  4510  					subT.Errorf("expected location header log")
  4511  				}
  4512  			} else {
  4513  				helper.Must(err)
  4514  			}
  4515  
  4516  			resBytes, err := io.ReadAll(res.Body)
  4517  			helper.Must(err)
  4518  			helper.Must(res.Body.Close())
  4519  
  4520  			if res.StatusCode != tc.expStatusCode {
  4521  				subT.Fatalf("%q: expected Status %d, got: %d", tc.path, tc.expStatusCode, res.StatusCode)
  4522  			}
  4523  
  4524  			if logHook.LastEntry().Data["status"] != tc.expStatusCode {
  4525  				subT.Logf("%v", logHook.LastEntry())
  4526  				subT.Errorf("Expected statusCode log: %d", tc.expStatusCode)
  4527  			}
  4528  
  4529  			if len(resBytes) > 0 {
  4530  				b, exist := logHook.LastEntry().Data["response"].(logging.Fields)["bytes"]
  4531  				if !exist || b != len(resBytes) {
  4532  					subT.Errorf("Want bytes log: %d\ngot:\t%v", len(resBytes), logHook.LastEntry())
  4533  				}
  4534  			}
  4535  		})
  4536  	}
  4537  }
  4538  
  4539  func TestCORS_Configuration(t *testing.T) {
  4540  	client := newClient()
  4541  
  4542  	shutdown, _ := newCouper("testdata/integration/config/06_couper.hcl", test.New(t))
  4543  	defer shutdown()
  4544  
  4545  	requestMethod := "GET"
  4546  	requestHeaders := "Authorization"
  4547  
  4548  	type testCase struct {
  4549  		path              string
  4550  		origin            string
  4551  		expAllowed        bool
  4552  		expAllowedMethods string
  4553  		expAllowedHeaders string
  4554  		expVaryPF         string
  4555  		expVary           string
  4556  		expVaryCred       string
  4557  	}
  4558  
  4559  	for _, tc := range []testCase{
  4560  		{"/06_couper.hcl", "a.com", true, requestMethod, requestHeaders, "Origin,Access-Control-Request-Method,Access-Control-Request-Headers", "Origin,Accept-Encoding", "Origin,Accept-Encoding"},
  4561  		{"/spa/", "b.com", true, requestMethod, requestHeaders, "Origin,Access-Control-Request-Method,Access-Control-Request-Headers", "Origin,Accept-Encoding", "Origin,Accept-Encoding"},
  4562  		{"/api/", "c.com", true, requestMethod, requestHeaders, "Origin,Access-Control-Request-Method,Access-Control-Request-Headers", "Origin,Accept-Encoding", "Origin"},
  4563  		{"/06_couper.hcl", "no.com", false, "", "", "Origin", "Origin,Accept-Encoding", "Origin,Accept-Encoding"},
  4564  		{"/spa/", "", false, "", "", "Origin", "Origin,Accept-Encoding", "Origin,Accept-Encoding"},
  4565  		{"/api/", "no.com", false, "", "", "Origin", "Origin,Accept-Encoding", "Origin"},
  4566  	} {
  4567  		t.Run(tc.path[1:], func(subT *testing.T) {
  4568  			helper := test.New(subT)
  4569  
  4570  			// preflight request
  4571  			req, err := http.NewRequest(http.MethodOptions, "http://localhost:8080"+tc.path, nil)
  4572  			helper.Must(err)
  4573  
  4574  			req.Header.Set("Access-Control-Request-Method", requestMethod)
  4575  			req.Header.Set("Access-Control-Request-Headers", requestHeaders)
  4576  			req.Header.Set("Origin", tc.origin)
  4577  
  4578  			res, err := client.Do(req)
  4579  			helper.Must(err)
  4580  
  4581  			helper.Must(res.Body.Close())
  4582  
  4583  			if res.StatusCode != http.StatusNoContent {
  4584  				subT.Fatalf("%q: expected Status %d, got: %d", tc.path, http.StatusNoContent, res.StatusCode)
  4585  			}
  4586  
  4587  			acao, acaoExists := res.Header["Access-Control-Allow-Origin"]
  4588  			acam, acamExists := res.Header["Access-Control-Allow-Methods"]
  4589  			acah, acahExists := res.Header["Access-Control-Allow-Headers"]
  4590  			acac, acacExists := res.Header["Access-Control-Allow-Credentials"]
  4591  			if tc.expAllowed {
  4592  				if !acaoExists || acao[0] != tc.origin {
  4593  					subT.Errorf("Expected allowed origin, got: %v", acao)
  4594  				}
  4595  				if !acamExists || acam[0] != tc.expAllowedMethods {
  4596  					subT.Errorf("Expected allowed methods, got: %v", acam)
  4597  				}
  4598  				if !acahExists || acah[0] != tc.expAllowedHeaders {
  4599  					subT.Errorf("Expected allowed headers, got: %v", acah)
  4600  				}
  4601  				if !acacExists || acac[0] != "true" {
  4602  					subT.Errorf("Expected allowed credentials, got: %v", acac)
  4603  				}
  4604  			} else {
  4605  				if acaoExists {
  4606  					subT.Errorf("Expected not allowed origin, got: %v", acao)
  4607  				}
  4608  				if acamExists {
  4609  					subT.Errorf("Expected not allowed methods, got: %v", acam)
  4610  				}
  4611  				if acahExists {
  4612  					subT.Errorf("Expected not allowed headers, got: %v", acah)
  4613  				}
  4614  				if acacExists {
  4615  					subT.Errorf("Expected not allowed credentials, got: %v", acac)
  4616  				}
  4617  			}
  4618  			vary, varyExists := res.Header["Vary"]
  4619  			if !varyExists || strings.Join(vary, ",") != tc.expVaryPF {
  4620  				subT.Errorf("Expected vary %q, got: %q", tc.expVaryPF, strings.Join(vary, ","))
  4621  			}
  4622  
  4623  			// actual request lacking credentials -> rejected by basic_auth AC
  4624  			req, err = http.NewRequest(requestMethod, "http://localhost:8080"+tc.path, nil)
  4625  			helper.Must(err)
  4626  
  4627  			req.Header.Set("Origin", tc.origin)
  4628  
  4629  			res, err = client.Do(req)
  4630  			helper.Must(err)
  4631  
  4632  			helper.Must(res.Body.Close())
  4633  
  4634  			if res.StatusCode != http.StatusUnauthorized {
  4635  				subT.Fatalf("%q: expected Status %d, got: %d", tc.path, http.StatusUnauthorized, res.StatusCode)
  4636  			}
  4637  
  4638  			acao, acaoExists = res.Header["Access-Control-Allow-Origin"]
  4639  			acac, acacExists = res.Header["Access-Control-Allow-Credentials"]
  4640  			if tc.expAllowed {
  4641  				if !acaoExists || acao[0] != tc.origin {
  4642  					subT.Errorf("Expected allowed origin, got: %v", acao)
  4643  				}
  4644  				if !acacExists || acac[0] != "true" {
  4645  					subT.Errorf("Expected allowed credentials, got: %v", acac)
  4646  				}
  4647  			} else {
  4648  				if acaoExists {
  4649  					subT.Errorf("Expected not allowed origin, got: %v", acao)
  4650  				}
  4651  				if acacExists {
  4652  					subT.Errorf("Expected not allowed credentials, got: %v", acac)
  4653  				}
  4654  			}
  4655  			vary, varyExists = res.Header["Vary"]
  4656  			if !varyExists || strings.Join(vary, ",") != tc.expVary {
  4657  				subT.Errorf("Expected vary %q, got: %q", tc.expVary, strings.Join(vary, ","))
  4658  			}
  4659  
  4660  			// actual request with credentials
  4661  			req, err = http.NewRequest(requestMethod, "http://localhost:8080"+tc.path, nil)
  4662  			helper.Must(err)
  4663  
  4664  			req.Header.Set("Origin", tc.origin)
  4665  			req.Header.Set("Authorization", "Basic Zm9vOmFzZGY=")
  4666  
  4667  			res, err = client.Do(req)
  4668  			helper.Must(err)
  4669  
  4670  			helper.Must(res.Body.Close())
  4671  
  4672  			if res.StatusCode != http.StatusOK {
  4673  				subT.Fatalf("%q: expected Status %d, got: %d", tc.path, http.StatusOK, res.StatusCode)
  4674  			}
  4675  
  4676  			acao, acaoExists = res.Header["Access-Control-Allow-Origin"]
  4677  			acac, acacExists = res.Header["Access-Control-Allow-Credentials"]
  4678  			if tc.expAllowed {
  4679  				if !acaoExists || acao[0] != tc.origin {
  4680  					subT.Errorf("Expected allowed origin, got: %v", acao)
  4681  				}
  4682  				if !acacExists || acac[0] != "true" {
  4683  					subT.Errorf("Expected allowed credentials, got: %v", acac)
  4684  				}
  4685  			} else {
  4686  				if acaoExists {
  4687  					subT.Errorf("Expected not allowed origin, got: %v", acao)
  4688  				}
  4689  				if acacExists {
  4690  					subT.Errorf("Expected not allowed credentials, got: %v", acac)
  4691  				}
  4692  			}
  4693  			vary, varyExists = res.Header["Vary"]
  4694  			if !varyExists || strings.Join(vary, ",") != tc.expVaryCred {
  4695  				subT.Errorf("Expected vary %q, got: %q", tc.expVaryCred, strings.Join(vary, ","))
  4696  			}
  4697  		})
  4698  	}
  4699  }
  4700  
  4701  func TestLog_Level(t *testing.T) {
  4702  	shutdown, hook := newCouper("testdata/integration/logs/03_couper.hcl", test.New(t))
  4703  	defer shutdown()
  4704  
  4705  	client := newClient()
  4706  
  4707  	helper := test.New(t)
  4708  
  4709  	req, err := http.NewRequest(http.MethodGet, "http://my.upstream:8080/", nil)
  4710  	helper.Must(err)
  4711  
  4712  	hook.Reset()
  4713  
  4714  	res, err := client.Do(req)
  4715  	helper.Must(err)
  4716  
  4717  	if res.StatusCode != http.StatusInternalServerError {
  4718  		t.Errorf("Expected status: %d, got: %d", http.StatusInternalServerError, res.StatusCode)
  4719  	}
  4720  
  4721  	for _, entry := range hook.AllEntries() {
  4722  		if entry.Level != logrus.InfoLevel {
  4723  			t.Errorf("Expected info level, got: %v", entry.Level)
  4724  		}
  4725  	}
  4726  }
  4727  
  4728  func TestOAuthPKCEFunctions(t *testing.T) {
  4729  	client := newClient()
  4730  
  4731  	shutdown, _ := newCouper("testdata/integration/functions/02_couper.hcl", test.New(t))
  4732  	defer shutdown()
  4733  
  4734  	helper := test.New(t)
  4735  
  4736  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/pkce", nil)
  4737  	helper.Must(err)
  4738  
  4739  	res, err := client.Do(req)
  4740  	helper.Must(err)
  4741  
  4742  	if res.StatusCode != 200 {
  4743  		t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode)
  4744  	}
  4745  
  4746  	v1 := res.Header.Get("x-v-1")
  4747  	v2 := res.Header.Get("x-v-2")
  4748  	hv := res.Header.Get("x-hv")
  4749  	if v2 != v1 {
  4750  		t.Errorf("multiple calls to oauth2_verifier() must return the same value:\n\t%s\n\t%s", v1, v2)
  4751  	}
  4752  	s256 := oauth2.Base64urlSha256(v1)
  4753  	if hv != s256 {
  4754  		t.Errorf("call to internal_oauth_hashed_verifier() returns wrong value:\nactual:\t\t%s\nexpected:\t%s", hv, s256)
  4755  	}
  4756  	au, err := url.Parse(res.Header.Get("x-au-pkce"))
  4757  	helper.Must(err)
  4758  	auq := au.Query()
  4759  	if auq.Get("response_type") != "code" {
  4760  		t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code")
  4761  	}
  4762  	if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" {
  4763  		t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback")
  4764  	}
  4765  	if auq.Get("scope") != "openid profile email" {
  4766  		t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile email")
  4767  	}
  4768  	if auq.Get("code_challenge_method") != "S256" {
  4769  		t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "S256")
  4770  	}
  4771  	if auq.Get("code_challenge") != hv {
  4772  		t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), hv)
  4773  	}
  4774  	if auq.Get("state") != "" {
  4775  		t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "")
  4776  	}
  4777  	if auq.Get("nonce") != "" {
  4778  		t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), "")
  4779  	}
  4780  	if auq.Get("client_id") != "foo" {
  4781  		t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo")
  4782  	}
  4783  	au, err = url.Parse(res.Header.Get("x-au-pkce-rel"))
  4784  	helper.Must(err)
  4785  	auq = au.Query()
  4786  	if auq.Get("redirect_uri") != "http://example.com:8080/oidc/callback" {
  4787  		t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://example.com:8080/oidc/callback")
  4788  	}
  4789  
  4790  	req, err = http.NewRequest(http.MethodGet, "http://example.com:8080/pkce", nil)
  4791  	helper.Must(err)
  4792  
  4793  	res, err = client.Do(req)
  4794  	helper.Must(err)
  4795  
  4796  	cv1_n := res.Header.Get("x-v-1")
  4797  	if cv1_n == v1 {
  4798  		t.Errorf("calls to oauth2_verifier() on different requests must not return the same value:\n\t%s\n\t%s", v1, cv1_n)
  4799  	}
  4800  }
  4801  
  4802  func TestOAuthStateFunctions(t *testing.T) {
  4803  	client := newClient()
  4804  
  4805  	shutdown, _ := newCouper("testdata/integration/functions/02_couper.hcl", test.New(t))
  4806  	defer shutdown()
  4807  
  4808  	helper := test.New(t)
  4809  
  4810  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/csrf", nil)
  4811  	helper.Must(err)
  4812  
  4813  	res, err := client.Do(req)
  4814  	helper.Must(err)
  4815  
  4816  	if res.StatusCode != 200 {
  4817  		t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode)
  4818  	}
  4819  
  4820  	hv := res.Header.Get("x-hv")
  4821  	au, err := url.Parse(res.Header.Get("x-au-state"))
  4822  	helper.Must(err)
  4823  	auq := au.Query()
  4824  	if auq.Get("response_type") != "code" {
  4825  		t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code")
  4826  	}
  4827  	if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" {
  4828  		t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback")
  4829  	}
  4830  	if auq.Get("scope") != "openid profile" {
  4831  		t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile")
  4832  	}
  4833  	if auq.Get("code_challenge_method") != "" {
  4834  		t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "")
  4835  	}
  4836  	if auq.Get("code_challenge") != "" {
  4837  		t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), "")
  4838  	}
  4839  	if auq.Get("state") != hv {
  4840  		t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), hv)
  4841  	}
  4842  	if auq.Get("nonce") != "" {
  4843  		t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), "")
  4844  	}
  4845  	if auq.Get("client_id") != "foo" {
  4846  		t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo")
  4847  	}
  4848  }
  4849  
  4850  func TestOIDCPKCEFunctions(t *testing.T) {
  4851  	client := newClient()
  4852  	helper := test.New(t)
  4853  
  4854  	oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  4855  		if req.URL.Path == "/.well-known/openid-configuration" {
  4856  			rw.Header().Set("Content-Type", "Application/json")
  4857  			body := []byte(`{
  4858  			"issuer": "https://authorization.server",
  4859  			"authorization_endpoint": "https://authorization.server/oauth2/authorize",
  4860  			"token_endpoint": "http://` + req.Host + `/token",
  4861  			"jwks_uri": "http://` + req.Host + `/jwks",
  4862  			"userinfo_endpoint": "http://` + req.Host + `/userinfo"
  4863  			}`)
  4864  			_, werr := rw.Write(body)
  4865  			helper.Must(werr)
  4866  			return
  4867  		} else if req.URL.Path == "/jwks" {
  4868  			rw.Header().Set("Content-Type", "Application/json")
  4869  			_, werr := rw.Write([]byte(`{}`))
  4870  			helper.Must(werr)
  4871  			return
  4872  		}
  4873  		rw.WriteHeader(http.StatusBadRequest)
  4874  	}))
  4875  	defer oauthOrigin.Close()
  4876  
  4877  	shutdown, _, err := newCouperWithTemplate("testdata/integration/functions/03_couper.hcl", test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL})
  4878  	helper.Must(err)
  4879  	defer shutdown()
  4880  
  4881  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/pkce", nil)
  4882  	helper.Must(err)
  4883  
  4884  	res, err := client.Do(req)
  4885  	helper.Must(err)
  4886  
  4887  	if res.StatusCode != 200 {
  4888  		t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode)
  4889  	}
  4890  
  4891  	hv := res.Header.Get("x-hv")
  4892  	au, err := url.Parse(res.Header.Get("x-au-pkce"))
  4893  	helper.Must(err)
  4894  	auq := au.Query()
  4895  	if auq.Get("response_type") != "code" {
  4896  		t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code")
  4897  	}
  4898  	if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" {
  4899  		t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback")
  4900  	}
  4901  	if auq.Get("scope") != "openid profile email" {
  4902  		t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile email")
  4903  	}
  4904  	if auq.Get("code_challenge_method") != "S256" {
  4905  		t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "S256")
  4906  	}
  4907  	if auq.Get("code_challenge") != hv {
  4908  		t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), hv)
  4909  	}
  4910  	if auq.Get("state") != "" {
  4911  		t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "")
  4912  	}
  4913  	if auq.Get("nonce") != "" {
  4914  		t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), "")
  4915  	}
  4916  	if auq.Get("client_id") != "foo" {
  4917  		t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo")
  4918  	}
  4919  	au, err = url.Parse(res.Header.Get("x-au-pkce-rel"))
  4920  	helper.Must(err)
  4921  	auq = au.Query()
  4922  	if auq.Get("redirect_uri") != "http://example.com:8080/oidc/callback" {
  4923  		t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://example.com:8080/oidc/callback")
  4924  	}
  4925  }
  4926  
  4927  func TestOIDCNonceFunctions(t *testing.T) {
  4928  	client := newClient()
  4929  	helper := test.New(t)
  4930  
  4931  	oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  4932  		if req.URL.Path == "/.well-known/openid-configuration" {
  4933  			body := []byte(`{
  4934  			"issuer": "https://authorization.server",
  4935  			"authorization_endpoint": "https://authorization.server/oauth2/authorize",
  4936  			"token_endpoint": "http://` + req.Host + `/token",
  4937  			"jwks_uri": "http://` + req.Host + `/jwks",
  4938  			"userinfo_endpoint": "http://` + req.Host + `/userinfo"
  4939  			}`)
  4940  			_, werr := rw.Write(body)
  4941  			helper.Must(werr)
  4942  
  4943  			return
  4944  		} else if req.URL.Path == "/jwks" {
  4945  			rw.Header().Set("Content-Type", "Application/json")
  4946  			_, werr := rw.Write([]byte(`{}`))
  4947  			helper.Must(werr)
  4948  			return
  4949  		}
  4950  		rw.WriteHeader(http.StatusBadRequest)
  4951  	}))
  4952  	defer oauthOrigin.Close()
  4953  
  4954  	shutdown, _, err := newCouperWithTemplate("testdata/integration/functions/03_couper.hcl", test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL})
  4955  	helper.Must(err)
  4956  	defer shutdown()
  4957  
  4958  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/csrf", nil)
  4959  	helper.Must(err)
  4960  
  4961  	res, err := client.Do(req)
  4962  	helper.Must(err)
  4963  
  4964  	if res.StatusCode != 200 {
  4965  		t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode)
  4966  	}
  4967  
  4968  	hv := res.Header.Get("x-hv")
  4969  	au, err := url.Parse(res.Header.Get("x-au-nonce"))
  4970  	helper.Must(err)
  4971  	auq := au.Query()
  4972  	if auq.Get("response_type") != "code" {
  4973  		t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code")
  4974  	}
  4975  	if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" {
  4976  		t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback")
  4977  	}
  4978  	if auq.Get("scope") != "openid profile" {
  4979  		t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile")
  4980  	}
  4981  	if auq.Get("code_challenge_method") != "" {
  4982  		t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "")
  4983  	}
  4984  	if auq.Get("code_challenge") != "" {
  4985  		t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), "")
  4986  	}
  4987  	if auq.Get("state") != "" {
  4988  		t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "")
  4989  	}
  4990  	if auq.Get("nonce") != hv {
  4991  		t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), hv)
  4992  	}
  4993  	if auq.Get("client_id") != "foo" {
  4994  		t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo")
  4995  	}
  4996  }
  4997  
  4998  func TestOIDCDefaultPKCEFunctions(t *testing.T) {
  4999  	client := newClient()
  5000  	helper := test.New(t)
  5001  
  5002  	oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  5003  		if req.URL.Path == "/.well-known/openid-configuration" {
  5004  			body := []byte(`{
  5005  			"issuer": "https://authorization.server",
  5006  			"authorization_endpoint": "https://authorization.server/oauth2/authorize",
  5007  			"token_endpoint": "http://` + req.Host + `/token",
  5008  			"jwks_uri": "http://` + req.Host + `/jwks",
  5009  			"userinfo_endpoint": "http://` + req.Host + `/userinfo",
  5010  			"code_challenge_methods_supported": ["S256"]
  5011  			}`)
  5012  			_, werr := rw.Write(body)
  5013  			helper.Must(werr)
  5014  			return
  5015  		} else if req.URL.Path == "/jwks" {
  5016  			rw.Header().Set("Content-Type", "Application/json")
  5017  			_, werr := rw.Write([]byte(`{}`))
  5018  			helper.Must(werr)
  5019  			return
  5020  		}
  5021  
  5022  		rw.WriteHeader(http.StatusBadRequest)
  5023  	}))
  5024  	defer oauthOrigin.Close()
  5025  
  5026  	shutdown, _, err := newCouperWithTemplate("testdata/integration/functions/03_couper.hcl", test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL})
  5027  	helper.Must(err)
  5028  	defer shutdown()
  5029  
  5030  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/default", nil)
  5031  	helper.Must(err)
  5032  
  5033  	res, err := client.Do(req)
  5034  	helper.Must(err)
  5035  
  5036  	if res.StatusCode != 200 {
  5037  		t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode)
  5038  	}
  5039  
  5040  	hv := res.Header.Get("x-hv")
  5041  	au, err := url.Parse(res.Header.Get("x-au-default"))
  5042  	helper.Must(err)
  5043  	auq := au.Query()
  5044  	if auq.Get("response_type") != "code" {
  5045  		t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code")
  5046  	}
  5047  	if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" {
  5048  		t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback")
  5049  	}
  5050  	if auq.Get("scope") != "openid profile email address" {
  5051  		t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile email")
  5052  	}
  5053  	if auq.Get("code_challenge_method") != "S256" {
  5054  		t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "S256")
  5055  	}
  5056  	if auq.Get("code_challenge") != hv {
  5057  		t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), hv)
  5058  	}
  5059  	if auq.Get("state") != "" {
  5060  		t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "")
  5061  	}
  5062  	if auq.Get("nonce") != "" {
  5063  		t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), "")
  5064  	}
  5065  	if auq.Get("client_id") != "foo" {
  5066  		t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo")
  5067  	}
  5068  }
  5069  
  5070  func TestOIDCDefaultNonceFunctions(t *testing.T) {
  5071  	client := newClient()
  5072  	helper := test.New(t)
  5073  
  5074  	oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  5075  		if req.URL.Path == "/.well-known/openid-configuration" {
  5076  			body := []byte(`{
  5077  			"issuer": "https://authorization.server",
  5078  			"authorization_endpoint": "https://authorization.server/oauth2/authorize",
  5079  			"token_endpoint": "http://` + req.Host + `/token",
  5080  			"jwks_uri": "http://` + req.Host + `/jwks",
  5081  			"userinfo_endpoint": "http://` + req.Host + `/userinfo"
  5082  			}`)
  5083  			_, werr := rw.Write(body)
  5084  			helper.Must(werr)
  5085  			return
  5086  		} else if req.URL.Path == "/jwks" {
  5087  			rw.Header().Set("Content-Type", "Application/json")
  5088  			_, werr := rw.Write([]byte(`{}`))
  5089  			helper.Must(werr)
  5090  			return
  5091  		}
  5092  		rw.WriteHeader(http.StatusBadRequest)
  5093  	}))
  5094  	defer oauthOrigin.Close()
  5095  
  5096  	shutdown, _, err := newCouperWithTemplate("testdata/integration/functions/03_couper.hcl", test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL})
  5097  	helper.Must(err)
  5098  	defer shutdown()
  5099  
  5100  	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/default", nil)
  5101  	helper.Must(err)
  5102  
  5103  	res, err := client.Do(req)
  5104  	helper.Must(err)
  5105  
  5106  	if res.StatusCode != 200 {
  5107  		t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode)
  5108  	}
  5109  
  5110  	hv := res.Header.Get("x-hv")
  5111  	au, err := url.Parse(res.Header.Get("x-au-default"))
  5112  	helper.Must(err)
  5113  	auq := au.Query()
  5114  	if auq.Get("response_type") != "code" {
  5115  		t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code")
  5116  	}
  5117  	if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" {
  5118  		t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback")
  5119  	}
  5120  	if auq.Get("scope") != "openid profile email address" {
  5121  		t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile")
  5122  	}
  5123  	if auq.Get("code_challenge_method") != "" {
  5124  		t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "")
  5125  	}
  5126  	if auq.Get("code_challenge") != "" {
  5127  		t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), "")
  5128  	}
  5129  	if auq.Get("state") != "" {
  5130  		t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "")
  5131  	}
  5132  	if auq.Get("nonce") != hv {
  5133  		t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), hv)
  5134  	}
  5135  	if auq.Get("client_id") != "foo" {
  5136  		t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo")
  5137  	}
  5138  }
  5139  
  5140  func TestAllowedMethods(t *testing.T) {
  5141  	client := newClient()
  5142  
  5143  	confPath := "testdata/integration/config/11_couper.hcl"
  5144  	shutdown, logHook := newCouper(confPath, test.New(t))
  5145  	defer shutdown()
  5146  
  5147  	token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6ImZvbyJ9.7zkwmXTmzFTKHC0Qnpw7uQCcacogWUvi_JU56uWJlkw"
  5148  
  5149  	type testCase struct {
  5150  		name           string
  5151  		method         string
  5152  		path           string
  5153  		requestHeaders http.Header
  5154  		status         int
  5155  		couperError    string
  5156  	}
  5157  
  5158  	for _, tc := range []testCase{
  5159  		{"path not found, authorized", http.MethodGet, "/api1/not-found", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusNotFound, "route not found error"},
  5160  
  5161  		{"unrestricted, authorized, GET", http.MethodGet, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
  5162  		{"unrestricted, authorized, HEAD", http.MethodHead, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
  5163  		{"unrestricted, authorized, POST", http.MethodPost, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
  5164  		{"unrestricted, authorized, PUT", http.MethodPut, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
  5165  		{"unrestricted, authorized, PATCH", http.MethodPatch, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
  5166  		{"unrestricted, authorized, DELETE", http.MethodDelete, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5167  		{"unrestricted, authorized, OPTIONS", http.MethodOptions, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5168  		{"unrestricted, authorized, CONNECT", http.MethodConnect, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5169  		{"unrestricted, authorized, TRACE", http.MethodTrace, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5170  		{"unrestricted, authorized, BREW", "BREW", "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5171  		{"unrestricted, unauthorized, GET", http.MethodGet, "/api1/unrestricted", http.Header{}, http.StatusUnauthorized, "access control error"},
  5172  		{"unrestricted, unauthorized, BREW", "BREW", "/api1/unrestricted", http.Header{}, http.StatusUnauthorized, "access control error"},
  5173  		{"unrestricted, CORS preflight", http.MethodOptions, "/api1/unrestricted", http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}, "Access-Control-Request-Headers": []string{"Authorization"}}, http.StatusNoContent, ""},
  5174  
  5175  		{"restricted, authorized, GET", http.MethodGet, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
  5176  		{"restricted, authorized, HEAD", http.MethodHead, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5177  		{"restricted, authorized, POST", http.MethodPost, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
  5178  		{"restricted, authorized, PUT", http.MethodPut, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5179  		{"restricted, authorized, PATCH", http.MethodPatch, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5180  		{"restricted, authorized, DELETE", http.MethodDelete, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5181  		{"restricted, authorized, OPTIONS", http.MethodOptions, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5182  		{"restricted, authorized, CONNECT", http.MethodConnect, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5183  		{"restricted, authorized, TRACE", http.MethodTrace, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
  5184  		{"restricted, authorized, BREW", "BREW", "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
  5185  		{"restricted, CORS preflight", http.MethodOptions, "/api1/restricted", http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}, "Access-Control-Request-Headers": []string{"Authorization"}}, http.StatusNoContent, ""},
  5186  
  5187  		{"wildcard, GET", http.MethodGet, "/api1/wildcard", http.Header{}, http.StatusOK, ""},
  5188  		{"wildcard, HEAD", http.MethodHead, "/api1/wildcard", http.Header{}, http.StatusOK, ""},
  5189  		{"wildcard, POST", http.MethodPost, "/api1/wildcard", http.Header{}, http.StatusOK, ""},
  5190  		{"wildcard, PUT", http.MethodPut, "/api1/wildcard", http.Header{}, http.StatusOK, ""},
  5191  		{"wildcard, PATCH", http.MethodPatch, "/api1/wildcard", http.Header{}, http.StatusOK, ""},
  5192  		{"wildcard, DELETE", http.MethodDelete, "/api1/wildcard", http.Header{}, http.StatusOK, ""},
  5193  		{"wildcard, OPTIONS", http.MethodOptions, "/api1/wildcard", http.Header{}, http.StatusOK, ""},
  5194  		{"wildcard, CONNECT", http.MethodConnect, "/api1/wildcard", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5195  		{"wildcard, TRACE", http.MethodTrace, "/api1/wildcard", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5196  		{"wildcard, BREW", "BREW", "/api1/wildcard", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5197  
  5198  		{"wildcard and more, GET", http.MethodGet, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""},
  5199  		{"wildcard and more, HEAD", http.MethodHead, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""},
  5200  		{"wildcard and more, PoSt", "PoSt", "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""},
  5201  		{"wildcard and more, PUT", http.MethodPut, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""},
  5202  		{"wildcard and more, PATCH", http.MethodPatch, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""},
  5203  		{"wildcard and more, DELETE", http.MethodDelete, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""},
  5204  		{"wildcard and more, OPTIONS", http.MethodOptions, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""},
  5205  		{"wildcard and more, CONNECT", http.MethodConnect, "/api1/wildcardAndMore", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5206  		{"wildcard and more, TRACE", http.MethodTrace, "/api1/wildcardAndMore", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5207  		{"wildcard and more, bReW", "bReW", "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""},
  5208  
  5209  		{"blocked, GET", http.MethodGet, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5210  		{"blocked, HEAD", http.MethodHead, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5211  		{"blocked, POST", http.MethodPost, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5212  		{"blocked, PUT", http.MethodPut, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5213  		{"blocked, PATCH", http.MethodPatch, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5214  		{"blocked, DELETE", http.MethodDelete, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5215  		{"blocked, OPTIONS", http.MethodOptions, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5216  		{"blocked, CONNECT", http.MethodConnect, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5217  		{"blocked, TRACE", http.MethodTrace, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5218  		{"blocked, BREW", "BREW", "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5219  
  5220  		{"restricted methods override, GET", http.MethodGet, "/api2/restricted", http.Header{}, http.StatusOK, ""},
  5221  		{"restricted methods override, HEAD", http.MethodHead, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5222  		{"restricted methods override, POST", http.MethodPost, "/api2/restricted", http.Header{}, http.StatusOK, ""},
  5223  		{"restricted methods override, PUT", http.MethodPut, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5224  		{"restricted methods override, PATCH", http.MethodPatch, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5225  		{"restricted methods override, DELETE", http.MethodDelete, "/api2/restricted", http.Header{}, http.StatusOK, ""},
  5226  		{"restricted methods override, OPTIONS", http.MethodOptions, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5227  		{"restricted methods override, CONNECT", http.MethodConnect, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5228  		{"restricted methods override, TRACE", http.MethodTrace, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5229  		{"restricted methods override, BREW", "BREW", "/api2/restricted", http.Header{}, http.StatusOK, ""},
  5230  
  5231  		{"restricted by api only, GET", http.MethodGet, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5232  		{"restricted by api only, HEAD", http.MethodHead, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5233  		{"restricted by api only, POST", http.MethodPost, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5234  		{"restricted by api only, PUT", http.MethodPut, "/api2/restrictedByApiOnly", http.Header{}, http.StatusOK, ""},
  5235  		{"restricted by api only, PATCH", http.MethodPatch, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5236  		{"restricted by api only, DELETE", http.MethodDelete, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5237  		{"restricted by api only, OPTIONS", http.MethodOptions, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5238  		{"restricted by api only, CONNECT", http.MethodConnect, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5239  		{"restricted by api only, TRACE", http.MethodTrace, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5240  		{"restricted by api only, BREW", "BREW", "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5241  
  5242  		{"files, GET", http.MethodGet, "/index.html", http.Header{}, http.StatusOK, ""},
  5243  		{"files, HEAD", http.MethodHead, "/index.html", http.Header{}, http.StatusOK, ""},
  5244  		{"files, POST", http.MethodPost, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5245  		{"files, PUT", http.MethodPut, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5246  		{"files, PATCH", http.MethodPatch, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5247  		{"files, DELETE", http.MethodDelete, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5248  		{"files, OPTIONS", http.MethodOptions, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5249  		{"files, CONNECT", http.MethodConnect, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5250  		{"files, TRACE", http.MethodTrace, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5251  		{"files, BREW", "BREW", "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5252  		{"files, CORS preflight", http.MethodOptions, "/index.html", http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}, "Access-Control-Request-Headers": []string{"Authorization"}}, http.StatusNoContent, ""},
  5253  
  5254  		{"spa, GET", http.MethodGet, "/app/foo", http.Header{}, http.StatusOK, ""},
  5255  		{"spa, HEAD", http.MethodHead, "/app/foo", http.Header{}, http.StatusOK, ""},
  5256  		{"spa, POST", http.MethodPost, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5257  		{"spa, PUT", http.MethodPut, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5258  		{"spa, PATCH", http.MethodPatch, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5259  		{"spa, DELETE", http.MethodDelete, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5260  		{"spa, OPTIONS", http.MethodOptions, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5261  		{"spa, CONNECT", http.MethodConnect, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5262  		{"spa, TRACE", http.MethodTrace, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5263  		{"spa, BREW", "BREW", "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"},
  5264  		{"spa, CORS preflight", http.MethodOptions, "/app/foo", http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}, "Access-Control-Request-Headers": []string{"Authorization"}}, http.StatusNoContent, ""},
  5265  	} {
  5266  		t.Run(tc.name, func(subT *testing.T) {
  5267  			helper := test.New(subT)
  5268  			logHook.Reset()
  5269  			req, err := http.NewRequest(tc.method, "http://example.com:8080"+tc.path, nil)
  5270  			helper.Must(err)
  5271  			req.Header = tc.requestHeaders
  5272  
  5273  			res, err := client.Do(req)
  5274  			helper.Must(err)
  5275  
  5276  			if tc.status != res.StatusCode {
  5277  				subT.Errorf("Unexpected status code given; want: %d; got: %d", tc.status, res.StatusCode)
  5278  			}
  5279  
  5280  			couperError := res.Header.Get("Couper-Error")
  5281  			if tc.couperError != couperError {
  5282  				subT.Errorf("Unexpected couper-error given; want: %q; got: %q", tc.couperError, couperError)
  5283  			}
  5284  		})
  5285  	}
  5286  }
  5287  
  5288  func TestAllowedMethodsCORS_Preflight(t *testing.T) {
  5289  	client := newClient()
  5290  
  5291  	confPath := "testdata/integration/config/11_couper.hcl"
  5292  	shutdown, logHook := newCouper(confPath, test.New(t))
  5293  	defer shutdown()
  5294  
  5295  	type testCase struct {
  5296  		name          string
  5297  		path          string
  5298  		requestMethod string
  5299  		status        int
  5300  		allowMethods  []string
  5301  		couperError   string
  5302  	}
  5303  
  5304  	for _, tc := range []testCase{
  5305  		{"unrestricted, CORS preflight, POST allowed", "/api1/unrestricted", http.MethodPost, http.StatusNoContent, []string{"POST"}, ""},
  5306  		{"restricted, CORS preflight, POST allowed", "/api1/restricted", http.MethodPost, http.StatusNoContent, []string{"POST"}, ""}, // CORS preflight ok even if OPTIONS is otherwise not allowed
  5307  		{"restricted, CORS preflight, PUT not allowed", "/api1/restricted", http.MethodPut, http.StatusNoContent, nil, ""},
  5308  	} {
  5309  		t.Run(tc.name, func(subT *testing.T) {
  5310  			helper := test.New(subT)
  5311  			logHook.Reset()
  5312  			req, err := http.NewRequest(http.MethodOptions, "http://example.com:8080"+tc.path, nil)
  5313  			helper.Must(err)
  5314  			req.Header.Set("Origin", "https://www.example.com")
  5315  			req.Header.Set("Access-Control-Request-Method", tc.requestMethod)
  5316  
  5317  			res, err := client.Do(req)
  5318  			helper.Must(err)
  5319  
  5320  			if tc.status != res.StatusCode {
  5321  				subT.Errorf("Unexpected status code given; want: %d; got: %d", tc.status, res.StatusCode)
  5322  			}
  5323  
  5324  			allowMethods := res.Header.Values("Access-Control-Allow-Methods")
  5325  			if !cmp.Equal(tc.allowMethods, allowMethods) {
  5326  				subT.Errorf(cmp.Diff(tc.allowMethods, allowMethods))
  5327  			}
  5328  
  5329  			couperError := res.Header.Get("Couper-Error")
  5330  			if tc.couperError != couperError {
  5331  				subT.Errorf("Unexpected couper-error given; want: %q; got: %q", tc.couperError, couperError)
  5332  			}
  5333  		})
  5334  	}
  5335  }
  5336  
  5337  func TestEndpoint_ResponseNilEvaluation(t *testing.T) {
  5338  	client := newClient()
  5339  
  5340  	shutdown, hook := newCouper("testdata/integration/endpoint_eval/20_couper.hcl", test.New(t))
  5341  	defer shutdown()
  5342  
  5343  	type testCase struct {
  5344  		path      string
  5345  		expVal    bool
  5346  		expCtyVal string
  5347  	}
  5348  
  5349  	for _, tc := range []testCase{
  5350  		{"/1stchild", true, ""},
  5351  		{"/2ndchild/no", false, ""},
  5352  		{"/child-chain/no", false, ""},
  5353  		{"/list-idx", true, ""},
  5354  		{"/list-idx-splat", true, ""},
  5355  		{"/list-idx/no", false, ""},
  5356  		{"/list-idx-chain/no", false, ""},
  5357  		{"/list-idx-key-chain/no", false, ""},
  5358  		{"/root/no", false, ""},
  5359  		{"/tpl", true, ""},
  5360  		{"/for", true, ""},
  5361  		{"/conditional/false", true, ""},
  5362  		{"/conditional/true", false, ""},
  5363  		{"/conditional/nested", true, ""},
  5364  		{"/conditional/nested/true", true, ""},
  5365  		{"/conditional/nested/false", true, ""},
  5366  		{"/functions/arg-items", true, `{"foo":"bar","obj":{"key":"val"},"xxxx":null}`},
  5367  		{"/functions/tuple-expr", true, `{"array":["a","b"]}`},
  5368  		{"/rte1", true, "2"},
  5369  		{"/rte2", true, "2"},
  5370  		{"/ie1", true, "2"},
  5371  		{"/ie2", true, "2"},
  5372  		{"/uoe1", true, "-2"},
  5373  		{"/uoe2", true, "true"},
  5374  		{"/bad/dereference/string?foo=bar", false, ""},
  5375  		{"/bad/dereference/array?foo=bar", false, ""},
  5376  	} {
  5377  		t.Run(tc.path[1:], func(subT *testing.T) {
  5378  			helper := test.New(subT)
  5379  
  5380  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil)
  5381  			helper.Must(err)
  5382  
  5383  			hook.Reset()
  5384  			defer func() {
  5385  				if subT.Failed() {
  5386  					time.Sleep(time.Millisecond * 100)
  5387  					for _, entry := range hook.AllEntries() {
  5388  						s, _ := entry.String()
  5389  						println(s)
  5390  					}
  5391  				}
  5392  			}()
  5393  
  5394  			res, err := client.Do(req)
  5395  			helper.Must(err)
  5396  
  5397  			if res.StatusCode != http.StatusOK {
  5398  				subT.Errorf("Expected Status OK, got: %d", res.StatusCode)
  5399  				return
  5400  			}
  5401  
  5402  			defer func() {
  5403  				if subT.Failed() {
  5404  					for k := range res.Header {
  5405  						subT.Logf("%s: %s", k, res.Header.Get(k))
  5406  					}
  5407  				}
  5408  			}()
  5409  
  5410  			val, ok := res.Header[http.CanonicalHeaderKey("X-Value")]
  5411  			if !tc.expVal && ok {
  5412  				subT.Errorf("%q: expected no value, got: %q", tc.path, val)
  5413  			} else if tc.expVal && !ok {
  5414  				subT.Errorf("%q: expected X-Value header, got: nothing", tc.path)
  5415  			}
  5416  
  5417  			if res.Header.Get("Z-Value") != "y" {
  5418  				subT.Errorf("additional header Z-Value should always been written")
  5419  			}
  5420  
  5421  			if tc.expCtyVal != "" && tc.expCtyVal != val[0] {
  5422  				subT.Errorf("Want: %s, got: %v", tc.expCtyVal, val[0])
  5423  			}
  5424  
  5425  		})
  5426  	}
  5427  }
  5428  
  5429  func TestEndpoint_ConditionalEvaluationError(t *testing.T) {
  5430  	client := newClient()
  5431  
  5432  	wd, werr := os.Getwd()
  5433  	if werr != nil {
  5434  		t.Fatal(werr)
  5435  	}
  5436  	wd = wd + "/testdata/integration/endpoint_eval"
  5437  
  5438  	shutdown, hook := newCouper("testdata/integration/endpoint_eval/20_couper.hcl", test.New(t))
  5439  	defer shutdown()
  5440  
  5441  	type testCase struct {
  5442  		path       string
  5443  		expMessage string
  5444  	}
  5445  
  5446  	for _, tc := range []testCase{
  5447  		{"/conditional/null", wd + "/20_couper.hcl:281,16-20: Null condition; The condition value is null. Conditions must either be true or false."},
  5448  		{"/conditional/string", wd + "/20_couper.hcl:287,16-21: Incorrect condition type; The condition expression must be of type bool."},
  5449  		{"/conditional/number", wd + "/20_couper.hcl:293,16-17: Incorrect condition type; The condition expression must be of type bool."},
  5450  		{"/conditional/tuple", wd + "/20_couper.hcl:299,16-18: Incorrect condition type; The condition expression must be of type bool."},
  5451  		{"/conditional/object", wd + "/20_couper.hcl:305,16-18: Incorrect condition type; The condition expression must be of type bool."},
  5452  		{"/conditional/string/expr", wd + "/20_couper.hcl:311,16-30: Incorrect condition type; The condition expression must be of type bool."},
  5453  		{"/conditional/number/expr", wd + "/20_couper.hcl:317,16-26: Incorrect condition type; The condition expression must be of type bool."},
  5454  	} {
  5455  		t.Run(tc.path[1:], func(subT *testing.T) {
  5456  			helper := test.New(subT)
  5457  
  5458  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil)
  5459  			helper.Must(err)
  5460  
  5461  			hook.Reset()
  5462  			defer func() {
  5463  				if subT.Failed() {
  5464  					time.Sleep(time.Millisecond * 100)
  5465  					for _, entry := range hook.AllEntries() {
  5466  						s, _ := entry.String()
  5467  						println(s)
  5468  					}
  5469  				}
  5470  			}()
  5471  
  5472  			res, err := client.Do(req)
  5473  			helper.Must(err)
  5474  
  5475  			if res.StatusCode != http.StatusInternalServerError {
  5476  				subT.Errorf("Expected Status InternalServerError, got: %d", res.StatusCode)
  5477  				return
  5478  			}
  5479  
  5480  			time.Sleep(time.Millisecond * 100)
  5481  			entry := hook.LastEntry()
  5482  			if entry != nil && entry.Level == logrus.ErrorLevel {
  5483  				if entry.Message != tc.expMessage {
  5484  					subT.Errorf("wrong error message,\nexp: %s\ngot: %s", tc.expMessage, entry.Message)
  5485  				}
  5486  			}
  5487  		})
  5488  	}
  5489  }
  5490  
  5491  func TestEndpoint_ForLoop(t *testing.T) {
  5492  	client := newClient()
  5493  
  5494  	shutdown, hook := newCouper("testdata/integration/endpoint_eval/21_couper.hcl", test.New(t))
  5495  	defer shutdown()
  5496  
  5497  	type testCase struct {
  5498  		path      string
  5499  		header    http.Header
  5500  		expResult string
  5501  	}
  5502  
  5503  	for _, tc := range []testCase{
  5504  		{"/for0", http.Header{}, `["a","b"]`},
  5505  		{"/for1", http.Header{}, `[0,1]`},
  5506  		{"/for2", http.Header{}, `{"a":0,"b":1}`},
  5507  		{"/for3", http.Header{}, `{"a":[0,1],"b":[2]}`},
  5508  		{"/for4", http.Header{}, `["a","b"]`},
  5509  		{"/for5", http.Header{"x-1": []string{"val1"}, "x-2": []string{"val2"}, "y": []string{`["x-1","x-2"]`}, "z": []string{"pfx"}}, `{"pfx-x-1":"val1","pfx-x-2":"val2"}`},
  5510  	} {
  5511  		t.Run(tc.path[1:], func(subT *testing.T) {
  5512  			helper := test.New(subT)
  5513  
  5514  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil)
  5515  			req.Header = tc.header
  5516  			helper.Must(err)
  5517  
  5518  			hook.Reset()
  5519  
  5520  			res, err := client.Do(req)
  5521  			helper.Must(err)
  5522  
  5523  			resBytes, err := io.ReadAll(res.Body)
  5524  			helper.Must(err)
  5525  
  5526  			helper.Must(res.Body.Close())
  5527  
  5528  			if res.StatusCode != http.StatusOK {
  5529  				subT.Errorf("Expected Status OK, got: %d", res.StatusCode)
  5530  				return
  5531  			}
  5532  
  5533  			result := string(resBytes)
  5534  			if result != tc.expResult {
  5535  				subT.Errorf("Want: %s, got: %v", tc.expResult, result)
  5536  			}
  5537  		})
  5538  	}
  5539  }
  5540  
  5541  func TestWildcardURLAttribute(t *testing.T) {
  5542  	client := newClient()
  5543  
  5544  	shutdown, hook := newCouper("testdata/integration/url/07_couper.hcl", test.New(t))
  5545  	defer shutdown()
  5546  
  5547  	for _, testcase := range []struct{ path, expectedPath, expectedQuery string }{
  5548  		{"/req/anything", "/anything", ""},
  5549  		{"/req/anything/", "/anything/", ""},
  5550  		{"/req-query/anything/?a=b", "/anything/", "a=c"},
  5551  		{"/req-backend/anything/?a=b", "/anything/", "a=c"},
  5552  		{"/proxy/anything", "/anything", ""},
  5553  		{"/proxy/anything/", "/anything/", ""},
  5554  		{"/proxy-query/anything/?a=b", "/anything/", "a=c"},
  5555  		{"/proxy-backend/anything", "/anything", ""},
  5556  		{"/proxy-backend-rel/anything?a=b", "/anything", "a=c"},
  5557  		{"/proxy-backend-path/other-wildcard?a=b", "/anything", "a=c"},
  5558  	} {
  5559  		t.Run(testcase.path[1:], func(st *testing.T) {
  5560  			helper := test.New(st)
  5561  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+testcase.path, nil)
  5562  			helper.Must(err)
  5563  
  5564  			hook.Reset()
  5565  
  5566  			res, err := client.Do(req)
  5567  			helper.Must(err)
  5568  
  5569  			if res.StatusCode != http.StatusOK {
  5570  				st.Error("expected status OK")
  5571  			}
  5572  
  5573  			b, err := io.ReadAll(res.Body)
  5574  			helper.Must(res.Body.Close())
  5575  			helper.Must(err)
  5576  
  5577  			type result struct {
  5578  				Path     string
  5579  				RawQuery string
  5580  			}
  5581  			r := result{}
  5582  			helper.Must(json.Unmarshal(b, &r))
  5583  			//st.Logf("%v", r)
  5584  
  5585  			if testcase.expectedPath != r.Path {
  5586  				st.Errorf("Expected path: %q, got: %q", testcase.expectedPath, r.Path)
  5587  			}
  5588  
  5589  			if testcase.expectedQuery != r.RawQuery {
  5590  				st.Errorf("Expected query: %q, got: %q", testcase.expectedQuery, r.RawQuery)
  5591  			}
  5592  		})
  5593  	}
  5594  }
  5595  
  5596  func TestEnvironmentSetting(t *testing.T) {
  5597  	helper := test.New(t)
  5598  	tests := []struct {
  5599  		env string
  5600  	}{
  5601  		{"foo"},
  5602  		{"bar"},
  5603  	}
  5604  
  5605  	template := `
  5606  	  server {
  5607  	    endpoint "/" {
  5608  	      response {
  5609  	        environment "foo" {
  5610  	          headers = { X-Env: "foo" }
  5611  	        }
  5612  	        environment "bar" {
  5613  	          headers = { X-Env: "bar" }
  5614  	        }
  5615  	      }
  5616  	    }
  5617  	  }
  5618  	  settings {
  5619  	    environment = "%s"
  5620  	  }
  5621  	`
  5622  
  5623  	file, err := os.CreateTemp("", "tmpfile-")
  5624  	helper.Must(err)
  5625  	defer file.Close()
  5626  	defer os.Remove(file.Name())
  5627  
  5628  	client := newClient()
  5629  	for _, tt := range tests {
  5630  		t.Run(tt.env, func(subT *testing.T) {
  5631  			config := []byte(fmt.Sprintf(template, tt.env))
  5632  			err := os.Truncate(file.Name(), 0)
  5633  			helper.Must(err)
  5634  			_, err = file.Seek(0, 0)
  5635  			helper.Must(err)
  5636  			_, err = file.Write(config)
  5637  			helper.Must(err)
  5638  
  5639  			couperConfig, err := configload.LoadFile(file.Name(), "")
  5640  			helper.Must(err)
  5641  
  5642  			shutdown, _ := newCouperWithConfig(couperConfig, helper)
  5643  			defer shutdown()
  5644  
  5645  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/", nil)
  5646  			helper.Must(err)
  5647  
  5648  			res, err := client.Do(req)
  5649  			helper.Must(err)
  5650  
  5651  			if header := res.Header.Get("X-Env"); header != tt.env {
  5652  				subT.Errorf("Unexpected header:\n\tWant: %q\n\tGot:  %q", tt.env, header)
  5653  			}
  5654  		})
  5655  	}
  5656  }
  5657  
  5658  func TestWildcardVsEmptyPathParams(t *testing.T) {
  5659  	client := newClient()
  5660  
  5661  	shutdown, _ := newCouper("testdata/integration/url/08_couper.hcl", test.New(t))
  5662  	defer shutdown()
  5663  
  5664  	type testCase struct {
  5665  		path     string
  5666  		expected string
  5667  	}
  5668  
  5669  	for _, tc := range []testCase{
  5670  		{"/foo", "/**"},
  5671  		{"/p1/A/B/C", "/**"},
  5672  		{"/p1/A/B/", "/p1/{x}/{y}"},
  5673  		{"/p1/A//", "/**"},
  5674  		{"/p1///", "/**"},
  5675  		{"/p1//", "/**"},
  5676  		{"/p1/A/B", "/p1/{x}/{y}"},
  5677  		{"/p1/A/", "/**"},
  5678  		{"/p1/A", "/**"},
  5679  		{"/p1/", "/**"},
  5680  		{"/p1", "/**"},
  5681  		{"/p2/A/B/C", "/p2/**"},
  5682  		{"/p2/A/B/", "/p2/{x}/{y}"},
  5683  		{"/p2/A/B", "/p2/{x}/{y}"},
  5684  		{"/p2/A/", "/p2/**"},
  5685  		{"/p2/A", "/p2/**"},
  5686  		{"/p2/", "/p2/**"},
  5687  		{"/p2", "/p2/**"},
  5688  		{"/p3/A/B/C", "/p3/**"},
  5689  		{"/p3/A/B/", "/p3/{x}/{y}"},
  5690  		{"/p3/A/B", "/p3/{x}/{y}"},
  5691  		{"/p3/A/", "/p3/{x}"},
  5692  		{"/p3/A", "/p3/{x}"},
  5693  		{"/p3/", "/p3/**"},
  5694  		{"/p3", "/p3/**"},
  5695  	} {
  5696  		t.Run(tc.path, func(subT *testing.T) {
  5697  			helper := test.New(subT)
  5698  			req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil)
  5699  			helper.Must(err)
  5700  
  5701  			res, err := client.Do(req)
  5702  			helper.Must(err)
  5703  
  5704  			if res.StatusCode != http.StatusOK {
  5705  				subT.Errorf("Unexpected status: want: 200, got %d", res.StatusCode)
  5706  			}
  5707  
  5708  			match := res.Header.Get("Match")
  5709  			if match != tc.expected {
  5710  				subT.Errorf("Unexpected match for %s: want %s, got %s", tc.path, tc.expected, match)
  5711  			}
  5712  		})
  5713  	}
  5714  }