kong/spec/03-plugins/23-rate-limiting/04-access_spec.lua (1,095 lines of code) (raw):
local helpers = require "spec.helpers"
local cjson = require "cjson"
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 fmt = string.format
local proxy_client = helpers.proxy_client
-- This performs the test up to two times (and no more than two).
-- We are **not** retrying to "give it another shot" in case of a flaky test.
-- The reason why we allow for a single retry in this test suite is because
-- tests are dependent on the value of the current minute. If the minute
-- flips during the test (i.e. going from 03:43:59 to 03:44:00), the result
-- will fail. Since each test takes less than a minute to run, running it
-- a second time right after that failure ensures that another flip will
-- not occur. If the second execution failed as well, this means that there
-- was an actual problem detected by the test.
local function it_with_retry(desc, test)
return it(desc, function(...)
if not pcall(test, ...) then
ngx.sleep(61 - (ngx.now() % 60)) -- Wait for minute to expire
test(...)
end
end)
end
local function GET(url, opts, res_status)
ngx.sleep(0.010)
local client = proxy_client()
local res, err = client:get(url, opts)
if not res then
client:close()
return nil, err
end
local body, err = assert.res_status(res_status, res)
if not body then
return nil, err
end
client:close()
return res, body
end
local function flush_redis()
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(2000)
local ok, err = red:connect(REDIS_HOST, REDIS_PORT)
if not ok then
error("failed to connect to Redis: " .. err)
end
if REDIS_PASSWORD and REDIS_PASSWORD ~= "" then
local ok, err = red:auth(REDIS_PASSWORD)
if not ok then
error("failed to connect to Redis: " .. err)
end
end
local ok, err = red:select(REDIS_DATABASE)
if not ok then
error("failed to change Redis database: " .. err)
end
red:flushall()
red:close()
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,
},
}
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: rate-limiting (access) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function()
local bp
local db
lazy_setup(function()
helpers.kill_all()
flush_redis()
bp, db = helpers.get_db_utils(strategy)
local consumer1 = bp.consumers:insert {
custom_id = "provider_123",
}
bp.keyauth_credentials:insert {
key = "apikey122",
consumer = { id = consumer1.id },
}
local consumer2 = bp.consumers:insert {
custom_id = "provider_124",
}
bp.keyauth_credentials:insert {
key = "apikey123",
consumer = { id = consumer2.id },
}
bp.keyauth_credentials:insert {
key = "apikey333",
consumer = { id = consumer2.id },
}
local route1 = bp.routes:insert {
hosts = { "test1.com" },
}
bp.rate_limiting_plugins:insert({
route = { id = route1.id },
config = {
policy = policy,
minute = 6,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
local route_grpc_1 = assert(bp.routes:insert {
protocols = { "grpc" },
paths = { "/hello.HelloService/" },
service = assert(bp.services:insert {
name = "grpc",
url = helpers.grpcbin_url,
}),
})
bp.rate_limiting_plugins:insert({
route = { id = route_grpc_1.id },
config = {
policy = policy,
minute = 6,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
local route2 = bp.routes:insert {
hosts = { "test2.com" },
}
bp.rate_limiting_plugins:insert({
route = { id = route2.id },
config = {
minute = 3,
hour = 5,
fault_tolerant = false,
policy = policy,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
local route3 = bp.routes:insert {
hosts = { "test3.com" },
}
bp.plugins:insert {
name = "key-auth",
route = { id = route3.id },
}
bp.rate_limiting_plugins:insert({
route = { id = route3.id },
config = {
minute = 6,
limit_by = "credential",
fault_tolerant = false,
policy = policy,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
bp.rate_limiting_plugins:insert({
route = { id = route3.id },
consumer = { id = consumer1.id },
config = {
minute = 8,
fault_tolerant = false,
policy = policy,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE
}
})
local route4 = bp.routes:insert {
hosts = { "test4.com" },
}
bp.plugins:insert {
name = "key-auth",
route = { id = route4.id },
}
bp.rate_limiting_plugins:insert({
route = { id = route4.id },
consumer = { id = consumer1.id },
config = {
minute = 6,
fault_tolerant = true,
policy = policy,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
},
})
local route5 = bp.routes:insert {
hosts = { "test5.com" },
}
bp.rate_limiting_plugins:insert({
route = { id = route5.id },
config = {
policy = policy,
minute = 6,
hide_client_headers = true,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
},
})
local service = bp.services:insert()
bp.routes:insert {
hosts = { "test-service1.com" },
service = service,
}
bp.routes:insert {
hosts = { "test-service2.com" },
service = service,
}
bp.rate_limiting_plugins:insert({
service = { id = service.id },
config = {
policy = policy,
minute = 6,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
local service = bp.services:insert()
bp.routes:insert {
hosts = { "test-path.com" },
service = service,
}
bp.rate_limiting_plugins:insert({
service = { id = service.id },
config = {
limit_by = "path",
path = "/status/200",
policy = policy,
minute = 6,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
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()
assert(db:truncate())
end)
describe("Without authentication (IP address)", function()
it_with_retry("blocks if exceeding limit", function()
for i = 1, 6 do
local res = GET("/status/200", {
headers = { Host = "test1.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset >= 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additonal request, while limit is 6/minute
local res, body = GET("/status/200", {
headers = { Host = "test1.com" },
}, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
it_with_retry("blocks if exceeding limit, only if done via same path", function()
for i = 1, 3 do
local res = GET("/status/200", {
headers = { Host = "test-path.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Try a different path on the same host. This should reset the timers
for i = 1, 3 do
local res = GET("/status/201", {
headers = { Host = "test-path.com" },
}, 201)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Continue doing requests on the path which "blocks"
for i = 4, 6 do
local res = GET("/status/200", {
headers = { Host = "test-path.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additonal request, while limit is 6/minute
local res, body = GET("/status/200", {
headers = { Host = "test-path.com" },
}, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
it_with_retry("counts against the same service register from different routes", function()
for i = 1, 3 do
local res = GET("/status/200", {
headers = { Host = "test-service1.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
for i = 4, 6 do
local res = GET("/status/200", {
headers = { Host = "test-service2.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additonal request, while limit is 6/minute
local res, body = GET("/status/200", {
headers = { Host = "test-service1.com" },
}, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
it_with_retry("handles multiple limits #flaky", function()
local limits = {
minute = 3,
hour = 5
}
for i = 1, 3 do
local res = GET("/status/200", {
headers = { Host = "test2.com" },
}, 200)
assert.are.same(limits.minute, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(limits.minute - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(limits.hour, tonumber(res.headers["x-ratelimit-limit-hour"]))
assert.are.same(limits.hour - i, tonumber(res.headers["x-ratelimit-remaining-hour"]))
assert.are.same(limits.minute, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(limits.minute - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
local res, body = GET("/status/200", {
path = "/status/200",
headers = { Host = "test2.com" },
}, 429)
assert.are.same(limits.minute, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
assert.equal(2, tonumber(res.headers["x-ratelimit-remaining-hour"]))
assert.equal(0, tonumber(res.headers["x-ratelimit-remaining-minute"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
end)
describe("Without authentication (IP address)", function()
it_with_retry("blocks if exceeding limit #grpc", function()
for i = 1, 6 do
local ok, res = helpers.proxy_client_grpc(){
service = "hello.HelloService.SayHello",
opts = {
["-v"] = true,
},
}
assert.truthy(ok)
assert.matches("x%-ratelimit%-limit%-minute: 6", res)
assert.matches("x%-ratelimit%-remaining%-minute: " .. (6 - i), res)
assert.matches("ratelimit%-limit: 6", res)
assert.matches("ratelimit%-remaining: " .. (6 - i), res)
local reset = tonumber(string.match(res, "ratelimit%-reset: (%d+)"))
assert.equal(true, reset <= 60 and reset >= 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additonal request, while limit is 6/minute
local ok, res = helpers.proxy_client_grpc(){
service = "hello.HelloService.SayHello",
opts = {
["-v"] = true,
},
}
assert.falsy(ok)
assert.matches("Code: ResourceExhausted", res)
assert.matches("ratelimit%-limit: 6", res)
assert.matches("ratelimit%-remaining: 0", res)
local retry = tonumber(string.match(res, "retry%-after: (%d+)"))
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(string.match(res, "ratelimit%-reset: (%d+)"))
assert.equal(true, reset <= 60 and reset > 0)
end)
end)
describe("With authentication", function()
describe("API-specific plugin", function()
it_with_retry("blocks if exceeding limit", function()
for i = 1, 6 do
local res = GET("/status/200?apikey=apikey123", {
headers = { Host = "test3.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Third query, while limit is 2/minute
local res, body = GET("/status/200?apikey=apikey123", {
headers = { Host = "test3.com" },
}, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
-- Using a different key of the same consumer works
GET("/status/200?apikey=apikey333", {
headers = { Host = "test3.com" },
}, 200)
end)
end)
describe("#flaky Plugin customized for specific consumer and route", function()
it_with_retry("blocks if exceeding limit", function()
for i = 1, 8 do
local res = GET("/status/200?apikey=apikey122", {
headers = { Host = "test3.com" },
}, 200)
assert.are.same(8, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(8 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(8, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(8 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
local res, body = GET("/status/200?apikey=apikey122", {
headers = { Host = "test3.com" },
}, 429)
assert.are.same(8, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
it_with_retry("blocks if the only rate-limiting plugin existing is per consumer and not per API", function()
for i = 1, 6 do
local res = GET("/status/200?apikey=apikey122", {
headers = { Host = "test4.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
local res, body = GET("/status/200?apikey=apikey122", {
headers = { Host = "test4.com" },
}, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
end)
end)
describe("Config with hide_client_headers", function()
it_with_retry("does not send rate-limit headers when hide_client_headers==true", function()
local res = GET("/status/200", {
headers = { Host = "test5.com" },
}, 200)
assert.is_nil(res.headers["x-ratelimit-limit-minute"])
assert.is_nil(res.headers["x-ratelimit-remaining-minute"])
assert.is_nil(res.headers["ratelimit-limit"])
assert.is_nil(res.headers["ratelimit-remaining"])
assert.is_nil(res.headers["ratelimit-reset"])
assert.is_nil(res.headers["retry-after"])
end)
end)
if policy == "cluster" then
describe("#flaky Fault tolerancy", function()
before_each(function()
helpers.kill_all()
assert(db:truncate())
local route1 = bp.routes:insert {
hosts = { "failtest1.com" },
}
bp.rate_limiting_plugins:insert {
route = { id = route1.id },
config = { minute = 6, fault_tolerant = false }
}
local route2 = bp.routes:insert {
hosts = { "failtest2.com" },
}
bp.rate_limiting_plugins:insert {
name = "rate-limiting",
route = { id = route2.id },
config = { minute = 6, fault_tolerant = true },
}
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.kill_all()
assert(db:truncate())
end)
it_with_retry("does not work if an error occurs", function()
local res = GET("/status/200", {
headers = { Host = "failtest1.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(5, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(5, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- Simulate an error on the database
assert(db.connector:query("DROP TABLE ratelimiting_metrics"))
-- Make another request
local _, body = GET("/status/200", {
headers = { Host = "failtest1.com" },
}, 500)
local json = cjson.decode(body)
assert.same({ message = "An unexpected error occurred" }, json)
db:reset()
bp, db = helpers.get_db_utils(strategy)
end)
it_with_retry("keeps working if an error occurs", function()
local res = GET("/status/200", {
headers = { Host = "failtest2.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(5, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(5, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- Simulate an error on the database
assert(db.connector:query("DROP TABLE ratelimiting_metrics"))
-- Make another request
local res = GET("/status/200", {
headers = { Host = "failtest2.com" },
}, 200)
assert.falsy(res.headers["x-ratelimit-limit-minute"])
assert.falsy(res.headers["x-ratelimit-remaining-minute"])
assert.falsy(res.headers["ratelimit-limit"])
assert.falsy(res.headers["ratelimit-remaining"])
assert.falsy(res.headers["ratelimit-reset"])
db:reset()
bp, db = helpers.get_db_utils(strategy)
end)
end)
elseif policy == "redis" then
describe("#flaky Fault tolerancy", function()
before_each(function()
helpers.kill_all()
assert(db:truncate())
local service1 = bp.services:insert()
local route1 = bp.routes:insert {
hosts = { "failtest3.com" },
protocols = { "http", "https" },
service = service1
}
bp.rate_limiting_plugins:insert {
route = { id = route1.id },
config = { minute = 6, policy = policy, redis_host = "5.5.5.5", fault_tolerant = false },
}
local service2 = bp.services:insert()
local route2 = bp.routes:insert {
hosts = { "failtest4.com" },
protocols = { "http", "https" },
service = service2
}
bp.rate_limiting_plugins:insert {
name = "rate-limiting",
route = { id = route2.id },
config = { minute = 6, policy = policy, redis_host = "5.5.5.5", fault_tolerant = true },
}
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.kill_all()
assert(db:truncate())
end)
it_with_retry("does not work if an error occurs", function()
-- Make another request
local _, body = GET("/status/200", {
headers = { Host = "failtest3.com" },
}, 500)
local json = cjson.decode(body)
assert.same({ message = "An unexpected error occurred" }, json)
end)
it_with_retry("keeps working if an error occurs", function()
local res = GET("/status/200", {
headers = { Host = "failtest4.com" },
}, 200)
assert.falsy(res.headers["x-ratelimit-limit-minute"])
assert.falsy(res.headers["x-ratelimit-remaining-minute"])
assert.falsy(res.headers["ratelimit-limit"])
assert.falsy(res.headers["ratelimit-remaining"])
assert.falsy(res.headers["ratelimit-reset"])
end)
end)
end
describe("Expirations", function()
local route
lazy_setup(function()
helpers.stop_kong()
local bp = helpers.get_db_utils(strategy)
route = bp.routes:insert {
hosts = { "expire1.com" },
}
bp.rate_limiting_plugins:insert {
route = { id = route.id },
config = {
minute = 6,
policy = policy,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
fault_tolerant = false,
redis_database = REDIS_DATABASE,
},
}
assert(helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt",
}))
end)
it_with_retry("#flaky expires a counter", function()
local t = 61 - (ngx.now() % 60)
local res = GET("/status/200", {
headers = { Host = "expire1.com" },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(5, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(5, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
ngx.sleep(t) -- Wait for minute to expire
local res = GET("/status/200", {
headers = { Host = "expire1.com" }
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(5, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(5, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
end)
end)
end)
describe(fmt("Plugin: rate-limiting (access - global for single consumer) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function()
local bp
local db
lazy_setup(function()
helpers.kill_all()
flush_redis()
bp, db = helpers.get_db_utils(strategy)
local consumer = bp.consumers:insert {
custom_id = "provider_125",
}
bp.key_auth_plugins:insert()
bp.keyauth_credentials:insert {
key = "apikey125",
consumer = { id = consumer.id },
}
-- just consumer, no no route or service
bp.rate_limiting_plugins:insert({
consumer = { id = consumer.id },
config = {
limit_by = "credential",
policy = policy,
minute = 6,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
for i = 1, 6 do
bp.routes:insert({ hosts = { fmt("test%d.com", 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.kill_all()
assert(db:truncate())
end)
it_with_retry("blocks when the consumer exceeds their quota, no matter what service/route used", function()
for i = 1, 6 do
local res = GET("/status/200?apikey=apikey125", {
headers = { Host = fmt("test%d.com", i) },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additonal request, while limit is 6/minute
local res, body = GET("/status/200?apikey=apikey125", {
headers = { Host = "test1.com" },
}, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
end)
describe(fmt("Plugin: rate-limiting (access - global for service) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function()
local bp
local db
lazy_setup(function()
helpers.kill_all()
flush_redis()
bp, db = helpers.get_db_utils(strategy)
-- global plugin (not attached to route, service or consumer)
bp.rate_limiting_plugins:insert({
config = {
limit_by = "service",
policy = policy,
minute = 6,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
local service = bp.services:insert()
for i = 1, 6 do
bp.routes:insert({
hosts = { fmt("test%d.com", i) },
service = service,
})
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.kill_all()
assert(db:truncate())
end)
it_with_retry("blocks if exceeding limit", function()
for i = 1, 6 do
local res = GET("/status/200", {
headers = { Host = fmt("test%d.com", i) },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additonal request, while limit is 6/minute
local res, body = GET("/status/200", {
headers = { Host = "test1.com" },
}, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
end)
describe(fmt("Plugin: rate-limiting (access - per service) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function()
local bp
local db
lazy_setup(function()
helpers.kill_all()
flush_redis()
bp, db = helpers.get_db_utils(strategy)
-- global plugin (not attached to route, service or consumer)
bp.rate_limiting_plugins:insert({
config = {
limit_by = "service",
policy = policy,
minute = 6,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
local service1 = bp.services:insert()
bp.routes:insert {
hosts = { "test1.com" },
service = service1,
}
local service2 = bp.services:insert()
bp.routes:insert {
hosts = { "test2.com" },
service = service2,
}
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.kill_all()
assert(db:truncate())
end)
it_with_retry("blocks if exceeding limit", function()
for i = 1, 6 do
local res = GET("/status/200", { headers = { Host = "test1.com" } }, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
for i = 1, 6 do
local res = GET("/status/200", { headers = { Host = "test2.com" } }, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additonal request, while limit is 6/minute
for _, host in ipairs{ "test1.com", "test2.com" } do
local res, body = GET("/status/200", { headers = { Host = host } }, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end
end)
end)
describe(fmt("Plugin: rate-limiting (access - global) with policy: #%s #%s [#%s]", redis_conf_name, policy, strategy), function()
local bp
local db
lazy_setup(function()
helpers.kill_all()
flush_redis()
bp, db = helpers.get_db_utils(strategy)
-- global plugin (not attached to route, service or consumer)
bp.rate_limiting_plugins:insert({
config = {
policy = policy,
minute = 6,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
for i = 1, 6 do
bp.routes:insert({ hosts = { fmt("test%d.com", 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.kill_all()
assert(db:truncate())
end)
it_with_retry("blocks if exceeding limit", function()
for i = 1, 6 do
local res = GET("/status/200", {
headers = { Host = fmt("test%d.com", i) },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additonal request, while limit is 6/minute
local res, body = GET("/status/200", {
headers = { Host = "test1.com" },
}, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
end)
describe(fmt("Plugin: rate-limiting (access - global) with policy: #%s #%s [#%s] by path", redis_conf_name, policy, strategy), function()
local bp
local db
lazy_setup(function()
helpers.kill_all()
flush_redis()
bp, db = helpers.get_db_utils(strategy)
-- global plugin (not attached to route, service or consumer)
bp.rate_limiting_plugins:insert({
config = {
policy = policy,
minute = 6,
fault_tolerant = false,
redis_host = REDIS_HOST,
redis_port = redis_conf.redis_port,
redis_ssl = redis_conf.redis_ssl,
redis_ssl_verify = redis_conf.redis_ssl_verify,
redis_server_name = redis_conf.redis_server_name,
redis_password = REDIS_PASSWORD,
redis_database = REDIS_DATABASE,
}
})
-- hosts with services
for i = 1, 3 do
bp.routes:insert({ service = bp.services:insert(), hosts = { fmt("test%d.com", i) } })
end
-- serviceless routes
for i = 4, 6 do
bp.routes:insert({ hosts = { fmt("test%d.com", 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.kill_all()
assert(db:truncate())
end)
it_with_retry("maintains the counters for a path through different services and routes", function()
for i = 1, 6 do
local res = GET("/status/200", {
headers = { Host = fmt("test%d.com", i) },
}, 200)
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.are.same(6 - i, tonumber(res.headers["x-ratelimit-remaining-minute"]))
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(6 - i, tonumber(res.headers["ratelimit-remaining"]))
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additonal request, while limit is 6/minute
local res, body = GET("/status/200", {
headers = { Host = "test1.com" },
}, 429)
assert.are.same(6, tonumber(res.headers["ratelimit-limit"]))
assert.are.same(0, tonumber(res.headers["ratelimit-remaining"]))
local retry = tonumber(res.headers["retry-after"])
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(res.headers["ratelimit-reset"])
assert.equal(true, reset <= 60 and reset > 0)
local json = cjson.decode(body)
assert.same({ message = "API rate limit exceeded" }, json)
end)
end)
::continue::
end
end
end