04958c6951
Co-authored-by: Julien Salleyron <julien@containo.us>
449 lines
10 KiB
Go
449 lines
10 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/containous/mux"
|
|
"github.com/containous/traefik/middlewares/requestdecorator"
|
|
"github.com/containous/traefik/testhelpers"
|
|
"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 rule",
|
|
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: "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:/bar",
|
|
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: "Invalid rule syntax",
|
|
rule: "Query:param_one=true, /path2;Path: /path1",
|
|
expected: map[string]int{
|
|
"http://localhost/foo?bar=baz": http.StatusNotFound,
|
|
},
|
|
},
|
|
}
|
|
|
|
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 := mux.NewRouter()
|
|
router.SkipClean(true)
|
|
|
|
err := addRoute(context.Background(), router, 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",
|
|
},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
test := test
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
router := mux.NewRouter()
|
|
|
|
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 := addRoute(context.Background(), router, route.rule, route.priority, handler)
|
|
require.NoError(t, err, route)
|
|
}
|
|
|
|
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{}
|
|
hostRegexp(rt, test.hostExp)
|
|
|
|
for testURL, match := range test.urls {
|
|
req := testhelpers.MustNewRequest(http.MethodGet, testURL, nil)
|
|
assert.Equal(t, match, rt.Match(req, &mux.RouteMatch{}), testURL)
|
|
}
|
|
})
|
|
}
|
|
}
|