mirror of
				https://github.com/traefik/traefik.git
				synced 2025-10-31 16:31:16 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			737 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			737 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package rules
 | |
| 
 | |
| import (
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/containous/traefik/v2/pkg/middlewares/requestdecorator"
 | |
| 	"github.com/containous/traefik/v2/pkg/testhelpers"
 | |
| 	"github.com/gorilla/mux"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| )
 | |
| 
 | |
| func Test_addRoute(t *testing.T) {
 | |
| 	testCases := []struct {
 | |
| 		desc          string
 | |
| 		rule          string
 | |
| 		headers       map[string]string
 | |
| 		expected      map[string]int
 | |
| 		expectedError bool
 | |
| 	}{
 | |
| 		{
 | |
| 			desc:          "no tree",
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule with no matcher",
 | |
| 			rule:          "rulewithnotmatcher",
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "PathPrefix",
 | |
| 			rule: "PathPrefix(`/foo`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "wrong PathPrefix",
 | |
| 			rule: "PathPrefix(`/bar`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host",
 | |
| 			rule: "Host(`localhost`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host with trailing period in rule",
 | |
| 			rule: "Host(`localhost.`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host with trailing period in domain",
 | |
| 			rule: "Host(`localhost`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost./foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host with trailing period in domain and rule",
 | |
| 			rule: "Host(`localhost.`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost./foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "wrong Host",
 | |
| 			rule: "Host(`nope`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host and PathPrefix",
 | |
| 			rule: "Host(`localhost`) && PathPrefix(`/foo`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host and PathPrefix wrong PathPrefix",
 | |
| 			rule: "Host(`localhost`) && PathPrefix(`/bar`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host and PathPrefix wrong Host",
 | |
| 			rule: "Host(`nope`) && PathPrefix(`/foo`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host and PathPrefix Host OR, first host",
 | |
| 			rule: "Host(`nope`,`localhost`) && PathPrefix(`/foo`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host and PathPrefix Host OR, second host",
 | |
| 			rule: "Host(`nope`,`localhost`) && PathPrefix(`/foo`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://nope/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Host and PathPrefix Host OR, first host and wrong PathPrefix",
 | |
| 			rule: "Host(`nope,localhost`) && PathPrefix(`/bar`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "HostRegexp with capturing group",
 | |
| 			rule: "HostRegexp(`{subdomain:(foo\\.)?bar\\.com}`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://foo.bar.com": http.StatusOK,
 | |
| 				"http://bar.com":     http.StatusOK,
 | |
| 				"http://fooubar.com": http.StatusNotFound,
 | |
| 				"http://barucom":     http.StatusNotFound,
 | |
| 				"http://barcom":      http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "HostRegexp with non capturing group",
 | |
| 			rule: "HostRegexp(`{subdomain:(?:foo\\.)?bar\\.com}`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://foo.bar.com": http.StatusOK,
 | |
| 				"http://bar.com":     http.StatusOK,
 | |
| 				"http://fooubar.com": http.StatusNotFound,
 | |
| 				"http://barucom":     http.StatusNotFound,
 | |
| 				"http://barcom":      http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Methods with GET",
 | |
| 			rule: "Method(`GET`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Methods with GET and POST",
 | |
| 			rule: "Method(`GET`,`POST`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Methods with POST",
 | |
| 			rule: "Method(`POST`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusMethodNotAllowed,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Header with matching header",
 | |
| 			rule: "Headers(`Content-Type`,`application/json`)",
 | |
| 			headers: map[string]string{
 | |
| 				"Content-Type": "application/json",
 | |
| 			},
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Header without matching header",
 | |
| 			rule: "Headers(`Content-Type`,`application/foo`)",
 | |
| 			headers: map[string]string{
 | |
| 				"Content-Type": "application/json",
 | |
| 			},
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "HeaderRegExp with matching header",
 | |
| 			rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)",
 | |
| 			headers: map[string]string{
 | |
| 				"Content-Type": "application/json",
 | |
| 			},
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "HeaderRegExp without matching header",
 | |
| 			rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)",
 | |
| 			headers: map[string]string{
 | |
| 				"Content-Type": "application/foo",
 | |
| 			},
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "HeaderRegExp with matching second header",
 | |
| 			rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)",
 | |
