Files
2026-07-13 12:32:21 +08:00

731 lines
24 KiB
Lua

local helpers = require "spec.helpers"
local ssl_fixtures = require "spec.fixtures.ssl"
local cjson = require "cjson"
local constants = require "kong.constants"
local Errors = require "kong.db.errors"
local UUID_PATTERN = "%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x"
local strategies = {}
for _, strategy in helpers.each_strategy() do
table.insert(strategies, strategy)
end
table.insert(strategies, "off")
for _, strategy in pairs(strategies) do
describe("Admin API - Kong routes with strategy #" .. strategy, function()
local meta = require "kong.meta"
local client
lazy_setup(function()
helpers.get_db_utils(nil, {}) -- runs migrations
assert(helpers.start_kong {
database = strategy,
plugins = "bundled,reports-api,dummy",
pg_password = "hide_me"
})
client = helpers.admin_client(10000)
end)
lazy_teardown(function()
if client then client:close() end
helpers.stop_kong()
end)
describe("/", function()
it("returns headers with HEAD method", function()
local res1 = assert(client:send {
method = "GET",
path = "/"
})
local body = assert.res_status(200, res1)
assert.not_equal("", body)
local res2 = assert(client:send {
method = "HEAD",
path = "/"
})
local body = assert.res_status(200, res2)
assert.equal("", body)
res1.headers["Date"] = nil
res2.headers["Date"] = nil
res1.headers["X-Kong-Admin-Latency"] = nil
res2.headers["X-Kong-Admin-Latency"] = nil
assert.same(res1.headers, res2.headers)
end)
it("returns allow and CORS headers with OPTIONS method", function()
local res = assert(client:send {
method = "OPTIONS",
path = "/"
})
local body = assert.res_status(204, res)
assert.equal("", body)
assert.equal("GET, HEAD, OPTIONS", res.headers["Allow"])
assert.equal("GET, HEAD, OPTIONS", res.headers["Access-Control-Allow-Methods"])
assert.equal("Content-Type", res.headers["Access-Control-Allow-Headers"])
assert.equal("*", res.headers["Access-Control-Allow-Origin"])
assert.not_nil(res.headers["X-Kong-Admin-Latency"])
end)
it("returns Kong's version number, edition info and tagline", function()
local res = assert(client:send {
method = "GET",
path = "/"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal(meta._VERSION, json.version)
assert.equal(meta._VERSION:match("enterprise") and "enterprise" or "community", json.edition)
assert.equal("Welcome to kong", json.tagline)
end)
it("returns a UUID as the node_id", function()
local res = assert(client:send {
method = "GET",
path = "/"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.matches(UUID_PATTERN, json.node_id)
end)
it("response has the correct Server header", function()
local res = assert(client:send {
method = "GET",
path = "/"
})
assert.res_status(200, res)
assert.equal(meta._SERVER_TOKENS, res.headers.server)
assert.is_nil(res.headers.via) -- Via is only set for proxied requests
end)
it("returns 405 on invalid method", function()
local methods = {"POST", "PUT", "DELETE", "PATCH", "GEEEET"}
for i = 1, #methods do
local res = assert(client:send {
method = methods[i],
path = "/",
body = {}, -- tmp: body to allow POST/PUT to work
headers = {["Content-Type"] = "application/json"}
})
local body = assert.response(res).has.status(405)
local json = cjson.decode(body)
assert.same({ message = "Method not allowed" }, json)
end
end)
it("exposes the node's configuration", function()
local res = assert(client:send {
method = "GET",
path = "/"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_table(json.configuration)
end)
it("enabled_in_cluster property is an array", function()
local res = assert(client:send {
method = "GET",
path = "/"
})
local body = assert.res_status(200, res)
assert.matches('"enabled_in_cluster":[]', body, nil, true)
end)
it("obfuscates sensitive settings from the configuration", function()
local res = assert(client:send {
method = "GET",
path = "/"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_string(json.configuration.pg_password)
assert.not_equal("hide_me", json.configuration.pg_password)
end)
it("returns process ids", function()
local res = assert(client:send {
method = "GET",
path = "/",
})
local body = assert.response(res).has.status(200)
local json = cjson.decode(body)
assert.is_table(json.pids)
assert.matches("%d+", json.pids.master)
for _, v in pairs(json.pids.workers) do
assert.matches("%d+", v)
end
end)
it("does not return PRNG seeds", function()
local res = assert(client:send {
method = "GET",
path = "/",
})
local body = assert.response(res).has.status(200)
local json = cjson.decode(body)
assert.is_nil(json.prng_seeds)
end)
end)
describe("/endpoints", function()
it("only returns base, plugin, and custom-plugin endpoints", function()
local res = assert(client:send {
method = "GET",
path = "/endpoints"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
local function find(endpoint)
for _, ep in ipairs(json.data) do
if ep == endpoint then
return true
end
end
return nil, ("endpoint '%s' not found in list of endpoints from " ..
"`/endpoints`"):format(endpoint)
end
assert(find("/plugins")) -- Kong base endpoint
assert(find("/basic-auths/{basicauth_credentials}")) -- Core plugin endpoint
assert(find("/reports/send-ping")) -- Custom plugin "reports-api"
end)
end)
describe("/status", function()
local empty_config_hash = constants.DECLARATIVE_EMPTY_CONFIG_HASH
it("returns status info", function()
local res = assert(client:send {
method = "GET",
path = "/status"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_table(json.server)
assert.is_number(json.server.connections_accepted)
assert.is_number(json.server.connections_active)
assert.is_number(json.server.connections_handled)
assert.is_number(json.server.connections_reading)
assert.is_number(json.server.connections_writing)
assert.is_number(json.server.connections_waiting)
assert.is_number(json.server.total_requests)
if strategy == "off" then
assert.is_equal(empty_config_hash, json.configuration_hash) -- all 0 in DBLESS mode until configuration is applied
assert.is_nil(json.database)
else
assert.is_nil(json.configuration_hash) -- not present in DB mode
assert.is_table(json.database)
assert.is_boolean(json.database.reachable)
end
end)
it("returns status info including a configuration_hash in DBLESS mode if an initial configuration has been provided #off", function()
-- push an initial configuration so that a configuration_hash will be present
local postres = assert(client:send {
method = "POST",
path = "/config",
body = {
config = [[
_format_version: "1.1"
services:
- host: "konghq.com"
]],
},
headers = {
["Content-Type"] = "application/json"
}
})
assert.res_status(201, postres)
-- verify the status endpoint now includes a value (other than the default) for the configuration_hash
local res = assert(client:send {
method = "GET",
path = "/status"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
if strategy == "off" then
assert.is_nil(json.database)
else
assert.is_table(json.database)
assert.is_boolean(json.database.reachable)
end
assert.is_table(json.server)
assert.is_number(json.server.connections_accepted)
assert.is_number(json.server.connections_active)
assert.is_number(json.server.connections_handled)
assert.is_number(json.server.connections_reading)
assert.is_number(json.server.connections_writing)
assert.is_number(json.server.connections_waiting)
assert.is_number(json.server.total_requests)
assert.is_string(json.configuration_hash)
assert.equal(32, #json.configuration_hash)
assert.is_not_equal(empty_config_hash, json.configuration_hash)
end)
it("database.reachable is `true` when DB connection is healthy", function()
-- In this test, we know our DB is reachable because it must be
-- so in our test suite. Ideally, a test when the DB isn't reachable
-- should be provided (start Kong, kill DB, request `/status`),
-- but this isn't currently possible in our test suite.
-- Additionally, let's emphasize that we only test DB connection, not
-- the health of said DB itself.
local res = assert(client:send {
method = "GET",
path = "/status"
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
if strategy == "off" then
assert.is_nil(json.database)
else
assert.is_true(json.database.reachable)
end
end)
describe("memory stats", function()
it("returns lua_shared_dicts memory stats", function()
local res = assert(client:send {
method = "GET",
path = "/status",
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
local lua_shared_dicts = json.memory.lua_shared_dicts
assert.matches("%d+%.%d+ MiB", lua_shared_dicts.kong.capacity)
assert.matches("%d+%.%d+ MiB", lua_shared_dicts.kong.allocated_slabs)
assert.matches("%d+%.%d+ MiB", lua_shared_dicts.kong_db_cache.capacity)
assert.matches("%d+%.%d+ MiB", lua_shared_dicts.kong_db_cache.allocated_slabs)
end)
it("returns workers Lua VM allocated memory", function()
local res = assert(client:send {
method = "GET",
path = "/status",
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
local workers_lua_vms = json.memory.workers_lua_vms
for _, worker in ipairs(workers_lua_vms) do
assert.matches("%d+%.%d+ MiB", worker.http_allocated_gc)
assert.matches("%d+", worker.pid)
assert.is_number(worker.pid)
end
end)
it("returns workers in ascending PID values", function()
do
-- restart with 2 workers
if client then
client:close()
end
helpers.stop_kong()
assert(helpers.start_kong {
nginx_worker_processes = 2,
})
client = helpers.admin_client(10000)
end
finally(function()
-- restart with default number of workers
if client then
client:close()
end
helpers.stop_kong()
assert(helpers.start_kong())
client = helpers.admin_client(10000)
end)
local res = assert(client:send {
method = "GET",
path = "/status",
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
local workers_lua_vms = json.memory.workers_lua_vms
if #workers_lua_vms > 1 then
for i = 1, #workers_lua_vms do
if workers_lua_vms[i + 1] then
assert.gt(workers_lua_vms[i].pid, workers_lua_vms[i + 1].pid)
end
end
end
end)
it("accepts a 'unit' querystring argument", function()
local res = assert(client:send {
method = "GET",
path = "/status?unit=k",
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
local lua_shared_dicts = json.memory.lua_shared_dicts
local workers_lua_vms = json.memory.workers_lua_vms
assert.matches("%d+%.%d+ KiB", lua_shared_dicts.kong.capacity)
assert.matches("%d+%.%d+ KiB", workers_lua_vms[1].http_allocated_gc)
end)
it("when unit is bytes, returned properties are numbers", function()
local res = assert(client:send {
method = "GET",
path = "/status?unit=b",
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
local lua_shared_dicts = json.memory.lua_shared_dicts
local workers_lua_vms = json.memory.workers_lua_vms
assert.matches("%d+", lua_shared_dicts.kong.capacity)
assert.is_number(lua_shared_dicts.kong.capacity)
assert.matches("%d+", lua_shared_dicts.kong.allocated_slabs)
assert.is_number(lua_shared_dicts.kong.allocated_slabs)
assert.matches("%d+", workers_lua_vms[1].http_allocated_gc)
assert.is_number(workers_lua_vms[1].http_allocated_gc)
end)
it("accepts a 'scale' querystring argument", function()
local res = assert(client:send {
method = "GET",
path = "/status?scale=3",
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
local lua_shared_dicts = json.memory.lua_shared_dicts
local workers_lua_vms = json.memory.workers_lua_vms
assert.matches("%d+%.%d%d%d MiB", lua_shared_dicts.kong.capacity)
assert.matches("%d+%.%d%d%d MiB", workers_lua_vms[1].http_allocated_gc)
end)
it("returns HTTP 400 on invalid 'unit' querystring parameter", function()
local res = assert(client:send {
method = "GET",
path = "/status?unit=V",
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.equal("invalid unit 'V' (expected 'k/K', 'm/M', or 'g/G')",
json.message)
end)
end)
end)
describe("/schemas/:entity", function()
it("returns the schema of all DB entities", function()
for _, dao in pairs(helpers.db.daos) do
local res = assert(client:send {
method = "GET",
path = "/schemas/" .. dao.schema.name,
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_table(json.fields)
assert.is_table(json.entity_checks)
end
end)
it("returns 404 on a missing entity", function()
local res = assert(client:send {
method = "GET",
path = "/schemas/not-present",
})
local body = assert.res_status(404, res)
local json = cjson.decode(body)
assert.same({ message = "No entity named 'not-present'" }, json)
end)
it("does not return schema of a foreign key", function()
local res = assert(client:send {
method = "GET",
path = "/schemas/routes",
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
for _, field in pairs(json.fields) do
if next(field) == "service" then
local fdata = field["service"]
assert.is_nil(fdata.schema)
end
end
end)
end)
describe("/schemas/vaults/:name", function()
it("returns schema of all vaults", function()
for _, vault in ipairs({"env"}) do
local res = assert(client:send {
method = "GET",
path = "/schemas/vaults/" .. vault,
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_table(json.fields)
end
end)
it("returns 404 on a non-existent vault", function()
local res = assert(client:send {
method = "GET",
path = "/schemas/vaults/not-present",
})
local body = assert.res_status(404, res)
local json = cjson.decode(body)
assert.same({ message = "No vault named 'not-present'" }, json)
end)
it("does not return 405 on /schemas/vaults/validate", function()
local res = assert(client:send {
method = "POST",
path = "/schemas/vaults/validate",
})
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.same("schema violation (name: required field missing)", json.message)
end)
end)
describe("/schemas/:entity", function()
it("returns schema of all plugins", function()
for plugin, _ in pairs(helpers.test_conf.loaded_plugins) do
local res = assert(client:send {
method = "GET",
path = "/schemas/plugins/" .. plugin,
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_table(json.fields)
end
end)
it("returns 404 on a non-existent plugin", function()
local res = assert(client:send {
method = "GET",
path = "/schemas/plugins/not-present",
})
local body = assert.res_status(404, res)
local json = cjson.decode(body)
assert.same({ message = "No plugin named 'not-present'" }, json)
end)
it("returns information about a deprecated field", function()
local res = assert(client:send {
method = "GET",
path = "/schemas/plugins/dummy",
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.is_table(json.fields)
local found = false
for _, f in ipairs(json.fields) do
local config_fields = f.config and f.config.fields
for _, cf in ipairs(config_fields or {}) do
local deprecation = cf.old_field and cf.old_field.deprecation
if deprecation then
assert.is_string(deprecation.message)
assert.is_number(deprecation.old_default)
assert.is_string(deprecation.removal_in_version)
found = true
end
end
end
assert(found)
end)
end)
describe("/schemas/:db_entity_name/validate", function()
it("returns 200 on a valid schema", function()
local res = assert(client:post("/schemas/services/validate", {
body = { host = "example.com" },
headers = {["Content-Type"] = "application/json"}
}))
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal("schema validation successful", json.message)
end)
it("returns 200 on certificates schema with snis", function()
local res = assert(client:post("/schemas/certificates/validate", {
body = {
cert = ssl_fixtures.cert,
key = ssl_fixtures.key,
snis = {"a", "b", "c" },
},
headers = { ["Content-Type"] = "application/json" }
}))
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal("schema validation successful", json.message)
end)
it("returns 400 on certificates schema with invalid snis", function()
local res = assert(client:post("/schemas/certificates/validate", {
body = {
cert = ssl_fixtures.cert,
key = ssl_fixtures.key,
snis = {"120.0.9.32:90" },
},
headers = { ["Content-Type"] = "application/json" }
}))
local body = assert.res_status(400, res)
local json = cjson.decode(body)
local expected_body = {
fields= {
snis= { "must not be an IP" }
},
name= "schema violation",
message= "schema violation (snis.1: must not be an IP)",
code= Errors.codes.SCHEMA_VIOLATION,
}
assert.same(expected_body, json)
end)
it("returns 200 on a valid plugin schema", function()
local res = assert(client:post("/schemas/plugins/validate", {
body = {
name = "key-auth",
config = {
key_names = { "foo", "bar" },
hide_credentials = true,
},
},
headers = {["Content-Type"] = "application/json"}
}))
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal("schema validation successful", json.message)
end)
it("returns 400 on an invalid plugin subschema", function()
local res = assert(client:post("/schemas/plugins/validate", {
body = {
name = "key-auth",
config = {
keys = "foo",
},
},
headers = {["Content-Type"] = "application/json"}
}))
local body = assert.res_status(400, res)
local json = cjson.decode(body)
assert.equal("schema violation", json.name)
end)
it("returns 200 on a valid plugin schema which contains dot in the key of custom_fields_by_lua", function()
local res = assert(client:post("/schemas/plugins/validate", {
body = {
name = "file-log",
config = {
path = "tmp/test",
custom_fields_by_lua = {
new_field = "return 123",
["request.headers.myheader"] = "return nil",
},
},
},
headers = {["Content-Type"] = "application/json"}
}))
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal("schema validation successful", json.message)
end)
end)
describe("/non-existing", function()
it("returns 404 with HEAD", function()
local res = assert(client:send {
method = "HEAD",
path = "/non-existing"
})
local body = assert.res_status(404, res)
assert.equal("", body)
end)
it("returns 404 with OPTIONS", function()
local res = assert(client:send {
method = "OPTIONS",
path = "/non-existing"
})
local body = assert.res_status(404, res)
local json = cjson.decode(body)
assert.equal("Not found", json.message)
end)
it("returns 404 with GET", function()
local res = assert(client:send {
method = "GET",
path = "/non-existing"
})
local body = assert.res_status(404, res)
local json = cjson.decode(body)
assert.equal("Not found", json.message)
end)
it("returns 404 with POST", function()
local res = assert(client:send {
method = "POST",
path = "/non-existing"
})
local body = assert.res_status(404, res)
local json = cjson.decode(body)
assert.equal("Not found", json.message)
end)
it("returns 404 with PUT", function()
local res = assert(client:send {
method = "PUT",
path = "/non-existing"
})
local body = assert.res_status(404, res)
local json = cjson.decode(body)
assert.equal("Not found", json.message)
end)
it("returns 404 with DELETE", function()
local res = assert(client:send {
method = "DELETE",
path = "/non-existing"
})
local body = assert.res_status(404, res)
local json = cjson.decode(body)
assert.equal("Not found", json.message)
end)
end)
end)
end
describe("Admin API - node ID is set correctly", function()
local client
local input_node_id = "592e1c2b-6678-45aa-80f9-78cfb29f5e31"
lazy_setup(function()
helpers.get_db_utils(nil, {}) -- runs migrations
assert(helpers.start_kong {
node_id = input_node_id
})
client = helpers.admin_client(10000)
end)
lazy_teardown(function()
if client then client:close() end
helpers.stop_kong()
end)
it("returns node-id set in configuration", function()
local res1 = assert(client:send {
method = "GET",
path = "/"
})
local body = assert.res_status(200, res1)
local json = cjson.decode(body)
assert.equal(input_node_id, json.node_id)
end)
end)