项目文件夹

文件
2026-07-13 12:32:21 +08:00

1019 行
38 KiB
Lua

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