local cjson = require "cjson" local helpers = require "spec.helpers" local redis_helper = require "spec.helpers.redis_helper" local REDIS_HOST = helpers.redis_host local REDIS_PORT = helpers.redis_port local REDIS_SSL_PORT = helpers.redis_ssl_port local REDIS_SSL_SNI = helpers.redis_ssl_sni local REDIS_PASSWORD = "" local REDIS_DATABASE = 1 local ITERATIONS = 6 local escape_uri = ngx.escape_uri local encode_args = ngx.encode_args local fmt = string.format local proxy_client = helpers.proxy_client -- wait for server timestamp reaching the ceiling of client timestamp secs -- e.g. if the client time is 1.531 secs, we want to start the test period -- at 2.000 of server time, so that we could have as close as 1 sec to -- avoid flaky caused by short period(e.g. server start at 1.998 and it soon -- exceed the time period) local function wait_server_sync(headers, api_key) ngx.update_time() local now = ngx.now() local secs = math.ceil(now) local path = api_key and "/timestamp?apikey="..api_key or "/timestamp" helpers.wait_until(function() local res = proxy_client():get(path, { headers = headers, }) assert(res.status == 200) local ts = res.headers["Server-Time"] return res.status == 200 and math.floor(tonumber(ts)) == secs end, 1, 0.1) end -- wait for the remain counter of ratelimintg reaching the expected number. -- kong server may need some time to sync the remain counter in db/redis, it's -- better to wait for the definite status then just wait for some time randonly -- 'path': the url to get remaining counter but not consume the rate -- 'expected': the expected number of remaining ratelimit counters -- 'expected_status': the expected resp status which is 200 by default local function wait_remaining_sync(path, headers, expected, expected_status, api_key) local res if api_key then path = path .. "?apikey="..api_key end helpers.wait_until(function() res = proxy_client():get(path, { headers = headers, }) -- if expected_status is not 200, just check the status, not counter. if expected_status and expected_status ~= 200 then return res.status == expected_status end -- check every expected counter specified for k, v in pairs(expected) do if tonumber(res.headers[k]) ~= v then return false end end return res.status == 200 end, 1) return res end local redis_confs = { no_ssl = { redis_port = REDIS_PORT, }, ssl_verify = { redis_ssl = true, redis_ssl_verify = true, redis_server_name = REDIS_SSL_SNI, redis_port = REDIS_SSL_PORT, }, ssl_no_verify = { redis_ssl = true, redis_ssl_verify = false, redis_server_name = "really.really.really.does.not.exist.host.test", redis_port = REDIS_SSL_PORT, }, } local function test_limit(path, uri_args, host, limit) local full_path = path .. "?" .. encode_args(uri_args) limit = limit or ITERATIONS for i = 1, limit do local res = proxy_client():get(full_path, { headers = { Host = host:format(i) }, }) assert.res_status(200, res) end -- wait for async timer to increment the limit wait_remaining_sync(path, { Host = host:format(1) }, {["x-ratelimit-remaining-video-second"] = 0}, 200, uri_args["apikey"]) local res = proxy_client():get(full_path, { headers = { Host = host:format(1) }, }) assert.res_status(429, res) assert.equal(limit, tonumber(res.headers["x-ratelimit-limit-video-second"])) assert.equal(0, tonumber(res.headers["x-ratelimit-remaining-video-second"])) end local function init_db(strategy, policy) local bp = helpers.get_db_utils(strategy, { "routes", "services", "plugins", "consumers", "keyauth_credentials", }) if policy == "redis" then redis_helper.reset_redis(REDIS_HOST, REDIS_PORT) end return bp end for _, strategy in helpers.each_strategy() do for _, policy in ipairs({"local", "cluster", "redis"}) do for redis_conf_name, redis_conf in pairs(redis_confs) do if redis_conf_name ~= "no_ssl" and policy ~= "redis" then goto continue end describe(fmt("Plugin: response-ratelimiting (access) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function() lazy_setup(function() local bp = init_db(strategy, policy) local consumer1 = bp.consumers:insert {custom_id = "provider_123"} bp.keyauth_credentials:insert { key = "apikey123", consumer = { id = consumer1.id }, } local consumer2 = bp.consumers:insert {custom_id = "provider_124"} bp.keyauth_credentials:insert { key = "apikey124", consumer = { id = consumer2.id }, } local route1 = bp.routes:insert { hosts = { "test1.test" }, protocols = { "http", "https" }, } bp.response_ratelimiting_plugins:insert({ route = { id = route1.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS } }, }, }) local route2 = bp.routes:insert { hosts = { "test2.test" }, protocols = { "http", "https" }, } bp.response_ratelimiting_plugins:insert({ route = { id = route2.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS*2, minute = ITERATIONS*4 }, image = { second = ITERATIONS } }, }, }) local route3 = bp.routes:insert { hosts = { "test3.test" }, protocols = { "http", "https" }, } bp.plugins:insert { name = "key-auth", route = { id = route3.id }, } bp.response_ratelimiting_plugins:insert({ route = { id = route3.id }, config = { policy = policy, redis = { host = REDIS_HOST, port = REDIS_PORT, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS - 3 } } }, }) bp.response_ratelimiting_plugins:insert({ route = { id = route3.id }, consumer = { id = consumer1.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS - 2 } }, }, }) local route4 = bp.routes:insert { hosts = { "test4.test" }, protocols = { "http", "https" }, } bp.response_ratelimiting_plugins:insert({ route = { id = route4.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS * 2 + 2 }, image = { second = ITERATIONS } }, } }) local route7 = bp.routes:insert { hosts = { "test7.test" }, protocols = { "http", "https" }, } bp.response_ratelimiting_plugins:insert({ route = { id = route7.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, block_on_first_violation = true, limits = { video = { second = ITERATIONS, minute = ITERATIONS * 2, }, image = { second = 4, }, }, } }) local route8 = bp.routes:insert { hosts = { "test8.test" }, protocols = { "http", "https" }, } bp.response_ratelimiting_plugins:insert({ route = { id = route8.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS, minute = ITERATIONS*2 }, image = { second = ITERATIONS-1 } }, } }) local route9 = bp.routes:insert { hosts = { "test9.test" }, protocols = { "http", "https" }, } bp.response_ratelimiting_plugins:insert({ route = { id = route9.id }, config = { fault_tolerant = false, policy = policy, hide_client_headers = true, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS } }, } }) local service10 = bp.services:insert() bp.routes:insert { hosts = { "test-service1.test" }, service = service10, } bp.routes:insert { hosts = { "test-service2.test" }, service = service10, } bp.response_ratelimiting_plugins:insert({ service = { id = service10.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS } }, } }) local grpc_service = assert(bp.services:insert { name = "grpc", url = helpers.grpcbin_url, }) assert(bp.routes:insert { protocols = { "grpc" }, paths = { "/hello.HelloService/" }, service = grpc_service, }) bp.response_ratelimiting_plugins:insert({ service = { id = grpc_service.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS } }, } }) assert(helpers.start_kong({ database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt", })) end) lazy_teardown(function() helpers.stop_kong() end) describe("Without authentication (IP address)", function() it("returns remaining counter", function() local host = "test1.test" wait_server_sync( { Host = host }) local n = math.floor(ITERATIONS / 2) for _ = 1, n do local res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = host }, }) assert.res_status(200, res) end wait_remaining_sync("/response-headers", { Host = "test1.test" }, {["x-ratelimit-remaining-video-second"] = ITERATIONS - n}) local res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = host }, }) assert.res_status(200, res) assert.equal(ITERATIONS, tonumber(res.headers["x-ratelimit-limit-video-second"])) assert.equal(ITERATIONS - n - 1, tonumber(res.headers["x-ratelimit-remaining-video-second"])) end) it("returns remaining counter #grpc", function() wait_server_sync({ Host = "test1.test" }) local ok, res = helpers.proxy_client_grpc(){ service = "hello.HelloService.SayHello", opts = { ["-v"] = true, }, } assert.truthy(ok, res) assert.matches("x%-ratelimit%-limit%-video%-second: %d+", res) assert.matches("x%-ratelimit%-remaining%-video%-second: %d+", res) -- Note: tests for this plugin rely on the ability to manipulate -- upstream response headers, which is not currently possible with -- the grpc service we use. Therefore, we are only testing that -- headers are indeed inserted. end) it("blocks if exceeding limit", function() wait_server_sync({ Host = "test1.test" }) test_limit("/response-headers", {["x-kong-limit"] = "video=1"}, "test1.test") end) it("counts against the same service register from different routes", function() wait_server_sync( { Host = "test1.test" }) local n = math.floor(ITERATIONS / 2) local url = "/response-headers?x-kong-limit=" .. escape_uri("video=1, test=" .. ITERATIONS) for i = 1, n do local res = proxy_client():get(url , { headers = { Host = "test-service1.test" }, }) assert.res_status(200, res) end for i = n+1, ITERATIONS do local res = proxy_client():get(url, { headers = { Host = "test-service2.test" }, }) assert.res_status(200, res) end wait_remaining_sync("/response-headers", { Host = "test-service1.test" }, {["x-ratelimit-remaining-video-second"] = 0}) -- Additional request, while limit is ITERATIONS/second local res = proxy_client():get(url, { headers = { Host = "test-service1.test" }, }) assert.res_status(429, res) end) it("handles multiple limits", function() wait_server_sync( { Host = "test1.test" }) local n = math.floor(ITERATIONS / 2) local res local url = "/response-headers?x-kong-limit=" .. escape_uri("video=2, image=1") local remain_in_sec = ITERATIONS * 2 local remain_in_min = ITERATIONS * 4 for i = 1, n do res = proxy_client():get(url, { headers = { Host = "test2.test" }, }) assert.res_status(200, res) remain_in_sec = remain_in_sec - 2 remain_in_min = remain_in_min - 2 end res = wait_remaining_sync("/response-headers", { Host = "test2.test" }, {["x-ratelimit-remaining-video-second"] = remain_in_sec, ["x-ratelimit-remaining-video-minute"] = remain_in_min} ) assert.equal(ITERATIONS * 2, tonumber(res.headers["x-ratelimit-limit-video-second"])) assert.equal(remain_in_sec, tonumber(res.headers["x-ratelimit-remaining-video-second"])) assert.equal(ITERATIONS * 4, tonumber(res.headers["x-ratelimit-limit-video-minute"])) assert.equal(remain_in_min, tonumber(res.headers["x-ratelimit-remaining-video-minute"])) assert.equal(ITERATIONS, tonumber(res.headers["x-ratelimit-limit-image-second"])) assert.equal(ITERATIONS - n, tonumber(res.headers["x-ratelimit-remaining-image-second"])) url = "/response-headers?x-kong-limit=" .. escape_uri("video=1, image=1") for i = n+1, ITERATIONS do res = proxy_client():get(url, { headers = { Host = "test2.test" }, }) assert.res_status(200, res) remain_in_sec = remain_in_sec - 1 remain_in_min = remain_in_min - 1 end res = wait_remaining_sync("/response-headers", { Host = "test2.test" }, {["x-ratelimit-remaining-video-second"] = remain_in_sec, ["x-ratelimit-remaining-video-minute"] = remain_in_min}) assert.equal(0, tonumber(res.headers["x-ratelimit-remaining-image-second"])) assert.equal(remain_in_min, tonumber(res.headers["x-ratelimit-remaining-video-minute"])) assert.equal(remain_in_sec, tonumber(res.headers["x-ratelimit-remaining-video-second"])) local res = proxy_client():get(url, { headers = { Host = "test2.test" }, }) remain_in_sec = remain_in_sec - 1 remain_in_min = remain_in_min - 1 assert.equal(0, tonumber(res.headers["x-ratelimit-remaining-image-second"])) assert.equal(remain_in_min, tonumber(res.headers["x-ratelimit-remaining-video-minute"])) assert.equal(remain_in_sec, tonumber(res.headers["x-ratelimit-remaining-video-second"])) assert.res_status(429, res) end) end) describe("With authentication", function() describe("API-specific plugin", function() it("blocks if exceeding limit and a per consumer & route setting", function() wait_server_sync({ Host = "test3.test" }, "apikey123") test_limit("/response-headers", {["apikey"] = "apikey123", ["x-kong-limit"] = "video=1"}, "test3.test", ITERATIONS - 2) end) it("blocks if exceeding limit and a per route setting", function() wait_server_sync({ Host = "test3.test" }, "apikey123") test_limit("/response-headers", {["apikey"] = "apikey124", ["x-kong-limit"] = "video=1"}, "test3.test", ITERATIONS - 3) end) end) end) describe("Upstream usage headers", function() it("should append the headers with multiple limits", function() wait_server_sync( { Host = "test8.test" }) local res = proxy_client():get("/get", { headers = { Host = "test8.test" }, }) local json = cjson.decode(assert.res_status(200, res)) assert.equal(ITERATIONS-1, tonumber(json.headers["x-ratelimit-remaining-image"])) assert.equal(ITERATIONS, tonumber(json.headers["x-ratelimit-remaining-video"])) -- Actually consume the limits res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=2, image=1"), { headers = { Host = "test8.test" }, }) local json2 = cjson.decode(assert.res_status(200, res)) assert.equal(ITERATIONS-1, tonumber(json2.headers["x-ratelimit-remaining-image"])) assert.equal(ITERATIONS, tonumber(json2.headers["x-ratelimit-remaining-video"])) wait_remaining_sync("/response-headers", { Host = "test8.test" }, {["x-ratelimit-remaining-video-second"] =ITERATIONS - 2}) local res = proxy_client():get("/get", { headers = { Host = "test8.test" }, }) local body = cjson.decode(assert.res_status(200, res)) assert.equal(ITERATIONS-2, tonumber(body.headers["x-ratelimit-remaining-image"])) assert.equal(ITERATIONS-2, tonumber(body.headers["x-ratelimit-remaining-video"])) end) it("combines multiple x-kong-limit headers from upstream", function() wait_server_sync( { Host = "test4.test" }) -- NOTE: this test is not working as intended because multiple response headers are merged into one comma-joined header by send_text_response function for _ = 1, ITERATIONS do local res = proxy_client():get("/response-headers?x-kong-limit=video%3D2&x-kong-limit=image%3D1", { headers = { Host = "test4.test" }, }) assert.res_status(200, res) end proxy_client():get("/response-headers?x-kong-limit=video%3D1", { headers = { Host = "test4.test" }, }) wait_remaining_sync("/response-headers", { Host = "test4.test" }, {["x-ratelimit-remaining-video-second"] = 1}) local res = proxy_client():get("/response-headers?x-kong-limit=video%3D2&x-kong-limit=image%3D1", { headers = { Host = "test4.test" }, }) assert.res_status(429, res) assert.equal(0, tonumber(res.headers["x-ratelimit-remaining-image-second"])) assert.equal(0, tonumber(res.headers["x-ratelimit-remaining-video-second"])) end) end) it("should block on first violation", function() wait_server_sync( { Host = "test7.test" }) local res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=2, image=4"), { headers = { Host = "test7.test" }, }) assert.res_status(200, res) wait_remaining_sync("/response-headers", { Host = "test7.test" }, {["x-ratelimit-remaining-video-second"] = ITERATIONS}, 429) res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=2"), { headers = { Host = "test7.test" }, }) local body = assert.res_status(429, res) local json = cjson.decode(body) assert.matches("API rate limit exceeded for 'image'", json.message) end) describe("Config with hide_client_headers", function() it("does not send rate-limit headers when hide_client_headers==true", function() wait_server_sync( { Host = "test9.test" }) local res = proxy_client():get("/status/200", { headers = { Host = "test9.test" }, }) assert.res_status(200, res) assert.is_nil(res.headers["x-ratelimit-remaining-video-second"]) assert.is_nil(res.headers["x-ratelimit-limit-video-second"]) end) end) end) describe(fmt("Plugin: response-ratelimiting (expirations) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function() lazy_setup(function() local bp = init_db(strategy, policy) local route = bp.routes:insert { hosts = { "expire1.test" }, protocols = { "http", "https" }, } bp.response_ratelimiting_plugins:insert { route = { id = route.id }, config = { policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, }, fault_tolerant = false, limits = { video = { second = ITERATIONS } }, } } assert(helpers.start_kong({ database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt", })) end) lazy_teardown(function() helpers.stop_kong() end) it("expires a counter", function() wait_server_sync( { Host = "expire1.test" }) local res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = "expire1.test" }, }) assert.res_status(200, res) assert.equal(ITERATIONS, tonumber(res.headers["x-ratelimit-limit-video-second"])) assert.equal(ITERATIONS-1, tonumber(res.headers["x-ratelimit-remaining-video-second"])) wait_server_sync( { Host = "expire1.test" }) -- Wait for counter to expire res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = "expire1.test" }, }) assert.res_status(200, res) assert.equal(ITERATIONS, tonumber(res.headers["x-ratelimit-limit-video-second"])) assert.equal(ITERATIONS-1, tonumber(res.headers["x-ratelimit-remaining-video-second"])) end) end) describe(fmt("Plugin: response-ratelimiting (access - global for single consumer) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function() lazy_setup(function() local bp = init_db(strategy, policy) local consumer = bp.consumers:insert { custom_id = "provider_126", } bp.key_auth_plugins:insert() bp.keyauth_credentials:insert { key = "apikey126", consumer = { id = consumer.id }, } -- just consumer, no no route or service bp.response_ratelimiting_plugins:insert({ consumer = { id = consumer.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS } }, } }) for i = 1, ITERATIONS do bp.routes:insert({ hosts = { fmt("test%d.test", i) } }) end assert(helpers.start_kong({ database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt", })) end) lazy_teardown(function() helpers.stop_kong() end) it("blocks when the consumer exceeds their quota, no matter what service/route used", function() wait_server_sync({ Host = "test1.test" }, "apikey126") test_limit("/response-headers", {["apikey"] = "apikey126", ["x-kong-limit"] = "video=1"}, "test%d.test") end) end) describe(fmt("Plugin: response-ratelimiting (access - global) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function() lazy_setup(function() local bp = init_db(strategy, policy) -- global plugin (not attached to route, service or consumer) bp.response_ratelimiting_plugins:insert({ config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, database = REDIS_DATABASE, }, limits = { video = { second = ITERATIONS } }, } }) for i = 1, ITERATIONS do bp.routes:insert({ hosts = { fmt("test%d.test", i) } }) end assert(helpers.start_kong({ database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt", })) end) lazy_teardown(function() helpers.stop_kong() end) before_each(function() wait_server_sync({ Host = "test1.test" }) end) it("blocks if exceeding limit", function() for i = 1, ITERATIONS do local res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = fmt("test%d.test", i) }, }) assert.res_status(200, res) end -- Wait for async timer to increment the limit wait_remaining_sync("/response-headers", { Host = "test1.test" }, {["x-ratelimit-remaining-video-second"] = 0}) -- last query, while limit is ITERATIONS/second local res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = "test1.test" }, }) assert.res_status(429, res) assert.equal(0, tonumber(res.headers["x-ratelimit-remaining-video-second"])) assert.equal(ITERATIONS, tonumber(res.headers["x-ratelimit-limit-video-second"])) end) end) describe(fmt("Plugin: response-ratelimiting (fault tolerance) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function() if policy == "cluster" then local bp, db pending("fault tolerance tests for cluster policy temporarily disabled", function() before_each(function() bp, db = init_db(strategy, policy) local route1 = bp.routes:insert { hosts = { "failtest1.test" }, } bp.response_ratelimiting_plugins:insert { route = { id = route1.id }, config = { fault_tolerant = false, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, }, limits = { video = { second = ITERATIONS} }, } } local route2 = bp.routes:insert { hosts = { "failtest2.test" }, } bp.response_ratelimiting_plugins:insert { route = { id = route2.id }, config = { fault_tolerant = true, policy = policy, redis = { host = REDIS_HOST, port = redis_conf.redis_port, ssl = redis_conf.redis_ssl, ssl_verify = redis_conf.redis_ssl_verify, server_name = redis_conf.redis_server_name, password = REDIS_PASSWORD, }, limits = { video = {second = ITERATIONS} } } } assert(helpers.start_kong({ database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt", })) end) after_each(function() helpers.stop_kong() end) it("does not work if an error occurs", function() local res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = "failtest1.test" }, }) assert.res_status(200, res) assert.equal(ITERATIONS, tonumber(res.headers["x-ratelimit-limit-video-second"])) assert.equal(ITERATIONS, tonumber(res.headers["x-ratelimit-remaining-video-second"])) -- Simulate an error on the database -- (valid SQL and CQL) db.connector:query("DROP TABLE response_ratelimiting_metrics;") -- FIXME this leaves the database in a bad state after this test, -- affecting subsequent tests. -- Make another request res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = "failtest1.test" }, }) local body = assert.res_status(500, res) local json = cjson.decode(body) assert.matches("An unexpected error occurred", json.message) end) it("keeps working if an error occurs", function() local res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = "failtest2.test" }, }) assert.res_status(200, res) assert.equal(ITERATIONS, tonumber(res.headers["x-ratelimit-limit-video-second"])) assert.equal(ITERATIONS, tonumber(res.headers["x-ratelimit-remaining-video-second"])) -- Simulate an error on the database -- (valid SQL and CQL) db.connector:query("DROP TABLE response_ratelimiting_metrics;") -- FIXME this leaves the database in a bad state after this test, -- affecting subsequent tests. -- Make another request res = proxy_client():get("/response-headers?x-kong-limit="..escape_uri("video=1"), { headers = { Host = "failtest2.test" }, }) assert.res_status(200, res) assert.is_nil(res.headers["x-ratelimit-limit-video-second"]) assert.is_nil(res.headers["x-ratelimit-remaining-video-second"]) end) end) end if policy == "redis" then before_each(function() local bp = init_db(strategy, policy) local route1 = bp.routes:insert { hosts = { "failtest3.test" }, protocols = { "http", "https" }, } bp.response_ratelimiting_plugins:insert { route = { id = route1.id }, config = { fault_tolerant = false, policy = policy, redis = { host = "5.5.5.5", port = REDIS_PORT }, limits = { video = { second = ITERATIONS } }, } } local route2 = bp.routes:insert { hosts = { "failtest4.test" }, protocols = { "http", "https" }, } bp.response_ratelimiting_plugins:insert { route = { id = route2.id }, config = { fault_tolerant = true, policy = policy, redis = { host = "5.5.5.5", port = REDIS_PORT }, limits = { video = { second = ITERATIONS } }, } } assert(helpers.start_kong({ database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt", })) end) after_each(function() helpers.stop_kong() end) it("does not work if an error occurs", function() -- Make another request local res = proxy_client():get("/status/200", { headers = { Host = "failtest3.test" }, }) local body = assert.res_status(500, res) local json = cjson.decode(body) assert.matches("An unexpected error occurred", json.message) end) it("keeps working if an error occurs", function() -- Make another request local res = proxy_client():get("/status/200", { headers = { Host = "failtest4.test" }, }) assert.res_status(200, res) assert.falsy(res.headers["x-ratelimit-limit-video-second"]) assert.falsy(res.headers["x-ratelimit-remaining-video-second"]) end) end end) ::continue:: end end end