kong/spec/02-integration/04-admin_api/15-off_spec.lua (1,057 lines of code) (raw):
local cjson = require "cjson"
local lyaml = require "lyaml"
local utils = require "kong.tools.utils"
local pl_utils = require "pl.utils"
local helpers = require "spec.helpers"
local Errors = require "kong.db.errors"
local mocker = require("spec.fixtures.mocker")
local WORKER_SYNC_TIMEOUT = 10
local LMDB_MAP_SIZE = "10m"
local TEST_CONF = helpers.test_conf
local function it_content_types(title, fn)
local test_form_encoded = fn("application/x-www-form-urlencoded")
local test_multipart = fn("multipart/form-data")
local test_json = fn("application/json")
it(title .. " with application/www-form-urlencoded", test_form_encoded)
it(title .. " with multipart/form-data", test_multipart)
it(title .. " with application/json", test_json)
end
describe("Admin API #off", function()
local client
lazy_setup(function()
assert(helpers.start_kong({
database = "off",
lmdb_map_size = LMDB_MAP_SIZE,
stream_listen = "127.0.0.1:9011",
nginx_conf = "spec/fixtures/custom_nginx.template",
}))
end)
lazy_teardown(function()
helpers.stop_kong(nil, true)
end)
before_each(function()
client = assert(helpers.admin_client())
end)
after_each(function()
if client then
client:close()
end
end)
describe("/routes", function()
describe("POST", function()
it_content_types("doesn't allow to creates a route", function(content_type)
return function()
if content_type == "multipart/form-data" then
-- the client doesn't play well with this
return
end
local res = client:post("/routes", {
body = {
protocols = { "http" },
hosts = { "my.route.com" },
service = { id = utils.uuid() },
},
headers = { ["Content-Type"] = content_type }
})
local body = assert.res_status(405, res)
local json = cjson.decode(body)
assert.same({
code = Errors.codes.OPERATION_UNSUPPORTED,
name = Errors.names[Errors.codes.OPERATION_UNSUPPORTED],
message = "cannot create 'routes' entities when not using a database",
}, json)
end
end)
it_content_types("doesn't allow to creates a complex route", function(content_type)
return function()
if content_type == "multipart/form-data" then
-- the client doesn't play well with this
return
end
local res = client:post("/routes", {
body = {
protocols = { "http" },
methods = { "GET", "POST", "PATCH" },
hosts = { "foo.api.com", "bar.api.com" },
paths = { "/foo", "/bar" },
service = { id = utils.uuid() },
},
headers = { ["Content-Type"] = content_type }
})
local body = assert.res_status(405, res)
local json = cjson.decode(body)
assert.same({
code = Errors.codes.OPERATION_UNSUPPORTED,
name = Errors.names[Errors.codes.OPERATION_UNSUPPORTED],
message = "cannot create 'routes' entities when not using a database",
}, json)
end
end)
end)
describe("GET", function()
describe("errors", function()
it("handles invalid offsets", function()
local res = client:get("/routes", { query = { offset = "x" } })
local body = assert.res_status(400, res)
assert.same({
code = Errors.codes.INVALID_OFFSET,
name = "invalid offset",
message = "'x' is not a valid offset: bad base64 encoding"
}, cjson.decode(body))
res = client:get("/routes", { query = { offset = "|potato|" } })
body = assert.res_status(400, res)
local json = cjson.decode(body)
json.message = nil
assert.same({
code = Errors.codes.INVALID_OFFSET,
name = "invalid offset",
}, json)
end)
end)
end)
it("returns HTTP 405 on invalid method", function()
local methods = { "DELETE", "PUT", "PATCH", "POST" }
for i = 1, #methods do
local res = assert(client:send {
method = methods[i],
path = "/routes",
body = {
paths = { "/" },
service = { id = utils.uuid() }
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.response(res).has.status(405)
local json = cjson.decode(body)
if methods[i] == "POST" then
assert.same({
code = Errors.codes.OPERATION_UNSUPPORTED,
name = Errors.names[Errors.codes.OPERATION_UNSUPPORTED],
message = "cannot create 'routes' entities when not using a database",
}, json)
else
assert.same({ message = "Method not allowed" }, json)
end
end
end)
end)
describe("/routes/{route}", function()
it("returns HTTP 405 on invalid method", function()
local methods = { "PUT", "POST" }
for i = 1, #methods do
local res = assert(client:send {
method = methods[i],
path = "/routes/" .. utils.uuid(),
body = {
paths = { "/" },
service = { id = utils.uuid() }
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.response(res).has.status(405)
local json = cjson.decode(body)
if methods[i] ~= "POST" then
assert.same({
code = Errors.codes.OPERATION_UNSUPPORTED,
name = Errors.names[Errors.codes.OPERATION_UNSUPPORTED],
message = "cannot create or update 'routes' entities when not using a database",
}, json)
else
assert.same({ message = "Method not allowed" }, json)
end
end
end)
end)
describe("/config", function()
describe("POST", function()
it("accepts configuration as JSON body", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
_format_version = "1.1",
consumers = {
{
username = "bobby_in_json_body",
},
},
},
headers = {
["Content-Type"] = "application/json"
},
})
assert.response(res).has.status(201)
end)
it("accepts configuration as YAML body", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = helpers.unindent([[
_format_version: "1.1"
consumers:
- username: "bobby_in_yaml_body"
]]),
headers = {
["Content-Type"] = "text/yaml"
},
})
assert.response(res).has.status(201)
end)
it("accepts configuration as a JSON string under `config` JSON key", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = [[
{
"_format_version" : "1.1",
"consumers" : [
{
"username" : "bobby_in_json_under_config"
}
]
}
]],
},
headers = {
["Content-Type"] = "application/json"
},
})
assert.response(res).has.status(201)
end)
it("accepts configuration as a YAML string under `config` JSON key", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = helpers.unindent([[
_format_version: "1.1"
consumers:
- username: "bobby_in_yaml_under_config"
]]),
},
headers = {
["Content-Type"] = "application/json"
},
})
assert.response(res).has.status(201)
end)
it("fails with 413 and preserves previous cache if config does not fit in cache", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = [[
{
"_format_version" : "1.1",
"consumers" : [
{
"username" : "previous"
}
]
}
]],
type = "json",
},
headers = {
["Content-Type"] = "application/json"
},
})
assert.response(res).has.status(201)
helpers.wait_until(function()
res = assert(client:send {
method = "GET",
path = "/consumers/previous",
headers = {
["Content-Type"] = "application/json"
}
})
local body = res:read_body()
local json = cjson.decode(body)
if res.status == 200 and json.username == "previous" then
return true
end
end, WORKER_SYNC_TIMEOUT)
client:close()
client = assert(helpers.admin_client())
local consumers = {}
for i = 1, 20000 do
table.insert(consumers, [[
{
"username" : "bobby-]] .. i .. [["
}
]])
end
local config = [[
{
"_format_version" : "1.1",
"consumers" : [
]] .. table.concat(consumers, ", ") .. [[
]
}
]]
res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = config,
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(413)
helpers.wait_until(function()
client:close()
client = assert(helpers.admin_client())
res = assert(client:send {
method = "GET",
path = "/consumers/previous",
headers = {
["Content-Type"] = "application/json"
}
})
local body = res:read_body()
local json = cjson.decode(body)
if res.status == 200 and json.username == "previous" then
return true
end
end, WORKER_SYNC_TIMEOUT)
end)
it("accepts configuration containing null as a YAML string", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = [[
_format_version: "1.1"
routes:
- paths:
- "/"
service: null
]],
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
end)
it("hides workspace related fields from /config response", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = [[
_format_version: "1.1"
services:
- name: my-service
id: 0855b320-0dd2-547d-891d-601e9b38647f
url: https://example.com
plugins:
- name: file-log
id: 0611a5a9-de73-5a2d-a4e6-6a38ad4c3cb2
config:
path: /tmp/file.log
- name: key-auth
id: 661199ff-aa1c-5498-982c-d57a4bd6e48b
routes:
- name: my-route
id: 481a9539-f49c-51b6-b2e2-fe99ee68866c
paths:
- /
consumers:
- username: my-user
id: 4b1b701d-de2b-5588-9aa2-3b97061d9f52
keyauth_credentials:
- key: my-key
id: 487ab43c-b2c9-51ec-8da5-367586ea2b61
]],
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.response(res).has.status(201)
local entities = cjson.decode(body)
assert.is_nil(entities.workspaces)
assert.is_nil(entities.consumers["4b1b701d-de2b-5588-9aa2-3b97061d9f52"].ws_id)
assert.is_nil(entities.keyauth_credentials["487ab43c-b2c9-51ec-8da5-367586ea2b61"].ws_id)
assert.is_nil(entities.plugins["0611a5a9-de73-5a2d-a4e6-6a38ad4c3cb2"].ws_id)
assert.is_nil(entities.plugins["661199ff-aa1c-5498-982c-d57a4bd6e48b"].ws_id)
assert.is_nil(entities.routes["481a9539-f49c-51b6-b2e2-fe99ee68866c"].ws_id)
assert.is_nil(entities.services["0855b320-0dd2-547d-891d-601e9b38647f"].ws_id)
end)
it("can reload upstreams (regression test)", function()
local config = [[
_format_version: "1.1"
services:
- host: foo
routes:
- paths:
- "/"
upstreams:
- name: "foo"
targets:
- target: 10.20.30.40
]]
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = config,
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
client:close()
client = helpers.admin_client()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = config,
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
end)
it("returns 304 if checking hash and configuration is identical", function()
local res = assert(client:send {
method = "POST",
path = "/config?check_hash=1",
body = {
config = [[
_format_version: "1.1"
consumers:
- username: bobby_tables
]],
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
client:close()
client = helpers.admin_client()
res = assert(client:send {
method = "POST",
path = "/config?check_hash=1",
body = {
config = [[
_format_version: "1.1"
consumers:
- username: bobby_tables
]],
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(304)
end)
it("returns 400 on an invalid config string", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = "bobby tables",
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.response(res).has.status(400)
local json = cjson.decode(body)
assert.same({
code = 14,
fields = {
error ="failed parsing declarative configuration: expected an object",
},
message = [[declarative config is invalid: ]] ..
[[{error="failed parsing declarative configuration: ]] ..
[[expected an object"}]],
name = "invalid declarative configuration",
}, json)
end)
it("returns 400 on a validation error", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = [[
_format_version: "1.1"
services:
- port: -12
]],
},
headers = {
["Content-Type"] = "application/json"
}
})
local body = assert.response(res).has.status(400)
local json = cjson.decode(body)
assert.same({
code = 14,
fields = {
services = {
{
host = "required field missing",
port = "value should be between 0 and 65535",
}
}
},
message = [[declarative config is invalid: ]] ..
[[{services={{host="required field missing",]] ..
[[port="value should be between 0 and 65535"}}}]],
name = "invalid declarative configuration",
}, json)
end)
it("returns 400 when given no input", function()
local res = assert(client:send {
method = "POST",
path = "/config",
})
local body = assert.response(res).has.status(400)
local json = cjson.decode(body)
assert.same({
message = "expected a declarative configuration",
}, json)
end)
it("sparse responses are correctly generated", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = [[
{
"_format_version" : "1.1",
"plugins": [{
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "key-auth",
"enabled": true,
"protocols": ["http", "https"]
}, {
"name": "cors",
"config": {
"credentials": true,
"exposed_headers": ["*"],
"headers": ["*"],
"methods": ["*"],
"origins": ["*"],
"preflight_continue": true
},
"enabled": true,
"protocols": ["http", "https"]
}]
}
]],
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(400)
end)
end)
describe("GET", function()
it("returns back the configuration", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
_format_version = "1.1",
consumers = {
{
username = "bobo",
id = "d885e256-1abe-5e24-80b6-8f68fe59ea8e",
created_at = 1566863706,
},
},
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
local res = assert(client:send {
method = "GET",
path = "/config",
})
local body = assert.response(res).has.status(200)
local json = cjson.decode(body)
local config = assert(lyaml.load(json.config))
assert.same({
_format_version = "3.0",
_transform = false,
consumers = {
{ id = "d885e256-1abe-5e24-80b6-8f68fe59ea8e",
created_at = 1566863706,
username = "bobo",
custom_id = lyaml.null,
tags = lyaml.null,
},
},
}, config)
end)
end)
it("can load large declarative config (regression test)", function()
local config = assert(pl_utils.readfile("spec/fixtures/burst.yml"))
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = config,
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
end)
it("updates stream subsystem config", function()
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = [[
_format_version: "1.1"
services:
- connect_timeout: 60000
host: 127.0.0.1
name: mock
port: 15557
protocol: tcp
routes:
- name: mock_route
protocols:
- tcp
destinations:
- port: 9011
]],
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
helpers.wait_until(function()
local sock = ngx.socket.tcp()
assert(sock:connect("127.0.0.1", 9011))
assert(sock:send("hi\n"))
local pok = pcall(helpers.wait_until, function()
return sock:receive() == "hi"
end, 1)
sock:close()
return pok == true
end)
end)
end)
describe("/upstreams", function()
it("can set target health without port", function()
local config = [[
_format_version: "1.1"
services:
- host: foo
routes:
- paths:
- "/"
upstreams:
- name: "foo"
targets:
- target: 10.20.30.40
healthchecks:
passive:
healthy:
successes: 1
unhealthy:
http_failures: 1
]]
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = config,
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
helpers.wait_until(function()
local res = assert(client:send {
method = "PUT",
path = "/upstreams/foo/targets/c830b59e-59cc-5392-adfd-b414d13adfc4/10.20.30.40/unhealthy",
})
return pcall(function()
assert.response(res).has.status(204)
end)
end, 10)
client:close()
end)
it("targets created missing ports listed with ports", function()
local config = [[
_format_version: "1.1"
services:
- host: foo
routes:
- paths:
- "/"
upstreams:
- name: "foo"
targets:
- target: 10.20.30.40
- target: 50.60.70.80:90
]]
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = config,
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
local res = assert(client:send {
method = "GET",
path = "/upstreams/foo/targets/all",
})
local body = assert.response(res).has.status(200)
local json = cjson.decode(body)
table.sort(json.data, function(t1, t2)
return t1.target < t2.target
end)
assert.same("10.20.30.40:8000", json.data[1].target)
assert.same("50.60.70.80:90", json.data[2].target)
client:close()
end)
end)
end)
describe("Admin API (concurrency tests) #off", function()
local client
before_each(function()
assert(helpers.start_kong({
database = "off",
nginx_worker_processes = 8,
lmdb_map_size = LMDB_MAP_SIZE,
}))
client = assert(helpers.admin_client())
end)
after_each(function()
helpers.stop_kong(nil, true)
if client then
client:close()
end
end)
it("succeeds with 200 and replaces previous cache if config fits in cache", function()
-- stress test to check for worker concurrency issues
for k = 1, 100 do
if client then
client:close()
client = helpers.admin_client()
end
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = [[
{
"_format_version" : "1.1",
"consumers" : [
{
"username" : "previous",
},
],
}
]],
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
client:close()
local consumers = {}
for i = 1, 10 do
table.insert(consumers, [[
{
"username" : "bobby-]] .. k .. "-" .. i .. [[",
}
]])
end
local config = [[
{
"_format_version" : "1.1",
"consumers" : [
]] .. table.concat(consumers, ", ") .. [[
]
}
]]
client = assert(helpers.admin_client())
res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = config,
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.response(res).has.status(201)
client:close()
helpers.wait_until(function()
client = assert(helpers.admin_client())
res = assert(client:send {
method = "GET",
path = "/consumers/previous",
headers = {
["Content-Type"] = "application/json"
}
})
client:close()
return res.status == 404
end, WORKER_SYNC_TIMEOUT)
helpers.wait_until(function()
client = assert(helpers.admin_client())
res = assert(client:send {
method = "GET",
path = "/consumers/bobby-" .. k .. "-10",
headers = {
["Content-Type"] = "application/json"
}
})
local body = res:read_body()
client:close()
if res.status ~= 200 then
return false
end
local json = cjson.decode(body)
return "bobby-" .. k .. "-10" == json.username
end, WORKER_SYNC_TIMEOUT)
end
end)
end)
describe("Admin API #off with Unique Foreign #unique", function()
local client
lazy_setup(function()
assert(helpers.start_kong({
database = "off",
plugins = "unique-foreign",
nginx_worker_processes = 1,
lmdb_map_size = LMDB_MAP_SIZE,
}))
end)
lazy_teardown(function()
helpers.stop_kong(nil, true)
end)
before_each(function()
client = assert(helpers.admin_client())
end)
after_each(function()
if client then
client:close()
end
end)
it("unique foreign works with dbless", function()
local config = [[
_format_version: "1.1"
unique_foreigns:
- name: name
unique_references:
- note: note
]]
local res = assert(client:send {
method = "POST",
path = "/config",
body = {
config = config,
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.res_status(201, res)
local res = assert(client:get("/unique-foreigns"))
local body = assert.res_status(200, res)
local foreigns = cjson.decode(body)
assert.equal(foreigns.data[1].name, "name")
local res = assert(client:get("/unique-references"))
local body = assert.res_status(200, res)
local references = cjson.decode(body)
assert.equal(references.data[1].note, "note")
assert.equal(references.data[1].unique_foreign.id, foreigns.data[1].id)
local declarative = require "kong.db.declarative"
local key = declarative.unique_field_key("unique_references", "", "unique_foreign",
foreigns.data[1].id, true)
local cmd = string.format(
[[resty --main-conf "lmdb_environment_path %s/%s;" spec/fixtures/dump_lmdb_key.lua %q]],
TEST_CONF.prefix, TEST_CONF.lmdb_environment_path, key)
local handle = io.popen(cmd)
local result = handle:read("*a")
handle:close()
assert.not_equals("", result, "empty result from unique lookup")
local cached_reference = assert(require("kong.db.declarative.marshaller").unmarshall(result))
assert.same(cached_reference, references.data[1])
local cache = {
get = function(_, k)
if k ~= "unique_references||unique_foreign:" .. foreigns.data[1].id then
return nil
end
return cached_reference
end
}
mocker.setup(finally, {
kong = {
core_cache = cache,
}
})
local _, db = helpers.get_db_utils("off", {}, {
"unique-foreign"
})
local i = 1
while true do
local n, v = debug.getupvalue(db.unique_references.strategy.select_by_field, i)
if not n then
break
end
if n == "select_by_key" then
local j = 1
while true do
local n, v = debug.getupvalue(v, j)
if not n then
break
end
if n == "kong" then
v.core_cache = cache
break
end
j = j + 1
end
break
end
i = i + 1
end
-- TODO: figure out how to mock LMDB in busted
-- local unique_reference, err, err_t = db.unique_references:select_by_unique_foreign({
-- id = foreigns.data[1].id,
-- })
-- assert.is_nil(err)
-- assert.is_nil(err_t)
-- assert.equal(references.data[1].id, unique_reference.id)
-- assert.equal(references.data[1].note, unique_reference.note)
-- assert.equal(references.data[1].unique_foreign.id, unique_reference.unique_foreign.id)
end)
end)
describe("Admin API #off worker_consistency=eventual", function()
local client
local WORKER_STATE_UPDATE_FREQ = 0.1
lazy_setup(function()
assert(helpers.start_kong({
database = "off",
lmdb_map_size = LMDB_MAP_SIZE,
worker_consistency = "eventual",
worker_state_update_frequency = WORKER_STATE_UPDATE_FREQ,
}))
end)
lazy_teardown(function()
helpers.stop_kong(nil, true)
end)
before_each(function()
client = assert(helpers.admin_client())
end)
after_each(function()
if client then
client:close()
end
end)
it("does not increase timer usage (regression)", function()
-- 1. configure a simple service
local res = assert(client:send {
method = "POST",
path = "/config",
body = helpers.unindent([[
_format_version: '1.1'
services:
- name: konghq
url: http://konghq.com
path: /
plugins:
- name: prometheus
]]),
headers = {
["Content-Type"] = "text/yaml"
},
})
assert.response(res).has.status(201)
-- 2. check the timer count
res = assert(client:send {
method = "GET",
path = "/metrics",
})
local res_body = assert.res_status(200, res)
local req1_pending_timers = assert.matches('kong_nginx_timers{state="pending"} %d+', res_body)
local req1_running_timers = assert.matches('kong_nginx_timers{state="running"} %d+', res_body)
req1_pending_timers = assert(tonumber(string.match(req1_pending_timers, "%d")))
req1_running_timers = assert(tonumber(string.match(req1_running_timers, "%d")))
-- 3. update the service
res = assert(client:send {
method = "POST",
path = "/config",
body = helpers.unindent([[
_format_version: '1.1'
services:
- name: konghq
url: http://konghq.com
path: /install#kong-community
plugins:
- name: prometheus
]]),
headers = {
["Content-Type"] = "text/yaml"
},
})
assert.response(res).has.status(201)
-- 4. check if timer count is still the same
res = assert(client:send {
method = "GET",
path = "/metrics",
})
local res_body = assert.res_status(200, res)
local req2_pending_timers = assert.matches('kong_nginx_timers{state="pending"} %d+', res_body)
local req2_running_timers = assert.matches('kong_nginx_timers{state="running"} %d+', res_body)
req2_pending_timers = assert(tonumber(string.match(req2_pending_timers, "%d")))
req2_running_timers = assert(tonumber(string.match(req2_running_timers, "%d")))
assert.equal(req1_pending_timers, req2_pending_timers)
assert.equal(req1_running_timers, req2_running_timers)
end)
end)