| 			headers: map[string]string{
 | |
| 				"Content-Type": "application/text",
 | |
| 			},
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Query with multiple params",
 | |
| 			rule: "Query(`foo=bar`, `bar=baz`)",
 | |
| 			expected: map[string]int{
 | |
| 				"http://localhost/foo?foo=bar&bar=baz": http.StatusOK,
 | |
| 				"http://localhost/foo?bar=baz":         http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule with simple path",
 | |
| 			rule: `Path("/a")`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://plop/a": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: `Rule with a simple host`,
 | |
| 			rule: `Host("plop")`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://plop": http.StatusOK,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule with Path AND Host",
 | |
| 			rule: `Path("/a") && Host("plop")`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://plop/a":  http.StatusOK,
 | |
| 				"http://plopi/a": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule with Host OR Host",
 | |
| 			rule: `Host("tchouk") || Host("pouet")`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/toto": http.StatusOK,
 | |
| 				"http://pouet/a":     http.StatusOK,
 | |
| 				"http://plopi/a":     http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule with host OR (host AND path)",
 | |
| 			rule: `Host("tchouk") || (Host("pouet") && Path("/powpow"))`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/toto":   http.StatusOK,
 | |
| 				"http://tchouk/powpow": http.StatusOK,
 | |
| 				"http://pouet/powpow":  http.StatusOK,
 | |
| 				"http://pouet/toto":    http.StatusNotFound,
 | |
| 				"http://plopi/a":       http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule with host OR host AND path",
 | |
| 			rule: `Host("tchouk") || Host("pouet") && Path("/powpow")`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/toto":   http.StatusOK,
 | |
| 				"http://tchouk/powpow": http.StatusOK,
 | |
| 				"http://pouet/powpow":  http.StatusOK,
 | |
| 				"http://pouet/toto":    http.StatusNotFound,
 | |
| 				"http://plopi/a":       http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule with (host OR host) AND path",
 | |
| 			rule: `(Host("tchouk") || Host("pouet")) && Path("/powpow")`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/toto":   http.StatusNotFound,
 | |
| 				"http://tchouk/powpow": http.StatusOK,
 | |
| 				"http://pouet/powpow":  http.StatusOK,
 | |
| 				"http://pouet/toto":    http.StatusNotFound,
 | |
| 				"http://plopi/a":       http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule with multiple host AND path",
 | |
| 			rule: `(Host("tchouk","pouet")) && Path("/powpow")`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/toto":   http.StatusNotFound,
 | |
| 				"http://tchouk/powpow": http.StatusOK,
 | |
| 				"http://pouet/powpow":  http.StatusOK,
 | |
| 				"http://pouet/toto":    http.StatusNotFound,
 | |
| 				"http://plopi/a":       http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule with multiple host AND multiple path",
 | |
| 			rule: `Host("tchouk","pouet") && Path("/powpow", "/titi")`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/toto":   http.StatusNotFound,
 | |
| 				"http://tchouk/powpow": http.StatusOK,
 | |
| 				"http://pouet/powpow":  http.StatusOK,
 | |
| 				"http://tchouk/titi":   http.StatusOK,
 | |
| 				"http://pouet/titi":    http.StatusOK,
 | |
| 				"http://pouet/toto":    http.StatusNotFound,
 | |
| 				"http://plopi/a":       http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule with (host AND path) OR (host AND path)",
 | |
| 			rule: `(Host("tchouk") && Path("/titi")) || ((Host("pouet")) && Path("/powpow"))`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/titi":   http.StatusOK,
 | |
| 				"http://tchouk/powpow": http.StatusNotFound,
 | |
| 				"http://pouet/powpow":  http.StatusOK,
 | |
| 				"http://pouet/toto":    http.StatusNotFound,
 | |
| 				"http://plopi/a":       http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule without quote",
 | |
| 			rule:          `Host(tchouk)`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule case UPPER",
 | |
| 			rule: `(HOST("tchouk") && PATHPREFIX("/titi"))`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/titi":   http.StatusOK,
 | |
| 				"http://tchouk/powpow": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule case lower",
 | |
| 			rule: `(host("tchouk") && pathprefix("/titi"))`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/titi":   http.StatusOK,
 | |
| 				"http://tchouk/powpow": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule case CamelCase",
 | |
| 			rule: `(Host("tchouk") && PathPrefix("/titi"))`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/titi":   http.StatusOK,
 | |
| 				"http://tchouk/powpow": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Rule case Title",
 | |
| 			rule: `(Host("tchouk") && Pathprefix("/titi"))`,
 | |
| 			expected: map[string]int{
 | |
| 				"http://tchouk/titi":   http.StatusOK,
 | |
| 				"http://tchouk/powpow": http.StatusNotFound,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule Path with error",
 | |
| 			rule:          `Path("titi")`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule PathPrefix with error",
 | |
| 			rule:          `PathPrefix("titi")`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule HostRegexp with error",
 | |
| 			rule:          `HostRegexp("{test")`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule Headers with error",
 | |
| 			rule:          `Headers("titi")`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule HeadersRegexp with error",
 | |
| 			rule:          `HeadersRegexp("titi")`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule Query",
 | |
| 			rule:          `Query("titi")`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule Query with bad syntax",
 | |
| 			rule:          `Query("titi={test")`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule with Path without args",
 | |
| 			rule:          `Host("tchouk") && Path()`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule with an empty path",
 | |
| 			rule:          `Host("tchouk") && Path("")`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:          "Rule with an empty path",
 | |
| 			rule:          `Host("tchouk") && Path("", "/titi")`,
 | |
| 			expectedError: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range testCases {
 | |
| 		test := test
 | |
| 		t.Run(test.desc, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
 | |
| 			router, err := NewRouter()
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			err = router.AddRoute(test.rule, 0, handler)
 | |
| 			if test.expectedError {
 | |
| 				require.Error(t, err)
 | |
| 			} else {
 | |
| 				require.NoError(t, err)
 | |
| 
 | |
| 				// RequestDecorator is necessary for the host rule
 | |
| 				reqHost := requestdecorator.New(nil)
 | |
| 
 | |
| 				results := make(map[string]int)
 | |
| 				for calledURL := range test.expected {
 | |
| 					w := httptest.NewRecorder()
 | |
| 
 | |
| 					req := testhelpers.MustNewRequest(http.MethodGet, calledURL, nil)
 | |
| 					for key, value := range test.headers {
 | |
| 						req.Header.Set(key, value)
 | |
| 					}
 | |
| 					reqHost.ServeHTTP(w, req, router.ServeHTTP)
 | |
| 					results[calledURL] = w.Code
 | |
| 				}
 | |
| 				assert.Equal(t, test.expected, results)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_addRoutePriority(t *testing.T) {
 | |
| 	type Case struct {
 | |
| 		xFrom    string
 | |
| 		rule     string
 | |
| 		priority int
 | |
| 	}
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		desc     string
 | |
| 		path     string
 | |
| 		cases    []Case
 | |
| 		expected string
 | |
| 	}{
 | |
| 		{
 | |
| 			desc: "Higher priority on second rule",
 | |
| 			path: "/my",
 | |
| 			cases: []Case{
 | |
| 				{
 | |
| 					xFrom:    "header1",
 | |
| 					rule:     "PathPrefix(`/my`)",
 | |
| 					priority: 10,
 | |
| 				},
 | |
| 				{
 | |
| 					xFrom:    "header2",
 | |
| 					rule:     "PathPrefix(`/my`)",
 | |
| 					priority: 20,
 | |
| 				},
 | |
| 			},
 | |
| 			expected: "header2",
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Higher priority on first rule",
 | |
| 			path: "/my",
 | |
| 			cases: []Case{
 | |
| 				{
 | |
| 					xFrom:    "header1",
 | |
| 					rule:     "PathPrefix(`/my`)",
 | |
| 					priority: 20,
 | |
| 				},
 | |
| 				{
 | |
| 					xFrom:    "header2",
 | |
| 					rule:     "PathPrefix(`/my`)",
 | |
| 					priority: 10,
 | |
| 				},
 | |
| 			},
 | |
| 			expected: "header1",
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Higher priority on second rule with different rule",
 | |
| 			path: "/mypath",
 | |
| 			cases: []Case{
 | |
| 				{
 | |
| 					xFrom:    "header1",
 | |
| 					rule:     "PathPrefix(`/mypath`)",
 | |
| 					priority: 10,
 | |
| 				},
 | |
| 				{
 | |
| 					xFrom:    "header2",
 | |
| 					rule:     "PathPrefix(`/my`)",
 | |
| 					priority: 20,
 | |
| 				},
 | |
| 			},
 | |
| 			expected: "header2",
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Higher priority on longest rule (longest first)",
 | |
| 			path: "/mypath",
 | |
| 			cases: []Case{
 | |
| 				{
 | |
| 					xFrom: "header1",
 | |
| 					rule:  "PathPrefix(`/mypath`)",
 | |
| 				},
 | |
| 				{
 | |
| 					xFrom: "header2",
 | |
| 					rule:  "PathPrefix(`/my`)",
 | |
| 				},
 | |
| 			},
 | |
| 			expected: "header1",
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Higher priority on longest rule (longest second)",
 | |
| 			path: "/mypath",
 | |
| 			cases: []Case{
 | |
| 				{
 | |
| 					xFrom: "header1",
 | |
| 					rule:  "PathPrefix(`/my`)",
 | |
| 				},
 | |
| 				{
 | |
| 					xFrom: "header2",
 | |
| 					rule:  "PathPrefix(`/mypath`)",
 | |
| 				},
 | |
| 			},
 | |
| 			expected: "header2",
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "Higher priority on longest rule (longest third)",
 | |
| 			path: "/mypath",
 | |
| 			cases: []Case{
 | |
| 				{
 | |
| 					xFrom: "header1",
 | |
| 					rule:  "PathPrefix(`/my`)",
 | |
| 				},
 | |
| 				{
 | |
| 					xFrom: "header2",
 | |
| 					rule:  "PathPrefix(`/mypa`)",
 | |
| 				},
 | |
| 				{
 | |
| 					xFrom: "header3",
 | |
| 					rule:  "PathPrefix(`/mypath`)",
 | |
| 				},
 | |
| 			},
 | |
| 			expected: "header3",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range testCases {
 | |
| 		test := test
 | |
| 		t.Run(test.desc, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 			router, err := NewRouter()
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			for _, route := range test.cases {
 | |
| 				route := route
 | |
| 				handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 					w.Header().Set("X-From", route.xFrom)
 | |
| 				})
 | |
| 
 | |
| 				err := router.AddRoute(route.rule, route.priority, handler)
 | |
| 				require.NoError(t, err, route.rule)
 | |
| 			}
 | |
| 
 | |
| 			router.SortRoutes()
 | |
| 
 | |
| 			w := httptest.NewRecorder()
 | |
| 			req := testhelpers.MustNewRequest(http.MethodGet, test.path, nil)
 | |
| 
 | |
| 			router.ServeHTTP(w, req)
 | |
| 
 | |
| 			assert.Equal(t, test.expected, w.Header().Get("X-From"))
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestHostRegexp(t *testing.T) {
 | |
| 	testCases := []struct {
 | |
| 		desc    string
 | |
| 		hostExp string
 | |
| 		urls    map[string]bool
 | |
| 	}{
 | |
| 		{
 | |
| 			desc:    "capturing group",
 | |
| 			hostExp: "{subdomain:(foo\\.)?bar\\.com}",
 | |
| 			urls: map[string]bool{
 | |
| 				"http://foo.bar.com": true,
 | |
| 				"http://bar.com":     true,
 | |
| 				"http://fooubar.com": false,
 | |
| 				"http://barucom":     false,
 | |
| 				"http://barcom":      false,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc:    "non capturing group",
 | |
| 			hostExp: "{subdomain:(?:foo\\.)?bar\\.com}",
 | |
| 			urls: map[string]bool{
 | |
| 				"http://foo.bar.com": true,
 | |
| 				"http://bar.com":     true,
 | |
| 				"http://fooubar.com": false,
 | |
| 				"http://barucom":     false,
 | |
| 				"http://barcom":      false,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc:    "regex insensitive",
 | |
| 			hostExp: "{dummy:[A-Za-z-]+\\.bar\\.com}",
 | |
| 			urls: map[string]bool{
 | |
| 				"http://FOO.bar.com": true,
 | |
| 				"http://foo.bar.com": true,
 | |
| 				"http://fooubar.com": false,
 | |
| 				"http://barucom":     false,
 | |
| 				"http://barcom":      false,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc:    "insensitive host",
 | |
| 			hostExp: "{dummy:[a-z-]+\\.bar\\.com}",
 | |
| 			urls: map[string]bool{
 | |
| 				"http://FOO.bar.com": true,
 | |
| 				"http://foo.bar.com": true,
 | |
| 				"http://fooubar.com": false,
 | |
| 				"http://barucom":     false,
 | |
| 				"http://barcom":      false,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			desc:    "insensitive host simple",
 | |
| 			hostExp: "foo.bar.com",
 | |
| 			urls: map[string]bool{
 | |
| 				"http://FOO.bar.com": true,
 | |
| 				"http://foo.bar.com": true,
 | |
| 				"http://fooubar.com": false,
 | |
| 				"http://barucom":     false,
 | |
| 				"http://barcom":      false,
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range testCases {
 | |
| 		test := test
 | |
| 		t.Run(test.desc, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			rt := &mux.Route{}
 | |
| 			err := hostRegexp(rt, test.hostExp)
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			for testURL, match := range test.urls {
 | |
| 				req := testhelpers.MustNewRequest(http.MethodGet, testURL, nil)
 | |
| 				assert.Equal(t, match, rt.Match(req, &mux.RouteMatch{}), testURL)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestParseDomains(t *testing.T) {
 | |
| 	testCases := []struct {
 | |
| 		description   string
 | |
| 		expression    string
 | |
| 		domain        []string
 | |
| 		errorExpected bool
 | |
| 	}{
 | |
| 		{
 | |
| 			description:   "Many host rules",
 | |
| 			expression:    "Host(`foo.bar`,`test.bar`)",
 | |
| 			domain:        []string{"foo.bar", "test.bar"},
 | |
| 			errorExpected: false,
 | |
| 		},
 | |
| 		{
 | |
| 			description:   "Many host rules upper",
 | |
| 			expression:    "HOST(`foo.bar`,`test.bar`)",
 | |
| 			domain:        []string{"foo.bar", "test.bar"},
 | |
| 			errorExpected: false,
 | |
| 		},
 | |
| 		{
 | |
| 			description:   "Many host rules lower",
 | |
| 			expression:    "host(`foo.bar`,`test.bar`)",
 | |
| 			domain:        []string{"foo.bar", "test.bar"},
 | |
| 			errorExpected: false,
 | |
| 		},
 | |
| 		{
 | |
| 			description:   "No host rule",
 | |
| 			expression:    "Path(`/test`)",
 | |
| 			errorExpected: false,
 | |
| 		},
 | |
| 		{
 | |
| 			description:   "Host rule and another rule",
 | |
| 			expression:    "Host(`foo.bar`) && Path(`/test`)",
 | |
| 			domain:        []string{"foo.bar"},
 | |
| 			errorExpected: false,
 | |
| 		},
 | |
| 		{
 | |
| 			description:   "Host rule to trim and another rule",
 | |
| 			expression:    "Host(`Foo.Bar`) && Path(`/test`)",
 | |
| 			domain:        []string{"foo.bar"},
 | |
| 			errorExpected: false,
 | |
| 		},
 | |
| 		{
 | |
| 			description:   "Host rule with no domain",
 | |
| 			expression:    "Host() && Path(`/test`)",
 | |
| 			errorExpected: false,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range testCases {
 | |
| 		test := test
 | |
| 		t.Run(test.expression, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			domains, err := ParseDomains(test.expression)
 | |
| 
 | |
| 			if test.errorExpected {
 | |
| 				require.Errorf(t, err, "unable to parse correctly the domains in the Host rule from %q", test.expression)
 | |
| 			} else {
 | |
| 				require.NoError(t, err, "%s: Error while parsing domain.", test.expression)
 | |
| 			}
 | |
| 
 | |
| 			assert.EqualValues(t, test.domain, domains, "%s: Error parsing domains from expression.", test.expression)
 | |
| 		})
 | |
| 	}
 | |
| }
 |