local Router local path_handling_tests = require "spec.fixtures.router_path_handling_tests" local uuid = require("kong.tools.uuid").uuid local get_expression = require("kong.router.transform").get_expression local deep_copy = require("kong.tools.table").deep_copy local function reload_router(flavor, subsystem) _G.kong = { configuration = { router_flavor = flavor, }, } ngx.config.subsystem = subsystem or "http" -- luacheck: ignore package.loaded["kong.router.fields"] = nil package.loaded["kong.router.atc"] = nil package.loaded["kong.router.compat"] = nil package.loaded["kong.router.expressions"] = nil package.loaded["kong.router"] = nil Router = require "kong.router" end local function new_router(cases, old_router) return Router.new(cases, nil, nil, old_router) end local service = { name = "service-invalid", protocol = "http", } local headers_mt = { __index = function(t, k) local u = rawget(t, string.upper(k)) if u then return u end return rawget(t, string.lower(k)) end } local spy_stub = { nop = function() end } local function mock_ngx(method, request_uri, headers, queries, vars) method = method or "GET" request_uri = request_uri or "/" headers = headers or {} queries = queries or {} vars = vars or {} local _ngx _ngx = { log = ngx.log, re = ngx.re, var = setmetatable({ request_uri = request_uri, http_kong_debug = headers.kong_debug }, { __index = function(_, key) if key:sub(1, 5) == "http_" then spy_stub.nop() return headers[key:sub(6)] or vars[key] end return vars[key] end, }), req = { get_method = function() return method end, get_headers = function() return setmetatable(headers, headers_mt) end, get_uri_args = function() return queries end, }, } return _ngx end for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do describe("Router (flavor = " .. flavor .. ")", function() reload_router(flavor) local it_trad_only = (flavor == "traditional") and it or pending describe("split_port()", function() local split_port = require("kong.router.traditional").split_port it("splits port number", function() for _, case in ipairs({ { { "" }, { "", "", false } }, { { "localhost" }, { "localhost", "localhost", false } }, { { "localhost:" }, { "localhost", "localhost", false } }, { { "localhost:80" }, { "localhost", "localhost:80", true } }, { { "localhost:23h" }, { "localhost:23h", "localhost:23h", false } }, { { "localhost/24" }, { "localhost/24", "localhost/24", false } }, { { "::1" }, { "::1", "::1", false } }, { { "[::1]" }, { "::1", "[::1]", false } }, { { "[::1]:" }, { "::1", "[::1]:", false } }, { { "[::1]:80" }, { "::1", "[::1]:80", true } }, { { "[::1]:80b" }, { "[::1]:80b", "[::1]:80b", false } }, { { "[::1]/96" }, { "[::1]/96", "[::1]/96", false } }, { { "", 88 }, { "", ":88", false } }, { { "localhost", 88 }, { "localhost", "localhost:88", false } }, { { "localhost:", 88 }, { "localhost", "localhost:88", false } }, { { "localhost:80", 88 }, { "localhost", "localhost:80", true } }, { { "localhost:23h", 88 }, { "localhost:23h", "[localhost:23h]:88", false } }, { { "localhost/24", 88 }, { "localhost/24", "localhost/24:88", false } }, { { "::1", 88 }, { "::1", "[::1]:88", false } }, { { "[::1]", 88 }, { "::1", "[::1]:88", false } }, { { "[::1]:", 88 }, { "::1", "[::1]:88", false } }, { { "[::1]:80", 88 }, { "::1", "[::1]:80", true } }, { { "[::1]:80b", 88 }, { "[::1]:80b", "[::1]:80b:88", false } }, { { "[::1]/96", 88 }, { "[::1]/96", "[::1]/96:88", false } }, }) do assert.same(case[2], { split_port(unpack(case[1])) }) end end) end) describe("new()", function() describe("[errors]", function() it("enforces args types", function() assert.error_matches(function() Router.new() end, "expected arg #1 routes to be a table", nil, true) end) it("enforces routes fields types", function() local router, err = Router.new { { route = { }, service = { name = "service-invalid" }, }, } assert.is_nil(router) assert.equal("could not categorize route", err) end) end) end) describe("select()", function() local use_case, router lazy_setup(function() use_case = { -- 1. host { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "domain-1.org", "domain-2.org" }, }, }, -- 2. method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", methods = { "TRACE" }, } }, -- 3. uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { "/my-route" }, } }, -- 4. host + uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", paths = { "/route-4" }, hosts = { "domain-1.org", "domain-2.org" }, }, }, -- 5. host + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8105", hosts = { "domain-1.org", "domain-2.org" }, methods = { "POST", "PUT", "PATCH" }, }, }, -- 6. uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8106", methods = { "POST", "PUT", "PATCH", }, paths = { "/route-6" }, } }, -- 7. host + uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8107", hosts = { "domain-with-uri-1.org", "domain-with-uri-2.org" }, methods = { "POST", "PUT", "PATCH", }, paths = { "/my-route-uri" }, }, }, -- 8. serviceless-route { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8108", paths = { "/serviceless" }, } }, -- 9. headers (single) { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8109", headers = { location = { "my-location-1", "my-location-2", }, }, }, }, -- 10. headers (multiple) { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8110", headers = { location = { "my-location-1", }, version = { "v1", "v2", }, }, }, }, -- 11. headers + uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8111", headers = { location = { "my-location-1", "my-location-2", }, }, paths = { "/headers-uri" }, }, }, -- 12. host + headers + uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8112", hosts = { "domain-with-headers-1.org", "domain-with-headers-2.org" }, headers = { location = { "my-location-1", "my-location-2", }, }, methods = { "POST", "PUT", "PATCH", }, paths = { "/headers-host-uri-method" }, }, }, -- 13. host + port { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8113", hosts = { "domain-1.org:321", "domain-2.org" }, }, }, -- 14. no "any-port" route { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8114", hosts = { "domain-3.org:321", }, }, }, -- 15. headers (regex) { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8115", headers = { user_agent = { "~*windows|linux|os\\s+x\\s*[\\d\\._]+|solaris|bsd", }, }, }, }, } router = assert(new_router(use_case)) -- let atc-router happy local _ngx = mock_ngx("GET", "/", { a = "1" }) router._set_ngx(_ngx) end) it("[host]", function() -- host local match_t = router:select("GET", "/", "domain-1.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same(use_case[1].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host] ignores default port", function() -- host local match_t = router:select("GET", "/", "domain-1.org:80") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same(use_case[1].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it_trad_only("[host] weird port matches no-port route", function() local match_t = router:select("GET", "/", "domain-1.org:123") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(use_case[1].route.hosts[1], match_t.matches.host) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host] matches specific port", function() -- host local match_t = router:select("GET", "/", "domain-1.org:321") assert.truthy(match_t) assert.same(use_case[13].route, match_t.route) if flavor == "traditional" then assert.same(use_case[13].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host] matches specific port on port-only route", function() -- host local match_t = router:select("GET", "/", "domain-3.org:321") assert.truthy(match_t) assert.same(use_case[14].route, match_t.route) if flavor == "traditional" then assert.same(use_case[14].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host] fails just because of port on port-only route", function() -- host local match_t = router:select("GET", "/", "domain-3.org:123") assert.falsy(match_t) end) it("[uri]", function() -- uri local match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[3].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("[uri + empty host]", function() -- uri only (no Host) -- Supported for HTTP/1.0 requests without a Host header local match_t = router:select("GET", "/my-route-uri", "") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[3].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("[method]", function() -- method local match_t = router:select("TRACE", "/", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.same(nil, match_t.matches.host) if flavor == "traditional" then assert.same(use_case[2].route.methods[1], match_t.matches.method) end assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[host + uri]", function() -- host + uri local match_t = router:select("GET", "/route-4", "domain-1.org") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) if flavor == "traditional" then assert.same(use_case[4].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[4].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("[host + method]", function() -- host + method local match_t = router:select("POST", "/", "domain-1.org") assert.truthy(match_t) assert.same(use_case[5].route, match_t.route) if flavor == "traditional" then assert.same(use_case[5].route.hosts[1], match_t.matches.host) assert.same(use_case[5].route.methods[1], match_t.matches.method) end assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("[uri + method]", function() -- uri + method local match_t = router:select("PUT", "/route-6", "domain.org") assert.truthy(match_t) assert.same(use_case[6].route, match_t.route) assert.same(nil, match_t.matches.host) if flavor == "traditional" then assert.same(use_case[6].route.methods[2], match_t.matches.method) assert.same(use_case[6].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("[host + uri + method]", function() -- uri + method local match_t = router:select("PUT", "/my-route-uri", "domain-with-uri-2.org") assert.truthy(match_t) assert.same(use_case[7].route, match_t.route) if flavor == "traditional" then assert.same(use_case[7].route.hosts[2], match_t.matches.host) assert.same(use_case[7].route.methods[2], match_t.matches.method) assert.same(use_case[7].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("single [headers] value", function() -- headers (single) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-1" }) assert.truthy(match_t) assert.same(use_case[9].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-1" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-2" }) assert.truthy(match_t) assert.same(use_case[9].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-2" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = { "my-location-3", "my-location-2" } }) assert.truthy(match_t) assert.same(use_case[9].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-2" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-3" }) assert.is_nil(match_t) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = { "my-location-3", "foo" } }) assert.is_nil(match_t) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" }) assert.truthy(match_t) assert.same(use_case[15].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ user_agent = "mozilla/5.0 (x11; linux x86_64) applewebkit/537.36 (khtml, like gecko) chrome/83.0.4103.116 safari/537.36" }, match_t.matches.headers) end end) it("multiple [headers] values", function() -- headers (multiple) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-1", version = "v1", }) assert.truthy(match_t) assert.same(use_case[10].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-1", version = "v1", }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-1", version = "v2", }) assert.truthy(match_t) assert.same(use_case[10].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-1", version = "v2", }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = { "my-location-3", "my-location-1" }, version = "v2", }) assert.truthy(match_t) assert.same(use_case[10].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-1", version = "v2", }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = { "my-location-3", "my-location-2" }, version = "v2", }) -- fallback to Route 9 assert.truthy(match_t) assert.same(use_case[9].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-2" }, match_t.matches.headers) end end) it("[headers + uri]", function() -- headers + uri local match_t = router:select("GET", "/headers-uri", nil, "http", nil, nil, nil, nil, nil, { location = "my-location-2" }) assert.truthy(match_t) assert.same(use_case[11].route, match_t.route) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[11].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same({ location = "my-location-2" }, match_t.matches.headers) end end) it("[host + headers + uri + method]", function() -- host + headers + uri + method local match_t = router:select("PUT", "/headers-host-uri-method", "domain-with-headers-1.org", "http", nil, nil, nil, nil, nil, { location = "my-location-2", }) assert.truthy(match_t) assert.same(use_case[12].route, match_t.route) if flavor == "traditional" then assert.same(use_case[12].route.hosts[1], match_t.matches.host) assert.same(use_case[12].route.methods[2], match_t.matches.method) assert.same(use_case[12].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) if flavor == "traditional" then assert.same(use_case[12].route.headers.location[2], match_t.matches.headers.location) end end) it("[serviceless]", function() local match_t = router:select("GET", "/serviceless") assert.truthy(match_t) assert.is_nil(match_t.service) assert.is_nil(match_t.matches.uri_captures) assert.same(use_case[8].route, match_t.route) if flavor == "traditional" then assert.same(use_case[8].route.paths[1], match_t.matches.uri) end end) if flavor == "traditional" then describe("[IPv6 literal host]", function() local use_case, router lazy_setup(function() use_case = { -- 1: no port, with and without brackets, unique IPs { service = service, route = { hosts = { "::11", "[::12]" }, }, }, -- 2: no port, with and without brackets, same hosts as 4 { service = service, route = { hosts = { "::21", "[::22]" }, }, }, -- 3: unique IPs, with port { service = service, route = { hosts = { "[::31]:321", "[::32]:321" }, }, }, -- 4: same hosts as 2, with port, needs brackets { service = service, route = { hosts = { "[::21]:321", "[::22]:321" }, }, }, } router = assert(new_router(use_case)) end) describe("no-port route is any-port", function() describe("no-port request", function() it("plain match", function() local match_t = assert(router:select("GET", "/", "::11")) assert.same(use_case[1].route, match_t.route) end) it("with brackets", function() local match_t = assert(router:select("GET", "/", "[::11]")) assert.same(use_case[1].route, match_t.route) end) end) it("explicit port still matches", function() local match_t = assert(router:select("GET", "/", "[::11]:654")) assert.same(use_case[1].route, match_t.route) end) end) describe("port-specific route", function() it("matches by port", function() local match_t = assert(router:select("GET", "/", "[::21]:321")) assert.same(use_case[4].route, match_t.route) local match_t = assert(router:select("GET", "/", "[::31]:321")) assert.same(use_case[3].route, match_t.route) end) it("matches other ports to any-port fallback", function() local match_t = assert(router:select("GET", "/", "[::21]:654")) assert.same(use_case[2].route, match_t.route) end) it("fails if there's no any-port route", function() local match_t = router:select("GET", "/", "[::31]:654") assert.falsy(match_t) end) end) end) end describe("[uri prefix]", function() it("matches when given [uri] is in request URI prefix", function() -- uri prefix local match_t = router:select("GET", "/my-route/some/path", "domain.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same(use_case[3].route.paths[1], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("does not supersede another route with a longer [uri]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/my-route/hello" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/my-route" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("/my-route/hello", match_t.matches.uri) end match_t = router:select("GET", "/my-route/hello/world", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("/my-route/hello", match_t.matches.uri) end match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("/my-route", match_t.matches.uri) end match_t = router:select("GET", "/my-route/world", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("/my-route", match_t.matches.uri) end end) it("does not supersede another route with a longer [uri] while [methods] are also defined", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "POST", "PUT", "GET" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", methods = { "POST", "PUT", "GET" }, paths = { "/my-route/hello" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/my-route/hello/world", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/my-route/world", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("does not superseds another route with a longer [uri] while [hosts] are also defined", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "domain.org" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "domain.org" }, paths = { "/my-route/hello" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/my-route/hello/world", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/my-route/world", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("does not supersede another route with a longer [uri] when a better [uri] match exists for another [host]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "example.com" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "example.com" }, paths = { "/my-route/hello" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "example.net" }, paths = { "/my-route/hello/world" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello/world", "example.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) local match_t = router:select("GET", "/my-route/hello/world/and/goodnight", "example.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("only matches [uri prefix] as a prefix (anchored mode)", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/something/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "example.com" }, paths = { "/my-route" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/something/my-route", "example.com") assert.truthy(match_t) -- would be route-2 if URI matching was not prefix-only (anchored mode) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("/something/my-route", match_t.matches.uri) end end) end) describe("[uri regex]", function() it("matches with [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/\d+/profile]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/users/123/profile", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same([[/users/\d+/profile]], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("matches the right route when several ones have a [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/route/persons/\d{3}]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { [[~/route/persons/\d{3}/following]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { [[~/route/persons/\d{3}/[a-z]+]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/route/persons/456", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("matches a [uri regex] even if a [prefix uri] got a match", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[/route/persons]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { [[~/route/persons/\d+/profile]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/route/persons/123/profile", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same([[/route/persons/\d+/profile]], match_t.matches.uri) end assert.same(nil, match_t.matches.uri_captures) end) it("matches a [uri regex] even if a [uri] got an exact match", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/route/fixture" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "~/route/(fixture)" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/route/fixture", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) if flavor == "traditional" then assert.same("/route/(fixture)", match_t.matches.uri) end end) it("matches a [uri regex + host] even if a [prefix uri] got a match", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "route.com" }, paths = { "/pat" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "route.com" }, paths = { "/path" }, methods = { "POST" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "route.com" }, paths = { "~/(path)" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/path", "route.com") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) if flavor == "traditional" then assert.same("route.com", match_t.matches.host) assert.same("/(path)", match_t.matches.uri) end assert.same(nil, match_t.matches.method) end) it("matches from the beginning of the request URI [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/prefix/[0-9]+]] } }, }, } local router = assert(new_router(use_case)) -- sanity local match_t = router:select("GET", "/prefix/123", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(nil, match_t.matches.host) assert.same(nil, match_t.matches.method) match_t = router:select("GET", "/extra/prefix/123", "domain.org") assert.is_nil(match_t) end) end) describe("[wildcard host]", function() local use_case, router lazy_setup(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.route.com" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "route.*" }, }, }, } router = assert(new_router(use_case)) end) it("matches leftmost wildcards", function() local match_t = router:select("GET", "/", "foo.route.com", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same(use_case[1].route.hosts[1], match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("matches rightmost wildcards", function() local match_t = router:select("GET", "/", "route.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("matches any port in request", function() local match_t = router:select("GET", "/", "route.org:123") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) local match_t = router:select("GET", "/", "foo.route.com:123", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("matches port-specific routes", function() table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "*.route.net:123" }, }, }) table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", hosts = { "route.*:123" }, -- same as [2] but port-specific }, }) router = assert(new_router(use_case)) finally(function() table.remove(use_case) table.remove(use_case) router = assert(new_router(use_case)) end) -- match the right port local match_t = router:select("GET", "/", "foo.route.net:123") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) -- fail different port assert.is_nil(router:select("GET", "/", "foo.route.net:456")) -- port-specific is higher priority local match_t = router:select("GET", "/", "route.org:123") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) it("prefers port-specific even for http default port", function() table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "route.*:80" }, -- same as [2] but port-specific }, }) router = assert(new_router(use_case)) finally(function() table.remove(use_case) router = assert(new_router(use_case)) end) -- non-port matches any local match_t = assert(router:select("GET", "/", "route.org:123")) assert.same(use_case[2].route, match_t.route) -- port 80 goes to port-specific route local match_t = assert(router:select("GET", "/", "route.org:80")) assert.same(use_case[3].route, match_t.route) -- even if it's implicit port 80 if flavor == "traditional" then local match_t = assert(router:select("GET", "/", "route.org")) assert.same(use_case[3].route, match_t.route) end end) it("prefers port-specific even for https default port", function() table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "route.*:443" }, -- same as [2] but port-specific }, }) router = assert(new_router(use_case)) finally(function() table.remove(use_case) router = assert(new_router(use_case)) end) -- non-port matches any local match_t = assert(router:select("GET", "/", "route.org:123")) assert.same(use_case[2].route, match_t.route) -- port 443 goes to port-specific route local match_t = assert(router:select("GET", "/", "route.org:443")) assert.same(use_case[3].route, match_t.route) -- even if it's implicit port 443 if flavor == "traditional" then local match_t = assert(router:select("GET", "/", "route.org", "https")) assert.same(use_case[3].route, match_t.route) end end) it("does not take precedence over a plain host", function() table.insert(use_case, 1, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "plain.route.com" }, }, }) table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", hosts = { "route.com" }, }, }) finally(function() table.remove(use_case, 1) table.remove(use_case) router = assert(new_router(use_case)) end) router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "route.com") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) if flavor == "traditional" then assert.same("route.com", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) match_t = router:select("GET", "/", "route.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) if flavor == "traditional" then assert.same("route.*", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) match_t = router:select("GET", "/", "plain.route.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("plain.route.com", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) match_t = router:select("GET", "/", "foo.route.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("*.route.com", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("matches [wildcard host + path] even if a similar [plain host] exists", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.route.com" }, paths = { "/path1" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "plain.route.com" }, paths = { "/path2" }, }, }, } router = assert(new_router(use_case)) local match_t = router:select("GET", "/path1", "plain.route.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same("*.route.com", match_t.matches.host) assert.same("/path1", match_t.matches.uri) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri_captures) end) it("matches [plain host + path] even if a matching [wildcard host] exists", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.route.com" }, paths = { "/path1" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "plain.route.com" }, paths = { "/path2" }, }, }, } router = assert(new_router(use_case)) local match_t = router:select("GET", "/path2", "plain.route.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("plain.route.com", match_t.matches.host) assert.same("/path2", match_t.matches.uri) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri_captures) end) it("submatch_weight [wildcard host port] > [wildcard host] ", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "route.*" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "route.*:80", "route.com.*" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "route.org:80") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("route.*:80", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end) it("matches a [wildcard host + port] even if a [wildcard host] matched", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "route.*" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "route.*:123" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "route.*:80" }, }, }, } local router = assert(new_router(use_case)) -- explicit port local match_t = router:select("GET", "/", "route.org:123") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("route.*:123", match_t.matches.host) end assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) -- implicit port if flavor == "traditional" then local match_t = router:select("GET", "/", "route.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) assert.same("route.*:80", match_t.matches.host) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end end) it("matches [wildcard/plain + uri + method]", function() finally(function() table.remove(use_case) router = assert(new_router(use_case)) end) table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.domain.com", "example.com" }, paths = { "/path" }, methods = { "GET", "TRACE" }, }, }) router = assert(new_router(use_case)) local match_t = router:select("POST", "/path", "foo.domain.com") assert.is_nil(match_t) match_t = router:select("GET", "/path", "foo.domain.com") assert.truthy(match_t) assert.same(use_case[#use_case].route, match_t.route) match_t = router:select("TRACE", "/path", "example.com") assert.truthy(match_t) assert.same(use_case[#use_case].route, match_t.route) match_t = router:select("POST", "/path", "foo.domain.com") assert.is_nil(match_t) end) end) describe("[wildcard host] + [uri regex]", function() it("matches", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "*.example.com" }, paths = { [[~/users/\d+/profile]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "*.example.com" }, paths = { [[/users]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/users/123/profile", "test.example.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/users", "test.example.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) end) describe("[headers]", function() it("evaluates Routes with more [headers] first", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", headers = { version = { "v1", "v2" }, user_agent = { "foo", "bar" }, }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", headers = { version = { "v1", "v2" }, user_agent = { "foo", "bar" }, location = { "east", "west" }, }, }, } } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { version = "v1", user_agent = "foo", location = { "north", "west" }, }) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("names are case-insensitive", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", headers = { ["USER_AGENT"] = { "foo", "bar" }, }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", headers = { user_agent = { "baz" }, }, }, } } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, setmetatable({ user_agent = "foo", }, headers_mt)) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same({ user_agent = "foo" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, setmetatable({ ["USER_AGENT"] = "baz", }, headers_mt)) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same({ user_agent = "baz" }, match_t.matches.headers) end end) it("matches values in a case-insensitive way", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", headers = { user_agent = { "foo", "bar" }, }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", headers = { user_agent = { "BAZ" }, }, }, } } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { user_agent = "FOO", }) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) if flavor == "traditional" then assert.same({ user_agent = "foo" }, match_t.matches.headers) end local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { user_agent = "baz", }) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same({ user_agent = "baz" }, match_t.matches.headers) end end) end) if flavor ~= "traditional" then describe("[wildcard sni]", function() local use_case, router lazy_setup(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", snis = { "*.sni.test" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", snis = { "sni.*" }, }, }, } router = assert(new_router(use_case)) end) it("matches leftmost wildcards", function() for _, sni in ipairs({"foo.sni.test", "foo.bar.sni.test"}) do local match_t = router:select("GET", "/", "any.test", "https", nil, nil, nil, nil, sni) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(nil, match_t.matches.method) assert.same(nil, match_t.matches.uri) assert.same(nil, match_t.matches.uri_captures) end end) it("matches rightmost wildcards", function() for _, sni in ipairs({"sni.foo", "sni.foo.bar"}) do local match_t = router:select("GET", "/", "any.test", "https", nil, nil, nil, nil, sni) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end end) it("doesn't match wildcard", function() for _, sni in ipairs({"bar.sni.foo", "foo.sni.test.bar"}) do local match_t = router:select("GET", "/", "any.test", "https", nil, nil, nil, nil, sni) assert.is_nil(match_t) end end) end) end -- if flavor ~= "traditional" then if flavor ~= "traditional" then describe("incremental rebuild", function() local router local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo", }, updated_at = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/bar", }, updated_at = 90, }, } } before_each(function() router = assert(new_router(use_case)) end) it("matches initially", function() local match_t = router:select("GET", "/foo") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/bar") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("update/remove works", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo1", }, updated_at = 100, }, }, } local nrouter = assert(new_router(use_case, router)) assert.equal(nrouter, router) local match_t = nrouter:select("GET", "/foo1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = nrouter:select("GET", "/bar") assert.falsy(match_t) end) it("update with wrong route", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "~/delay/(?[^\\/]+)$", }, updated_at = 100, }, }, } local ok, nrouter = pcall(new_router, use_case, router) assert(ok) assert.equal(nrouter, router) assert.equal(#nrouter.routes, 0) end) it("update skips routes if updated_at is unchanged", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo", }, updated_at = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/baz", }, updated_at = 90, }, } } local nrouter = assert(new_router(use_case, router)) assert.equal(nrouter, router) local match_t = nrouter:select("GET", "/baz") assert.falsy(match_t) match_t = nrouter:select("GET", "/bar") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("clears match and negative cache after rebuild", function() local match_t = router:select("GET", "/baz") assert.falsy(match_t) match_t = router:select("GET", "/foo") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foz", }, updated_at = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/baz", }, updated_at = 100, }, } } local nrouter = assert(new_router(use_case, router)) assert.equal(nrouter, router) local match_t = nrouter:select("GET", "/foo") assert.falsy(match_t) match_t = nrouter:select("GET", "/baz") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("detects concurrent incremental builds", function() local use_cases = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foz", }, updated_at = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/baz", }, updated_at = 100, }, } } -- needs to be larger than YIELD_ITERATIONS for i = 1, 2000 do use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb" .. string.format("%04d", i), paths = { "/" .. i, }, updated_at = 100, }, } end local threads = {} -- make sure yield() actually works ngx.IS_CLI = false for i = 1, 10 do threads[i] = ngx.thread.spawn(function() return new_router(use_cases, router) end) end local error_detected = false for i = 1, 10 do local _, _, err = ngx.thread.wait(threads[i]) if err == "concurrent incremental router rebuild without mutex, this is unsafe" then error_detected = true break end end ngx.IS_CLI = true assert.truthy(error_detected) end) it("generates the correct diff", function() local old_router = assert(new_router({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/a" }, updated_at = 100, }, }, })) local add_matcher = spy.on(old_router.router, "add_matcher") local remove_matcher = spy.on(old_router.router, "remove_matcher") assert(new_router({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/b", }, updated_at = 101, } }, { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/c", }, updated_at = 102, }, }, }, old_router)) assert.spy(add_matcher).was_called(2) assert.spy(remove_matcher).was_called(1) end) it("remove the correct diff", function() local old_router = assert(new_router({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/a" }, updated_at = 100, }, }, { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/b", }, updated_at = 100, }, }, })) local add_matcher = spy.on(old_router.router, "add_matcher") local remove_matcher = spy.on(old_router.router, "remove_matcher") assert(new_router({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/a", }, updated_at = 100, } }, }, old_router)) assert.spy(add_matcher).was_called(1) assert.spy(remove_matcher).was_called(2) end) it("update the correct diff: one route", function() local old_router = assert(new_router({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/a" }, updated_at = 100, }, }, { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/b", }, updated_at = 90, }, }, })) local add_matcher = spy.on(old_router.router, "add_matcher") local remove_matcher = spy.on(old_router.router, "remove_matcher") assert(new_router({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/aa", }, updated_at = 101, } }, { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/b", }, updated_at = 90, }, }, }, old_router)) assert.spy(add_matcher).was_called(1) assert.spy(remove_matcher).was_called(1) end) it("update the correct diff: two routes", function() local old_router = assert(new_router({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/a" }, updated_at = 100, }, }, { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/b", }, updated_at = 90, }, }, })) local add_matcher = spy.on(old_router.router, "add_matcher") local remove_matcher = spy.on(old_router.router, "remove_matcher") assert(new_router({ { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/aa", }, updated_at = 101, } }, { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/bb", }, updated_at = 91, }, }, }, old_router)) assert.spy(add_matcher).was_called(2) assert.spy(remove_matcher).was_called(2) end) it("works well if route has no proper id", function() local wrong_use_case = { { service = service, route = { id = nil, -- no id here paths = { "/foo", }, updated_at = 100, }, }, } local nrouter = new_router(wrong_use_case, router) assert.is_nil(nrouter) assert.falsy(router.rebuilding) local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo1", }, updated_at = 100, }, }, } local nrouter = assert(new_router(use_case, router)) assert.equal(nrouter, router) assert.falsy(router.rebuilding) local match_t = nrouter:select("GET", "/foo1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = nrouter:select("GET", "/bar") assert.falsy(match_t) end) end) describe("check empty route fields", function() local use_case before_each(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, paths = { "/foo", }, }, }, } end) local empty_values = { {}, ngx.null, nil } for i = 1, 3 do local v = empty_values[i] it("empty methods", function() use_case[1].route.methods = v assert.equal(get_expression(use_case[1].route), [[(http.path ^= r#"/foo"#)]]) assert(new_router(use_case)) end) it("empty hosts", function() use_case[1].route.hosts = v assert.equal(get_expression(use_case[1].route), [[(http.method == r#"GET"#) && (http.path ^= r#"/foo"#)]]) assert(new_router(use_case)) end) it("empty headers", function() use_case[1].route.headers = v assert.equal(get_expression(use_case[1].route), [[(http.method == r#"GET"#) && (http.path ^= r#"/foo"#)]]) assert(new_router(use_case)) end) it("empty paths", function() use_case[1].route.paths = v assert.equal(get_expression(use_case[1].route), [[(http.method == r#"GET"#)]]) assert(new_router(use_case)) end) it("empty snis", function() use_case[1].route.snis = v assert.equal(get_expression(use_case[1].route), [[(http.method == r#"GET"#) && (http.path ^= r#"/foo"#)]]) assert(new_router(use_case)) end) end end) describe("raw string", function() local use_case before_each(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, }, }, } end) it("path has '\"'", function() use_case[1].route.paths = { [[~/\"/*$]], } assert.equal([[(http.method == r#"GET"#) && (http.path ~ r#"^/\"/*$"#)]], get_expression(use_case[1].route)) assert(new_router(use_case)) end) it("path has '\"#'", function() use_case[1].route.paths = { [[~/\"#/*$]], } assert.equal([[(http.method == r#"GET"#) && (http.path ~ "^/\\\"#/*$")]], get_expression(use_case[1].route)) assert(new_router(use_case)) end) end) describe("check regex with '\\'", function() local use_case before_each(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, }, }, } end) it("regex path has double '\\'", function() use_case[1].route.paths = { [[~/\\/*$]], } assert.equal([[(http.method == r#"GET"#) && (http.path ~ r#"^/\\/*$"#)]], get_expression(use_case[1].route)) assert(new_router(use_case)) end) it("regex path has '\\d'", function() use_case[1].route.paths = { [[~/\d+]], } assert.equal([[(http.method == r#"GET"#) && (http.path ~ r#"^/\d+"#)]], get_expression(use_case[1].route)) assert(new_router(use_case)) end) end) describe("generate http expression", function() local use_case before_each(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", }, }, } end) it("should always add lower() when matching http.headers.*", function() use_case[1].route.methods = { "GET" } use_case[1].route.headers = { test = { "~*Quote" }, } assert.equal([[(http.method == r#"GET"#) && (any(lower(http.headers.test)) ~ r#"quote"#)]], get_expression(use_case[1].route)) assert(new_router(use_case)) end) it("should use 'net.dst.port' when deprecating 'net.port'", function() use_case[1].route.hosts = { "www.example.com:8000" } assert.equal([[((http.host == r#"www.example.com"# && net.dst.port == 8000))]], get_expression(use_case[1].route)) assert(new_router(use_case)) end) end) end -- if flavor ~= "traditional" describe("normalization stopgap measurements", function() local use_case, router lazy_setup(function() use_case = { -- plain { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8100", paths = { "/plain/a.b.c", -- /plain/a.b.c }, }, }, -- percent encoding with unreserved char, route should not be normalized { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/plain/a.b%58c", -- /plain/a.bXc }, }, }, -- regex. It is no longer normalized since 3.0 { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "~/reg%65x/\\d+", -- /regex/\d+ }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { "~/regex-meta/%5Cd\\+%2E", -- /regex-meta/\d\+. }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", paths = { "~/regex-reserved%2Fabc", -- /regex-reserved/abc }, }, }, } router = assert(new_router(use_case)) end) it("matches against plain text paths", function() local match_t = router:select("GET", "/plain/a.b.c", "example.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) -- route no longer normalize user configured path match_t = router:select("GET", "/plain/a.bXc", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/plain/a.b%58c", "example.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/plain/aab.c", "example.com") assert.falsy(match_t) end) it("matches against regex paths", function() local match_t = router:select("GET", "/regex/123", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/reg%65x/123", "example.com") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) match_t = router:select("GET", "/regex/\\d+", "example.com") assert.falsy(match_t) end) it("escapes meta character after percent decoding from regex paths", function() local match_t = router:select("GET", "/regex-meta/123a", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/regex-meta/\\d+.", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/regex-meta/%5Cd+%2E", "example.com") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) it("leave reserved characters alone in regex paths", function() local match_t = router:select("GET", "/regex-reserved/abc", "example.com") assert.falsy(match_t) match_t = router:select("GET", "/regex-reserved%2Fabc", "example.com") assert.truthy(match_t) assert.same(use_case[5].route, match_t.route) end) end) describe("edge-cases", function() it("[host] and [uri] have higher priority than [method]", function() local use_case = { -- 1. host { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "domain-1.org", "domain-2.org" }, }, }, -- 2. method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", methods = { "TRACE" }, } }, -- 3. uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { "/my-route" }, } }, -- 4. host + uri { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", paths = { "/route-4" }, hosts = { "domain-1.org", "domain-2.org" }, }, }, -- 5. host + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8105", hosts = { "domain-1.org", "domain-2.org" }, methods = { "POST", "PUT", "PATCH" }, }, }, -- 6. uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8106", methods = { "POST", "PUT", "PATCH", }, paths = { "/route-6" }, } }, -- 7. host + uri + method { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8107", hosts = { "domain-with-uri-1.org", "domain-with-uri-2.org" }, methods = { "POST", "PUT", "PATCH", }, paths = { "/my-route-uri" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("TRACE", "/", "domain-2.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) -- uri local match_t = router:select("TRACE", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) it("half [uri] and [host] match does not supersede another route", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "host1.com" }, paths = { "/v1/path" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "host2.com" }, paths = { "/" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/v1/path", "host1.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/v1/path", "host2.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("half [wildcard host] and [method] match does not supersede another route", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "host.*" }, methods = { "GET" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "host.*" }, methods = { "POST" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "host.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("POST", "/", "host.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("half [uri regex] and [method] match does not supersede another route", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, paths = { [[~/users/\d+/profile]] }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", methods = { "POST" }, paths = { [[~/users/\d*/profile]] }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/users/123/profile", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("POST", "/users/123/profile", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("[method] does not supersede [uri prefix]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/example" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/example", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) match_t = router:select("GET", "/example/status/200", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("[method] does not supersede [wildcard host]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", methods = { "GET" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "domain.*" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "nothing.com") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select("GET", "/", "domain.com") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it_trad_only("does not supersede another route with a longer [uri prefix]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/a", "/bbbbbbb" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/a/bb" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/a/bb/foobar", "domain.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) describe("root / [uri]", function() lazy_setup(function() table.insert(use_case, 1, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb810f", paths = { "/" }, } }) end) lazy_teardown(function() table.remove(use_case, 1) end) it("request with [method]", function() local router = assert(new_router(use_case)) local match_t = router:select("GET", "/", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("does not supersede another route", function() local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route", "domain.org") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) match_t = router:select("GET", "/my-route/hello/world", "domain.org") assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) it("acts as a catch-all route", function() local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foobar/baz", "domain.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) end) describe("multiple routes of same category with conflicting values", function() -- reload router to reset combined cached matchers reload_router(flavor) local n = 6 lazy_setup(function() -- all those routes are of the same category: -- [host + uri] for i = 1, n - 1 do table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb811" .. i, hosts = { "domain.org" }, paths = { "/my-uri" }, }, }) end table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8121", hosts = { "domain.org" }, paths = { "/my-target-uri" }, }, }) end) lazy_teardown(function() for _ = 1, n do table.remove(use_case) end end) it("matches correct route", function() local router = assert(new_router(use_case)) -- let atc-router happy local _ngx = mock_ngx("GET", "/", { a = "1" }) router._set_ngx(_ngx) local match_t = router:select("GET", "/my-target-uri", "domain.org") assert.truthy(match_t) assert.same(use_case[#use_case].route, match_t.route) end) end) it("more [headers] has priority over longer [paths]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", headers = { version = { "v1" }, }, paths = { "/my-route/hello" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", headers = { version = { "v1" }, location = { "us-east" }, }, paths = { "/my-route" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/my-route/hello", "domain.org", "http", nil, nil, nil, nil, nil, { version = "v1", location = "us-east", }) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("/my-route", match_t.matches.uri) assert.same({ version = "v1", location = "us-east" }, match_t.matches.headers) end local match_t = router:select("GET", "/my-route/hello/world", "http", "domain.org", nil, nil, nil, nil, nil, { version = "v1", location = "us-east", }) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) if flavor == "traditional" then assert.same("/my-route", match_t.matches.uri) assert.same({ version = "v1", location = "us-east" }, match_t.matches.headers) end end) end) describe("misses", function() it("invalid [host]", function() assert.is_nil(router:select("GET", "/", "domain-3.org")) end) it("invalid host in [host + uri]", function() assert.is_nil(router:select("GET", "/route-4", "domain-3.org")) end) it("invalid host in [host + method]", function() assert.is_nil(router:select("GET", "/", "domain-3.org")) end) it("invalid method in [host + uri + method]", function() assert.is_nil(router:select("GET", "/some-uri", "domain-with-uri-2.org")) end) it("invalid uri in [host + uri + method]", function() assert.is_nil(router:select("PUT", "/some-uri-foo", "domain-with-uri-2.org")) end) it("does not match when given [uri] is in URI but not in prefix", function() local match_t = router:select("GET", "/some-other-prefix/my-route", "domain.org") assert.is_nil(match_t) end) it("invalid [headers]", function() assert.is_nil(router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = "invalid-location" })) end) it("invalid headers in [headers + uri]", function() assert.is_nil(router:select("GET", "/headers-uri", nil, "http", nil, nil, nil, nil, nil, { location = "invalid-location" })) end) it("invalid headers in [headers + uri + method]", function() assert.is_nil(router:select("PUT", "/headers-uri-method", nil, "http", nil, nil, nil, nil, nil, { location = "invalid-location" })) end) it("invalid headers in [headers + host + uri + method]", function() assert.is_nil(router:select("PUT", "/headers-host-uri-method", nil, "http", nil, nil, nil, nil, nil, { location = "invalid-location", host = "domain-with-headers-1.org" })) end) end) describe("#benchmarks", function() --[[ Run: $ busted --tags=benchmarks To estimate how much time matching an route in a worst-case scenario with a set of ~1000 registered routes would take. We are aiming at sub-ms latency. ]] describe("plain [host]", function() local router local target_domain local benchmark_use_cases = {} lazy_setup(function() for i = 1, 10^5 do benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", i), hosts = { "domain-" .. i .. ".org" }, }, } end target_domain = "domain-" .. #benchmark_use_cases .. ".org" router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("GET", "/", target_domain) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) describe("[method + uri + host]", function() local router local target_uri local target_domain local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n - 1 do -- insert a lot of routes that don't match (missing methods) -- but have conflicting paths and hosts (domain-.org) benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", i), hosts = { "domain-" .. n .. ".org" }, paths = { "/my-route-" .. n }, }, } end -- insert our target route, which has the proper method as well benchmark_use_cases[n] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", n), hosts = { "domain-" .. n .. ".org" }, methods = { "POST" }, paths = { "/my-route-" .. n }, }, } target_uri = "/my-route-" .. n target_domain = "domain-" .. n .. ".org" router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("POST", target_uri, target_domain) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) describe("[headers]", function() describe("single key", function() local router local target_location local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n do benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", i), headers = { location = { "somewhere-" .. i }, }, }, } end target_location = "somewhere-" .. n router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { location = target_location }) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) if flavor == "traditional" then describe("10^4 keys", function() local router local target_val local target_key local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n do benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a1" .. string.format("%06d", i), headers = { ["key-" .. i] = { "somewhere" }, }, }, } end target_key = "key-" .. n target_val = "somewhere" router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("GET", "/", nil, "http", nil, nil, nil, nil, nil, { [target_key] = target_val }) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) end end) describe("multiple routes of same category with identical values", function() local router local target_uri local target_domain local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n - 1 do -- all our routes here use domain.org as the domain -- they all are [host + uri] category benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6" .. string.format("%06d", i), hosts = { "domain.org" }, paths = { "/my-route-" .. n }, }, } end -- this one too, but our target will be a -- different URI benchmark_use_cases[n] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6ffffff", hosts = { "domain.org" }, paths = { "/my-real-route" }, }, } target_uri = "/my-real-route" target_domain = "domain.org" router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("GET", target_uri, target_domain) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) describe("[method + uri + host + headers]", function() local router local target_uri local target_domain local target_location local benchmark_use_cases = {} lazy_setup(function() local n = 10^5 for i = 1, n - 1 do -- insert a lot of routes that don't match (missing methods) -- but have conflicting paths and hosts (domain-.org) benchmark_use_cases[i] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6" .. string.format("%06d", i), hosts = { "domain-" .. n .. ".org" }, paths = { "/my-route-" .. n }, headers = { location = { "somewhere-" .. n }, }, }, } end -- insert our target route, which has the proper method as well benchmark_use_cases[n] = { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6ffffff", hosts = { "domain-" .. n .. ".org" }, headers = { location = { "somewhere-" .. n }, }, methods = { "POST" }, paths = { "/my-route-" .. n }, }, } target_uri = "/my-route-" .. n target_domain = "domain-" .. n .. ".org" target_location = "somewhere-" .. n router = assert(new_router(benchmark_use_cases)) end) lazy_teardown(function() -- this avoids memory leakage router = nil benchmark_use_cases = nil end) it("takes < 1ms", function() local match_t = router:select("POST", target_uri, target_domain, "http", nil, nil, nil, nil, nil, { location = target_location, }) assert.truthy(match_t) assert.same(benchmark_use_cases[#benchmark_use_cases].route, match_t.route) end) end) end) describe("[errors]", function() it("enforces args types", function() assert.error_matches(function() router:select(1) end, "method must be a string", nil, true) assert.error_matches(function() router:select("GET", 1) end, "uri must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", 1) end, "host must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", "", 1) end, "scheme must be a string", nil, true) if flavor == "traditional" then assert.error_matches(function() router:select("GET", "/", "", "http", 1) end, "src_ip must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", "", "http", nil, "") end, "src_port must be a number", nil, true) assert.error_matches(function() router:select("GET", "/", "", "http", nil, nil, 1) end, "dst_ip must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", "", "http", nil, nil, nil, "") end, "dst_port must be a number", nil, true) end assert.error_matches(function() router:select("GET", "/", "", "http", nil, nil, nil, nil, 1) end, "sni must be a string", nil, true) assert.error_matches(function() router:select("GET", "/", "", "http", nil, nil, nil, nil, nil, 1) end, "headers must be a table", nil, true) end) end) end) describe("exec()", function() it("returns parsed upstream_url + upstream_uri", function() local use_case_routes = { { service = { name = "service-invalid", host = "example.org", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/my-route" }, }, }, { service = { name = "service-invalid", host = "example.org", protocol = "https" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/my-route-2" }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) local get_headers = spy.on(_ngx.req, "get_headers") router._set_ngx(_ngx) local match_t = router:exec() assert.spy(get_headers).was_not_called() assert.same(use_case_routes[1].route, match_t.route) -- upstream_url_t if flavor == "traditional" then assert.equal("http", match_t.upstream_url_t.scheme) end assert.equal("example.org", match_t.upstream_url_t.host) assert.equal(80, match_t.upstream_url_t.port) -- upstream_uri assert.is_nil(match_t.upstream_host) -- only when `preserve_host = true` assert.equal("/my-route", match_t.upstream_uri) _ngx = mock_ngx("GET", "/my-route-2", { host = "domain.org" }) get_headers = spy.on(_ngx.req, "get_headers") router._set_ngx(_ngx) match_t = router:exec() assert.spy(get_headers).was_not_called() assert.same(use_case_routes[2].route, match_t.route) -- upstream_url_t if flavor == "traditional" then assert.equal("https", match_t.upstream_url_t.scheme) end assert.equal("example.org", match_t.upstream_url_t.host) assert.equal(443, match_t.upstream_url_t.port) -- upstream_uri assert.is_nil(match_t.upstream_host) -- only when `preserve_host = true` assert.equal("/my-route-2", match_t.upstream_uri) end) it("returns matched_host + matched_uri + matched_method + matched_headers", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { "host.com" }, methods = { "GET" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "host.com" }, paths = { "/my-route" }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "*.host.com" }, headers = { location = { "my-location-1", "my-location-2" }, }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", paths = { [[~/users/\d+/profile]] }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/my-route", { host = "host.com" }) local get_headers = spy.on(_ngx.req, "get_headers") router._set_ngx(_ngx) local match_t = router:exec() assert.spy(get_headers).was_called(1) assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.equal("host.com", match_t.matches.host) assert.equal("/my-route", match_t.matches.uri) assert.equal("GET", match_t.matches.method) end assert.is_nil(match_t.matches.headers) _ngx = mock_ngx("GET", "/my-route/prefix/match", { host = "host.com" }) get_headers = spy.on(_ngx.req, "get_headers") router._set_ngx(_ngx) match_t = router:exec() assert.spy(get_headers).was_called(1) assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.equal("host.com", match_t.matches.host) assert.equal("/my-route", match_t.matches.uri) assert.equal("GET", match_t.matches.method) end assert.is_nil(match_t.matches.headers) _ngx = mock_ngx("POST", "/my-route", { host = "host.com" }) get_headers = spy.on(_ngx.req, "get_headers") router._set_ngx(_ngx) match_t = router:exec() assert.spy(get_headers).was_called(1) assert.same(use_case_routes[2].route, match_t.route) if flavor == "traditional" then assert.equal("host.com", match_t.matches.host) assert.equal("/my-route", match_t.matches.uri) end assert.is_nil(match_t.matches.method) assert.is_nil(match_t.matches.headers) _ngx = mock_ngx("GET", "/", { host = "test.host.com", location = "my-location-1" }) get_headers = spy.on(_ngx.req, "get_headers") router._set_ngx(_ngx) match_t = router:exec() assert.spy(get_headers).was_called(1) assert.same(use_case_routes[3].route, match_t.route) if flavor == "traditional" then assert.equal("*.host.com", match_t.matches.host) assert.same({ location = "my-location-1" }, match_t.matches.headers) end assert.is_nil(match_t.matches.uri) assert.is_nil(match_t.matches.method) _ngx = mock_ngx("GET", "/users/123/profile", { host = "domain.org" }) get_headers = spy.on(_ngx.req, "get_headers") router._set_ngx(_ngx) match_t = router:exec() assert.spy(get_headers).was_called(1) assert.same(use_case_routes[4].route, match_t.route) assert.is_nil(match_t.matches.host) if flavor == "traditional" then assert.equal([[/users/\d+/profile]], match_t.matches.uri) end assert.is_nil(match_t.matches.method) assert.is_nil(match_t.matches.headers) end) it("uri_captures works well with the optional capture group. Fix #13014", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/(users/)?1984/(?profile)$]] }, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/1984/profile", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.falsy(match_t.matches.uri_captures[1]) assert.equal("profile", match_t.matches.uri_captures.subpath) assert.is_nil(match_t.matches.uri_captures.scope) end) it("returns uri_captures from a [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/(?P\d+)/profile/?(?P[a-z]*)]] }, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/1984/profile", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("1984", match_t.matches.uri_captures[1]) assert.equal("1984", match_t.matches.uri_captures.user_id) assert.equal("", match_t.matches.uri_captures[2]) assert.equal("", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/1984/profile", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) -- again, this time from the LRU cache router._set_ngx(_ngx) match_t = router:exec() assert.equal("1984", match_t.matches.uri_captures[1]) assert.equal("1984", match_t.matches.uri_captures.user_id) assert.equal("", match_t.matches.uri_captures[2]) assert.equal("", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/1984/profile", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) _ngx = mock_ngx("GET", "/users/1984/profile/email", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.equal("1984", match_t.matches.uri_captures[1]) assert.equal("1984", match_t.matches.uri_captures.user_id) assert.equal("email", match_t.matches.uri_captures[2]) assert.equal("email", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/1984/profile/email", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) end) it("returns uri_captures normalized, fix #7913", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/(?P[a-zA-Z\s\d%]+)/profile/?(?P[a-z]*)]] }, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/%6aohn%20doe/profile", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("john doe", match_t.matches.uri_captures[1]) assert.equal("john doe", match_t.matches.uri_captures.fullname) assert.equal("", match_t.matches.uri_captures[2]) assert.equal("", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/john doe/profile", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) -- again, this time from the LRU cache local match_t = router:exec() assert.equal("john doe", match_t.matches.uri_captures[1]) assert.equal("john doe", match_t.matches.uri_captures.fullname) assert.equal("", match_t.matches.uri_captures[2]) assert.equal("", match_t.matches.uri_captures.scope) -- returns the full match as well assert.equal("/users/john doe/profile", match_t.matches.uri_captures[0]) -- no stripped_uri capture assert.is_nil(match_t.matches.uri_captures.stripped_uri) assert.equal(2, #match_t.matches.uri_captures) end) it("returns no uri_captures from a [uri prefix] match", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/hello" }, strip_path = true, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("/world", match_t.upstream_uri) assert.is_nil(match_t.matches.uri_captures) end) it("returns no uri_captures from a [uri regex] match without groups", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/\d+/profile]] }, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/1984/profile", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.is_nil(match_t.matches.uri_captures) end) it("parses path component from upstream_url property", function() local use_case_routes = { { service = { name = "service-invalid", host = "example.org", path = "/get", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/my-route" }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.equal("/get", match_t.upstream_url_t.path) end end) it("parses upstream_url port", function() local use_case_routes = { { service = { name = "service-invalid", host = "example.org", port = 8080, protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/my-route" }, }, }, { service = { name = "service-invalid", host = "example.org", port = 8443, protocol = "https" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/my-route-2" }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal(8080, match_t.upstream_url_t.port) _ngx = mock_ngx("GET", "/my-route-2", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.equal(8443, match_t.upstream_url_t.port) end) it("allows url encoded paths if they are reserved characters", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/endel%2Fst" }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/endel%2Fst", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/endel%2Fst", match_t.upstream_uri) end) describe("stripped paths #strip", function() local router local use_case_routes = { { service = service, route = { id = uuid(), paths = { "/my-route", "/xx-route" }, -- need to have same length for get_priority to work strip_path = true } }, -- don't strip this route's matching URI { service = service, route = { id = uuid(), methods = { "POST" }, paths = { "/my-route", "/xx-route" }, -- need to have same length for get_priority to work strip_path = false, }, }, } lazy_setup(function() -- deep copy use_case_routes in case it changes router = assert(new_router(deep_copy(use_case_routes))) end) it("strips the specified paths from the given uri if matching", function() local _ngx = mock_ngx("GET", "/my-route/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/hello/world", match_t.upstream_uri) end) it("strips if matched URI is plain (not a prefix)", function() local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) end) it("doesn't strip if 'strip_uri' is not enabled", function() local _ngx = mock_ngx("POST", "/my-route/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.is_nil(match_t.prefix) assert.equal("/my-route/hello/world", match_t.upstream_uri) end) it("normalized client URI before matching and proxying", function() local _ngx = mock_ngx("POST", "/my-route/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.is_nil(match_t.prefix) assert.equal("/my-route/hello/world", match_t.upstream_uri) end) it("does not strips root / URI", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/" }, strip_path = true, }, }, } -- deep copy use_case_routes in case it changes local router = assert(new_router(deep_copy(use_case_routes))) local _ngx = mock_ngx("POST", "/my-route/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/", match_t.prefix) assert.equal("/my-route/hello/world", match_t.upstream_uri) end) it("can find an route with stripped URI several times in a row", function() local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) end) it("can proxy an route with stripped URI with different URIs in a row", function() local _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) _ngx = mock_ngx("GET", "/xx-route", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/xx-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) _ngx = mock_ngx("GET", "/my-route", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/my-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) _ngx = mock_ngx("GET", "/xx-route", { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/xx-route", match_t.prefix) assert.equal("/", match_t.upstream_uri) end) it("strips url encoded paths", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/endel%2Fst" }, strip_path = true, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/endel%2Fst", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("/endel%2Fst", match_t.prefix) assert.equal("/", match_t.upstream_uri) end) it("strips a [uri regex]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/\d+/profile]] }, strip_path = true, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/123/profile/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("/users/123/profile", match_t.prefix) assert.equal("/hello/world", match_t.upstream_uri) end) it("strips a [uri regex] with a capture group", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[~/users/(\d+)/profile]] }, strip_path = true, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx("GET", "/users/123/profile/hello/world", { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.equal("/users/123/profile", match_t.prefix) assert.equal("/hello/world", match_t.upstream_uri) end) end) describe("preserve Host header", function() local router local use_case_routes = { -- use the request's Host header { service = { name = "service-invalid", host = "example.org", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", preserve_host = true, hosts = { "preserve.com" }, }, }, -- use the route's upstream_url's Host { service = { name = "service-invalid", host = "example.org", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", preserve_host = false, hosts = { "discard.com" }, }, }, } lazy_setup(function() router = assert(new_router(use_case_routes)) end) describe("when preserve_host is true", function() local host = "preserve.com" it("uses the request's Host header", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal(host, match_t.upstream_host) end) it("uses the request's Host header incl. port", function() local _ngx = mock_ngx("GET", "/", { host = host .. ":123" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal(host .. ":123", match_t.upstream_host) end) it("does not change the target upstream", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("example.org", match_t.upstream_url_t.host) end) it("uses the request's Host header when `grab_header` is disabled", function() local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", name = "route-1", preserve_host = true, paths = { "/foo" }, }, upstream_url = "http://example.org", }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/foo", { host = "preserve.com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("preserve.com", match_t.upstream_host) end) it("uses the request's Host header if an route with no host was cached", function() -- This is a regression test for: -- https://github.com/Kong/kong/issues/2825 -- Ensure cached routes (in the LRU cache) still get proxied with the -- correct Host header when preserve_host = true and no registered -- route has a `hosts` property. local use_case_routes = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", name = "no-host", paths = { "/nohost" }, preserve_host = true, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/nohost", { host = "domain1.com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("domain1.com", match_t.upstream_host) _ngx = mock_ngx("GET", "/nohost", { host = "domain2.com" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("domain2.com", match_t.upstream_host) end) end) describe("when preserve_host is false", function() local host = "discard.com" it("does not change the target upstream", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.equal("example.org", match_t.upstream_url_t.host) end) it("does not set the host_header", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.is_nil(match_t.upstream_host) end) end) end) describe("preserve Host header #grpc", function() local router local use_case_routes = { -- use the request's Host header { service = { name = "service-invalid", host = "example.org", protocol = "grpc" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", preserve_host = true, hosts = { "preserve.com" }, }, }, -- use the route's upstream_url's Host { service = { name = "service-invalid", host = "example.org", protocol = "grpc" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", preserve_host = false, hosts = { "discard.com" }, }, }, } lazy_setup(function() router = assert(new_router(use_case_routes)) end) describe("when preserve_host is true", function() local host = "preserve.com" it("uses the request's Host header", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal(host, match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) it("uses the request's Host header incl. port", function() local _ngx = mock_ngx("GET", "/", { host = host .. ":123" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal(host .. ":123", match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) it("does not change the target upstream", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("example.org", match_t.upstream_url_t.host) assert.equal("grpc", match_t.service.protocol) end) it("uses the request's Host header when `grab_header` is disabled", function() local use_case_routes = { { service = { name = "service-invalid", protocol = "grpc", }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", name = "route-1", preserve_host = true, paths = { "/foo" }, }, upstream_url = "http://example.org", }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/foo", { host = "preserve.com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("preserve.com", match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) it("uses the request's Host header if an route with no host was cached", function() -- This is a regression test for: -- https://github.com/Kong/kong/issues/2825 -- Ensure cached routes (in the LRU cache) still get proxied with the -- correct Host header when preserve_host = true and no registered -- route has a `hosts` property. local use_case_routes = { { service = { name = "service-invalid", protocol = "grpc", }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", name = "no-host", paths = { "/nohost" }, preserve_host = true, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "/nohost", { host = "domain1.com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("domain1.com", match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) _ngx = mock_ngx("GET", "/nohost", { host = "domain2.com" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) assert.equal("domain2.com", match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) end) describe("when preserve_host is false", function() local host = "discard.com" it("does not change the target upstream", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.equal("example.org", match_t.upstream_url_t.host) assert.equal("grpc", match_t.service.protocol) end) it("does not set the host_header", function() local _ngx = mock_ngx("GET", "/", { host = host }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) assert.is_nil(match_t.upstream_host) assert.equal("grpc", match_t.service.protocol) end) end) end) describe("#slash handling", function() for i, line in ipairs(path_handling_tests) do for j, test in ipairs(line:expand()) do if flavor == "traditional" or test.path_handling == "v0" then local strip = test.strip_path and "on" or "off" local route_uri_or_host if test.route_path then route_uri_or_host = "uri " .. test.route_path else route_uri_or_host = "host localbin-" .. i .. "-" .. j .. ".com" end local description = string.format("(%d-%d) plain, %s with %s, strip = %s, %s. req: %s", i, j, test.service_path, route_uri_or_host, strip, test.path_handling, test.request_path) it(description, function() local use_case_routes = { { service = { protocol = "http", name = "service-invalid", path = test.service_path, }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6" .. string.format("%03d%03d", i, j), strip_path = test.strip_path, path_handling = test.path_handling, -- only add the header is no path is provided hosts = test.service_path == nil and nil or { "localbin-" .. i .. "-" .. j .. ".com" }, paths = { test.route_path }, }, } } local router = assert(new_router(use_case_routes) ) local _ngx = mock_ngx("GET", test.request_path, { host = "localbin-" .. i .. "-" .. j .. ".com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.same(test.service_path, match_t.upstream_url_t.path) end assert.same(test.expected_path, match_t.upstream_uri) end) end end end -- this is identical to the tests above, except that for the path we match -- with an injected regex sequence, effectively transforming the path -- match into a regex match for i, line in ipairs(path_handling_tests) do if line.route_path then -- skip test cases which match on host for j, test in ipairs(line:expand()) do if flavor == "traditional" or test.path_handling == "v0" then local strip = test.strip_path and "on" or "off" local regex = "~/[0]?" .. test.route_path:sub(2, -1) local description = string.format("(%d-%d) regex, %s with %s, strip = %s, %s. req: %s", i, j, test.service_path, regex, strip, test.path_handling, test.request_path) it(description, function() local use_case_routes = { { service = { protocol = "http", name = "service-invalid", path = test.service_path, }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6" .. string.format("%03d%03d", i, j), strip_path = test.strip_path, -- only add the header is no path is provided path_handling = test.path_handling, hosts = { "localbin-" .. i .. ".com" }, paths = { regex }, }, } } local router = assert(new_router(use_case_routes) ) local _ngx = mock_ngx("GET", test.request_path, { host = "localbin-" .. i .. ".com" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) if flavor == "traditional" then assert.same(test.service_path, match_t.upstream_url_t.path) end assert.same(test.expected_path, match_t.upstream_uri) end) end end end end end) it("works with special characters('\"','\\')", function() local use_case_routes = { { service = { name = "service-invalid", host = "example.org", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[/\d]] }, }, }, { service = { name = "service-invalid", host = "example.org", protocol = "https" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { [[~/\d+"]] }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", [[/\d]], { host = "domain.org" }) router._set_ngx(_ngx) local match_t = router:exec() assert.same(use_case_routes[1].route, match_t.route) -- upstream_url_t if flavor == "traditional" then assert.equal("http", match_t.upstream_url_t.scheme) end assert.equal("example.org", match_t.upstream_url_t.host) assert.equal(80, match_t.upstream_url_t.port) -- upstream_uri assert.is_nil(match_t.upstream_host) -- only when `preserve_host = true` assert.equal([[/\d]], match_t.upstream_uri) _ngx = mock_ngx("GET", [[/123"]], { host = "domain.org" }) router._set_ngx(_ngx) match_t = router:exec() assert.same(use_case_routes[2].route, match_t.route) -- upstream_url_t if flavor == "traditional" then assert.equal("https", match_t.upstream_url_t.scheme) end assert.equal("example.org", match_t.upstream_url_t.host) assert.equal(443, match_t.upstream_url_t.port) -- upstream_uri assert.is_nil(match_t.upstream_host) -- only when `preserve_host = true` assert.equal([[/123"]], match_t.upstream_uri) end) if flavor == "traditional_compatible" or flavor == "expressions" then it("gracefully handles invalid utf-8 sequences", function() local use_case_routes = { { service = { name = "service-invalid", host = "example.org", protocol = "http" }, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { [[/hello]] }, }, }, } local router = assert(new_router(use_case_routes)) local _ngx = mock_ngx("GET", "\xfc\x80\x80\x80\x80\xaf", { host = "example.org" }) local log_spy = spy.on(_ngx, "log") router._set_ngx(_ngx) local match_t = router:exec() assert.is_nil(match_t) assert.spy(log_spy).was.called_with(ngx.ERR, "router returned an error: ", "invalid utf-8 sequence of 1 bytes from index 0", ", 404 Not Found will be returned for the current request") end) end end) -- flavor == "traditional"/"traditional_compatible"/"expressions" describe("#stream context", function() -- enable stream subsystem reload_router(flavor, "stream") describe("[sources]", function() local use_case, router lazy_setup(function() use_case = { -- plain { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", protocols = { "tcp", }, sources = { { ip = "127.0.0.1" }, { ip = "127.0.0.2" }, } } }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", protocols = { "tcp", }, sources = { { port = 65001 }, { port = 65002 }, } } }, -- range { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", protocols = { "tcp", }, sources = { { ip = "127.168.0.0/8" }, } } }, -- ip + port { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", protocols = { "tcp", }, sources = { { ip = "127.0.0.1", port = 65001 }, } } }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8105", protocols = { "tcp", }, sources = { { ip = "127.0.0.2", port = 65300 }, { ip = "127.168.0.0/16", port = 65301 }, } } }, } router = assert(new_router(use_case)) -- let atc-router happy local _ngx = mock_ngx("GET", "/", { a = "1" }) router._set_ngx(_ngx) end) it("[src_ip]", function() local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("[src_port]", function() local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.3", 65001) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("[src_ip] range match", function() local match_t = router:select(nil, nil, nil, "tcp", "127.168.0.1") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) it("[src_ip] + [src_port]", function() local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1", 65001) assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) it("[src_ip] range match + [src_port]", function() local match_t = router:select(nil, nil, nil, "tcp", "127.168.10.1", 65301) assert.truthy(match_t) assert.same(use_case[5].route, match_t.route) end) it("[src_ip] no match", function() local match_t = router:select(nil, nil, nil, "tcp", "10.0.0.1") assert.falsy(match_t) match_t = router:select(nil, nil, nil, "tcp", "10.0.0.2", 65301) assert.falsy(match_t) end) end) describe("[destinations]", function() local use_case, router lazy_setup(function() use_case = { -- plain { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", protocols = { "tcp", }, destinations = { { ip = "127.0.0.1" }, { ip = "127.0.0.2" }, } } }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", protocols = { "tcp", }, destinations = { { port = 65001 }, { port = 65002 }, } } }, -- range { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", protocols = { "tcp", }, destinations = { { ip = "127.168.0.0/8" }, } } }, -- ip + port { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", protocols = { "tcp", }, destinations = { { ip = "127.0.0.1", port = 65001 }, } } }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8105", protocols = { "tcp", }, destinations = { { ip = "127.0.0.2", port = 65300 }, { ip = "127.168.0.0/16", port = 65301 }, } } }, } router = assert(new_router(use_case)) end) it("[dst_ip]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.0.0.1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.0.0.1") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("[dst_port]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.0.0.3", 65001) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("[dst_ip] range match", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.168.0.1") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) it("[dst_ip] + [dst_port]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.0.0.1", 65001) assert.truthy(match_t) assert.same(use_case[4].route, match_t.route) end) it("[dst_ip] range match + [dst_port]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "127.168.10.1", 65301) assert.truthy(match_t) assert.same(use_case[5].route, match_t.route) end) it("[dst_ip] no match", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, "10.0.0.1") assert.falsy(match_t) match_t = router:select(nil, nil, nil, "tcp", nil, nil, "10.0.0.2", 65301) assert.falsy(match_t) end) end) describe("[snis]", function() local use_case, use_case_ignore_sni, router, router_ignore_sni lazy_setup(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", snis = { "www.example.org" } } }, -- see #6425 { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", hosts = { "sni.example.com", }, protocols = { "http", "https", }, snis = { "sni.example.com", }, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", hosts = { "sni.example.com", }, protocols = { "http", }, }, }, } use_case_ignore_sni = { -- see #6425 { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8104", hosts = { "sni.example.com", }, protocols = { "http", "https", }, snis = { "sni.example.com", }, }, }, } router = assert(new_router(use_case)) router_ignore_sni = assert(new_router(use_case_ignore_sni)) end) it_trad_only("[sni]", function() local match_t = router:select(nil, nil, nil, "tcp", nil, nil, nil, nil, "www.example.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it_trad_only("[sni] is ignored for http request without shadowing routes with `protocols={'http'}`. Fixes #6425", function() local match_t = router_ignore_sni:select(nil, nil, "sni.example.com", "http", nil, nil, nil, nil, nil) assert.truthy(match_t) assert.same(use_case_ignore_sni[1].route, match_t.route) match_t = router:select(nil, nil, "sni.example.com", "http", nil, nil, nil, nil, nil) assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) end) it("[sni] has higher priority than [src] or [dst]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", protocols = { "tls", }, snis = { "www.example.org" }, } }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", protocols = { "tls", }, sources = { { ip = "127.0.0.1" }, } } }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", protocols = { "tls", }, destinations = { { ip = "172.168.0.1" }, } } }, } local router = assert(new_router(use_case)) local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1", nil, nil, nil, "www.example.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) match_t = router:select(nil, nil, nil, "tcp", nil, nil, "172.168.0.1", nil, "www.example.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("[src] + [dst] has higher priority than [sni]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", protocols = { "tls", }, snis = { "www.example.org" }, } }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", protocols = { "tls", }, sources = { { ip = "127.0.0.1" }, }, destinations = { { ip = "172.168.0.1" }, } } }, } local router = assert(new_router(use_case)) local match_t = router:select(nil, nil, nil, "tcp", "127.0.0.1", nil, "172.168.0.1", nil, "www.example.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) end) -- flavor == "traditional"/"traditional_compatible"/"expressions" -- only traditional_compatible should check 'empty' fields if flavor == "traditional_compatible" then describe("#stream context", function() -- enable stream subsystem reload_router(flavor, "stream") describe("check empty route fields", function() local use_case before_each(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", protocols = { "tls" }, snis = { "www.example.org" }, sources = { { ip = "127.0.0.1" }, } }, }, } end) local empty_values = { {}, ngx.null, nil } for i = 1, 3 do local v = empty_values[i] it("empty snis", function() use_case[1].route.snis = v assert.equal(get_expression(use_case[1].route), [[(net.src.ip == 127.0.0.1)]]) assert(new_router(use_case)) end) it("empty snis and src cidr", function() use_case[1].route.snis = v use_case[1].route.sources = {{ ip = "127.168.0.1/8" },} assert.equal(get_expression(use_case[1].route), [[(net.src.ip in 127.0.0.0/8)]]) assert(new_router(use_case)) end) it("empty snis and dst cidr", function() use_case[1].route.snis = v use_case[1].route.destinations = {{ ip = "192.168.0.1/16" },} assert.equal(get_expression(use_case[1].route), [[(net.src.ip == 127.0.0.1) && (net.dst.ip in 192.168.0.0/16)]]) assert(new_router(use_case)) end) it("empty sources", function() use_case[1].route.sources = v use_case[1].route.destinations = {{ ip = "192.168.0.1/16" },} assert.equal(get_expression(use_case[1].route), [[(net.protocol != r#"tls"# || (tls.sni == r#"www.example.org"#)) && (net.dst.ip in 192.168.0.0/16)]]) assert(new_router(use_case)) end) it("empty destinations", function() use_case[1].route.destinations = v assert.equal(get_expression(use_case[1].route), [[(net.protocol != r#"tls"# || (tls.sni == r#"www.example.org"#)) && (net.src.ip == 127.0.0.1)]]) assert(new_router(use_case)) end) end end) end) -- #stream context end -- if flavor == "traditional_compatible" end) -- describe("Router (flavor = end -- for _, flavor for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do describe("[both regex and prefix with regex_priority]", function() reload_router(flavor) local use_case, router lazy_setup(function() use_case = { -- regex { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/.*" }, hosts = { "domain-1.org", }, }, }, -- prefix { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/" }, hosts = { "domain-2.org", }, regex_priority = 5 }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", paths = { "/v1" }, hosts = { "domain-2.org", }, }, }, } router = assert(new_router(use_case)) end) it("[prefix matching ignore regex_priority]", function() local match_t = router:select("GET", "/v1", "domain-2.org") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) end) end -- for flavor for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do describe("Router (flavor = " .. flavor .. ")", function() reload_router(flavor) describe("[both regex and prefix]", function() local use_case, router lazy_setup(function() use_case = { -- regex + prefix { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "~/some/thing/else", "/foo", }, hosts = { "domain-1.org", }, }, }, -- prefix { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/foo/bar" }, hosts = { "domain-1.org", }, }, }, } -- deep copy use_case in case it changes router = assert(new_router(deep_copy(use_case))) end) it("[assigns different priorities to regex and non-regex path]", function() local match_t = router:select("GET", "/some/thing/else", "domain-1.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/foo/bar", "domain-1.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) end) describe("[overlapping prefixes]", function() local use_case, router lazy_setup(function() use_case = { -- regex + prefix { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo", "/foo/bar/baz" }, hosts = { "domain-1.org", }, }, }, -- prefix { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", paths = { "/foo/bar" }, hosts = { "domain-1.org", }, }, }, } -- deep copy use_case in case it changes router = assert(new_router(deep_copy(use_case))) end) it("[assigns different priorities to each path]", function() local match_t = router:select("GET", "/foo", "domain-1.org") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/foo/bar", "domain-1.org") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) end) it("[can create route with multiple paths and no service]", function() local use_case = { -- regex + prefix { route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo", "/foo/bar/baz" }, }, }} assert(new_router(use_case)) end) end) end for _, flavor in ipairs({ "traditional_compatible", "expressions" }) do describe("Router (flavor = " .. flavor .. ")", function() reload_router(flavor) it("[cache hit should be case sensitive]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo", }, headers = { test1 = { "Quote" }, }, }, }, } local router = assert(new_router(use_case)) local ctx = {} local _ngx = mock_ngx("GET", "/foo", { test1 = "QUOTE", }) router._set_ngx(_ngx) -- first match, case insensitive local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.falsy(ctx.route_match_cached) -- cache hit local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(ctx.route_match_cached, "pos") local ctx = {} local _ngx = mock_ngx("GET", "/foo", { test1 = "QuoTe", }) router._set_ngx(_ngx) -- case insensitive match local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) -- cache miss, case sensitive assert.falsy(ctx.route_match_cached) end) it("[cache hit should have correct match_t.upstream_uri]", function() local host = "example.com" local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", hosts = { host }, preserve_host = true, }, }, } local router = assert(new_router(use_case)) local ctx = {} local _ngx = mock_ngx("GET", "/foo", { host = host, }) router._set_ngx(_ngx) -- first match local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.falsy(ctx.route_match_cached) assert.equal(host, match_t.upstream_host) assert.same("/foo", match_t.upstream_uri) local ctx = {} local _ngx = mock_ngx("GET", "/bar", { host = host, }) router._set_ngx(_ngx) -- cache hit local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(ctx.route_match_cached, "pos") assert.equal(host, match_t.upstream_host) assert.same("/bar", match_t.upstream_uri) end) end) describe("Router (flavor = " .. flavor .. ")", function() reload_router(flavor, "stream") it("[#stream SNI-based routing does work using tls_passthrough]", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", protocols = { "tls_passthrough", }, snis = { "www.example.com" }, preserve_host = true, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", protocols = { "tls_passthrough", }, snis = { "www.example.org" }, preserve_host = true, }, }, } local router = assert(new_router(use_case)) local _ngx = mock_ngx(nil, nil, nil, nil, { ssl_preread_server_name = "www.example.com", }) router._set_ngx(_ngx) local match_t = router:exec() assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local _ngx = mock_ngx(nil, nil, nil, nil, { ssl_preread_server_name = "www.example.org", }) router._set_ngx(_ngx) local match_t = router:exec() assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) end) end -- for flavor do local flavor = "expressions" describe("Router (flavor = " .. flavor .. ") [stream]", function() reload_router(flavor, "stream") local use_case, router local service = { name = "service-invalid", protocol = "tcp", } lazy_setup(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", protocols = { "tls" }, expression = [[tls.sni == "www.example.com"]], priority = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", protocols = { "tls_passthrough" }, expression = [[tls.sni == "www.example.org"]], priority = 100, }, }, } router = assert(new_router(use_case)) end) it("exec() should match tls with tls.sni", function() local _ngx = mock_ngx(nil, nil, nil, nil, { remote_port = 1000, server_port = 1000, ssl_preread_server_name = "www.example.com", }) router._set_ngx(_ngx) local match_t = router:exec() assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("exec() should match tls_passthrough with tls.sni", function() local _ngx = mock_ngx(nil, nil, nil, nil, { remote_port = 1000, server_port = 1000, ssl_preread_server_name = "www.example.org", }) router._set_ngx(_ngx) local match_t = router:exec() assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) end) describe("Router (flavor = " .. flavor .. ") [http]", function() reload_router(flavor) local use_case, router lazy_setup(function() use_case = { -- query has one value { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.path == "/foo/bar" && http.queries.a == "1"]], priority = 100, }, }, -- query has no value or is empty string { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", expression = [[http.path == "/foo/bar" && http.queries.a == ""]], priority = 100, }, }, -- query has multiple values { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", expression = [[http.path == "/foo/bar" && any(http.queries.a) == "2"]], priority = 100, }, }, } router = assert(new_router(use_case)) end) it("select() should match http.queries", function() local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, nil, {a = "1",}) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, nil, {a = ""}) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, nil, {a = true}) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, nil, {a = {"2", "10"}}) assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, nil, {a = "x"}) assert.falsy(match_t) end) it("exec() should match http.queries", function() local _ngx = mock_ngx("GET", "/foo/bar", { host = "domain.org"}, { a = "1"}) local get_uri_args = spy.on(_ngx.req, "get_uri_args") router._set_ngx(_ngx) local match_t = router:exec() assert.spy(get_uri_args).was_called(1) assert.same(use_case[1].route, match_t.route) local _ngx = mock_ngx("GET", "/foo/bar", { host = "domain.org"}, { a = ""}) local get_uri_args = spy.on(_ngx.req, "get_uri_args") router._set_ngx(_ngx) local match_t = router:exec() assert.spy(get_uri_args).was_called(1) assert.same(use_case[2].route, match_t.route) local _ngx = mock_ngx("GET", "/foo/bar", { host = "domain.org"}, { a = true}) local get_uri_args = spy.on(_ngx.req, "get_uri_args") router._set_ngx(_ngx) local match_t = router:exec() assert.spy(get_uri_args).was_called(1) assert.same(use_case[2].route, match_t.route) local _ngx = mock_ngx("GET", "/foo/bar", { host = "domain.org"}, { a = {"1", "2"}}) local get_uri_args = spy.on(_ngx.req, "get_uri_args") router._set_ngx(_ngx) local match_t = router:exec() assert.spy(get_uri_args).was_called(1) assert.same(use_case[3].route, match_t.route) local _ngx = mock_ngx("GET", "/foo/bar", { host = "domain.org"}, { a = "x"}) local get_uri_args = spy.on(_ngx.req, "get_uri_args") router._set_ngx(_ngx) local match_t = router:exec() assert.spy(get_uri_args).was_called(1) assert.falsy(match_t) end) end) describe("Router (flavor = " .. flavor .. ") [http]", function() reload_router(flavor) local use_case, router lazy_setup(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.path == "/foo/bar" && http.headers.test1 == "Quote"]], priority = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", expression = [[http.path == "/foo/bar" && lower(http.headers.test2) == "quote"]], priority = 100, }, }, } end) it("select() should match with case sensitivity", function() router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, {test1 = "quote"}) assert.falsy(match_t) local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, {test1 = "quoTe"}) assert.falsy(match_t) local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, {test1 = "Quote"}) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("select() should match with lower() (case insensitive)", function() router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, {test2 = "QuoTe"}) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) local match_t = router:select("GET", "/foo/bar", nil, nil, nil, nil, nil, nil, nil, {test2 = "QUOTE"}) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("exec() should hit cache with case sensitive", function() router = assert(new_router(use_case)) local ctx = {} local _ngx = mock_ngx("GET", "/foo/bar", { test1 = "Quote", }) router._set_ngx(_ngx) -- first match local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.falsy(ctx.route_match_cached) -- cache hit pos local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(ctx.route_match_cached, "pos") local ctx = {} local _ngx = mock_ngx("GET", "/foo/bar", { test1 = "QUOTE", }) router._set_ngx(_ngx) -- case sensitive not match local match_t = router:exec(ctx) assert.falsy(match_t) assert.falsy(ctx.route_match_cached) -- cache hit neg local match_t = router:exec(ctx) assert.falsy(match_t) assert.same(ctx.route_match_cached, "neg") end) end) describe("Router (flavor = " .. flavor .. ") [http]", function() reload_router(flavor) local use_case, router lazy_setup(function() use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.host == "www.example.com" && net.port == 8000]], priority = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", expression = [[net.src.ip == 1.1.1.1 && net.dst.ip == 2.2.2.2 && http.method == "GET"]], priority = 100, }, }, } end) it("select() should convert 'net.port' to 'net.dst.port' and work well", function() router = assert(new_router(use_case)) -- let atc-router happy local _ngx = mock_ngx("GET", "/", { a = "1" }) router._set_ngx(_ngx) local match_t = router:select("GET", "/", "www.example.com:80") assert.falsy(match_t) local match_t = router:select("GET", "/", "www.example.com:8000") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("exec() should use var.server_port if host has no port", function() router = assert(new_router(use_case)) local ctx = {} local _ngx = mock_ngx("GET", "/foo", { host = "www.example.com" }) router._set_ngx(_ngx) -- no port provided local match_t = router:exec() assert.falsy(match_t) -- var.server_port _ngx.var.server_port = 8000 router._set_ngx(_ngx) -- first match local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.falsy(ctx.route_match_cached) -- cache hit local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(ctx.route_match_cached, "pos") end) it("exec() should support net.src.* and net.dst.*", function() router = assert(new_router(use_case)) local ctx = {} local _ngx = mock_ngx("GET", "/foo", { host = "domain.org" }) router._set_ngx(_ngx) -- no ip address provided local match_t = router:exec() assert.falsy(match_t) -- ip address _ngx.var.remote_addr = "1.1.1.1" _ngx.var.server_addr = "2.2.2.2" router._set_ngx(_ngx) -- first match local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.falsy(ctx.route_match_cached) -- cache hit local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.same(ctx.route_match_cached, "pos") end) end) describe("Router (flavor = " .. flavor .. ") [http]", function() reload_router(flavor) it("select() should match single http.segments.*", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.path.segments.0 == "foo" && http.path.segments.1 == "bar"]], priority = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", expression = [[http.path.segments.0 == "foo" && http.path.segments.2 ^= "baz"]], priority = 200, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", expression = [[http.path.segments.0 == "foo" && http.path.segments.3 ~ r#"\d+"#]], priority = 300, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo/bar") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/foo/bar/bazxxx") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) local match_t = router:select("GET", "/foo/bar/baz/12345") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) local match_t = router:select("GET", "/foo/xxx") assert.falsy(match_t) end) it("select() should match http.segments.* with len", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.path.segments.0 == "foo" && http.path.segments.len == 1]], priority = 100, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/foo/") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/foo/xxx") assert.falsy(match_t) end) it("select() should match range http.segments.*", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.path.segments.0_1 ~ r#"\d+/\w+"#]], priority = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", expression = [[http.path.segments.1_3 == r#"xxx/yyy/zzz"#]], priority = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8103", expression = [[http.path.segments.1_2 == r#"xxx/yyy"# && http.path.segments.len == 3]], priority = 100, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/123/foo/bar") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/123/hello-world/bar") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/foo/xxx/yyy/zzz/bar") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) local match_t = router:select("GET", "/foo/xxx/yyy") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) local match_t = router:select("GET", "/foo/xxx/yyy/") assert.truthy(match_t) assert.same(use_case[3].route, match_t.route) end) it("select() accepts but does not match wrong http.segments.*", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.path.segments.4_1 == r#"foo"#]], priority = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", expression = [[http.path.segments.10_11 == r#"foo/bar"#]], priority = 100, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo/bar") assert.falsy(match_t) end) it("exec() should normalize uri with http.segments.*", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.path.segments.0 == "foo" && http.path.segments.1 == "bar" && http.path.segments.2 == "baz" && ]] .. [[http.path.segments.len == 3]], priority = 100, }, }, } local router = assert(new_router(use_case)) local ctx = {} local _ngx = mock_ngx("GET", "/foo/bar/baz", { a = "1", }) router._set_ngx(_ngx) local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local ctx = {} local _ngx = mock_ngx("GET", "/foo//bar///baz//", { a = "1", }) router._set_ngx(_ngx) local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) end) it("exec() should hit cache with http.segments.*", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.path.segments.0 == "foo" && http.path.segments.1 == "bar"]], priority = 100, }, }, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", expression = [[http.path.segments.1_3 == r#"xxx/yyy/zzz"#]], priority = 100, }, }, } local router = assert(new_router(use_case)) local ctx = {} local _ngx = mock_ngx("GET", "/foo/bar", { a = "1", }) router._set_ngx(_ngx) -- first match local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.falsy(ctx.route_match_cached) -- cache hit pos local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) assert.same(ctx.route_match_cached, "pos") local ctx = {} local _ngx = mock_ngx("GET", "/foo/xxx/yyy/zzz/bar", { a = "1", }) router._set_ngx(_ngx) -- first match local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.falsy(ctx.route_match_cached) -- cache hit pos local match_t = router:exec(ctx) assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) assert.same(ctx.route_match_cached, "pos") end) end) describe("Router (flavor = " .. flavor .. ") [http]", function() reload_router(flavor) it("select() should match not expression", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[!(http.path ^= r#"/foo"#)]], priority = 100, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/123/foo/bar") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/xyz/hello-world/bar") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) local match_t = router:select("GET", "/foo/bar") assert.falsy(match_t) end) end) describe("Router (flavor = " .. flavor .. ") [http]", function() reload_router(flavor) it("rejects other fields if expression field exists", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo" }, -- rejected expression = [[http.path ^= r#"/bar"#]], -- effective }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo") assert.falsy(match_t) local match_t = router:select("GET", "/bar") assert.truthy(match_t) end) it("expression route has higher priority than traditional route", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", paths = { "/foo" }, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo/bar") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", expression = [[http.path ^= r#"/foo"#]], }, }) local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo/bar") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) it("works when route.priority is near 2^46", function() local use_case = { { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8101", expression = [[http.path ^= r#"/foo"#]], priority = 2^46 - 3, }, }, } local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo/bar") assert.truthy(match_t) assert.same(use_case[1].route, match_t.route) table.insert(use_case, { service = service, route = { id = "e8fb37f1-102d-461e-9c51-6608a6bb8102", expression = [[http.path ^= r#"/foo"#]], priority = 2^46 - 2, }, }) local router = assert(new_router(use_case)) local match_t = router:select("GET", "/foo/bar") assert.truthy(match_t) assert.same(use_case[2].route, match_t.route) end) end) end -- local flavor = "expressions"