Kong Plugin: Easy Functional testing
By: Aurélien Lajoie
Tags: API, Backend, DevOps, Tdd

Iraty, Pyrenees — Navarre France — Véronique

We have seen on the two first articles how to set your env and how to deal with schema. Time to jump on the main part of this articles series, how to test the functional part. For some readers, this part can be seen as the only needed part.

If you start directly by this article you just need to have installed pongo (Kong is not needed at all), and you can checkout the repository.

Test the handler logic

The power of pongo will be seen in this part, instantiating kong with the right plugin configuration and kong configuration. We will also use a the mock server provided by kong spec/fixtures/custom_nginx.template and take a look on all the feature offered by this mock.

After the 01-schema_spec.lua , we will create a 02-handler_spec.lua file.

This file is starting with classic stuff, requiring the helpers, and defining the plugin name:

local helpers = require "spec.helpers"

local PLUGIN_NAME = "medium-test"

Then we will do a loop on the DB strategy, this will iterate on the default {“postgres", “cassandra"}

for _, strategy in helpers.each_strategy() do

You can have the temptation to only use one DB, the one you are using in production. But this is very easy to have both set and avoid bad surprise if you have to change your storage.

Then we will define classic busted functions:

lazy_setup to set up all the components,

to set up all the components, lazy_teardown to shutdown all the components,

to shutdown all the components, before_each and after_each that will be execute before and after each test.

Setup

First we need to get the database utility helpers and prepare the database for a test run:

local bp = helpers.get_db_utils(strategy, nil, { PLUGIN_NAME })

Then we have to configure kong, we will setup two routes for our tests:

local route1 = bp.routes:insert({

hosts = { "test1.com" },

})

local route2 = bp.routes:insert({

hosts = { "test2.com" },

})

And the core part of the configuration the plugin configuration with bp.plugins:insert

route1 will reject the queries when deny filters are matched, mark if a allow rule is matched. route2 will mark all action deny and allow.

Finally we have to start kong, with the strategy, the mock server and our plugin:

Mockup server

Lets have a look on the available endpoints of the mockup server. From spec/fictures/custom_nginx.template we can see:

/ws

Websocket echo server

Websocket echo server /get

Accepts a GET request and returns it in JSON format

Accepts a GET request and returns it in JSON format /xml

Returns a simple XML document

Returns a simple XML document /post

Accepts a POST request and returns it in JSON format

Accepts a POST request and returns it in JSON format /response-headers?:key=:val

Returns given response headers

Returns given response headers /cache/:n

Sets a Cache-Control header for n seconds

Sets a Cache-Control header for n seconds /anything

Accepts any request and returns it in JSON format

Accepts any request and returns it in JSON format /request

Alias to /anything

Alias to /anything /delay/:duration

Delay the response for <duration> seconds

Delay the response for <duration> seconds /basic-auth/:user/:pass

Performs HTTP basic authentication with the given credentials

Performs HTTP basic authentication with the given credentials /status/:code

Returns a response with the specified <status code>, accepts any request and returns it in JSON format

Returns a response with the specified <status code>, accepts any request and returns it in JSON format /stream/:num

Stream <num> chunks of JSON data via chunked Transfer Encoding

This endpoint helps a lot to write simple tesst, the interesting part is to get back in the response a json what was received by the mock server.

Teardown part is quite simple, just stop kong:

lazy_teardown(function()

helpers.stop_kong(nil, true)

end)

We need a http client, we define it as a global

local client

And we create it before the test and close it after (see below to get more information about this client)

before_each(function()

client = helpers.proxy_client()

end) after_each(function()

if client then client:close() end

end)

The strategy loop should look like this now:

We have every thing ready to do our tests. It is time to start our topic after all those preliminaries.

Bordeaux — France — Véronique

First functional test

We will use /status/200 path and targeting the route1 linked with the host test1.com with a simple GET query.

Our request should not be marked or rejected.

To do the request we are using the client previously set up:

client:send {

method = "GET",

path = "/status/200",

headers = {

["Host"] = "test1.com",

}

}

We add an assert to check if the client:send was successful and we retrieve the response:

local res = assert(client:send {

method = "GET",

path = "/status/200",

headers = {

["Host"] = "test1.com",

}

})

Then we need to check if the response status is a 200, and we have to retrieve the body of the answer. Good luck for us, once again, there is an helper for this assert.res_status

local body = assert.res_status(200, res)

And last check, we should not have an x-limit header. (To decode the Json we are using cjson)

assert.equal(nil, json.headers["x-limit"])

All this combined give:

First test with success

🎉 2 successes one for Cassandra one for PostgreSQL. \o/

Success — pixabay

To continue we will test with the x-source header set to different values:

We launch the test and:

Failure — pixabay

All our new tests are failing, this is normal as we have still not write anything into the handler.lua file. It is time to implement the code and have everything in green.

To test the behavior for the ip it is the same logic. You can find a full example of this plugins https://github.com/ManoManoTech/kong-plugin-mm-rate-limiting

95 tests in 9s

This way to do test allows to cover a lot of usage, and if you take a look a lot of plugins test are using this pattern.

But we can need more specific http mock server, responding something else than the query received.

Custom HTTP mock

When you want to test routing feature you want to check witch upstream server was hit, for that you want to configure different servers.

To illustrate that we will use the plugin kong-plugin-route-by-cookie made by Muhammad Redho Ayassa

Overview This Kong plugin allows you to route client request based on user cookies. It is inspired by Route By Header.

We have to test cookies and routing, quite interesting.

CookieMonster, Meme from Reddit

We have to use the fixtures option of the Kong start helper



--

--

--

-- -- @function start_kong-- @param env table with Kong configuration parameters (and values)-- @param tables list of database tables to truncate before starting-- @param preserve_prefix (boolean) if truthy, the prefix will not be cleaned before starting-- @param fixtures tables with fixtures, dns, http and stream mocks.

Fixtures allow us to create http mocks using nginx syntax, you can check nginx documentation if needed:

2 http Mock servers

Then we just start kong with this fixtures configuration, thanks to the helper:

assert(helpers.start_kong({

database = strategy,

nginx_conf = "spec/fixtures/custom_nginx.template",

plugins = "bundled," .. PLUGIN_NAME,

},nil, nil, fixtures))

And now we have two servers listening on port 16798 and 16799.

We add the plugin to route when the cookie foo is set to bar:

bp.plugins:insert {

name = PLUGIN_NAME,

route = { id = mainroute.id },

config = {

target_upstream = upstream1.name,

cookie_name="foo",

cookie_val="bar",

},

}

We have to be able to set the cookie value, and again it is quite easy. We can set the headers value of the request using options param of the client:get function:

http_client:get("path", [options])

To avoid to duplicate always the same code doing a query, checking of the return we can create a function.

This function set the targeted host,

local headers = {

host = host

}

set the cookie if a value is set,

if cookie then

headers.cookie = cookie

end

do the query, check the 200 status and retrieve the body of the response

local res = assert(client:get("/", { headers = headers }))

local body = assert.res_status(200, res)

And finally check if the receive answer is the expected one.

assert.equal(target, body)

With this function the test are very easy to write:

Coral Sea — Australia — Aurélien

Http client

We have used the helpers.proxy_client, this gives us a pre-configured http client for the Kong proxy port. It takes an optional param to set the timeout to use.

There is other pre-configured http clients quite useful:

proxy_ssl_client , a http client pre-configured for the Kong SSL proxy port

, a http client pre-configured for the Kong SSL proxy port admin_client , a pre-configured http client for the Kong admin port

, a pre-configured http client for the Kong admin port admin_ssl_client, a pre-configured http client for the Kong admin SSL port

The admin clients allow to test the Kong API, in a case of a plugins this is used to test the plugins configuration using the API for example, on the Kong proxy-cache plugin to test the admin API there is this test:

The http client offers nice methods to have more compact call, instead of using send and specifying the method there is the methods: “get”, “post”, “put”, “patch” and “delete”, with a first param the path and an optional param to set some options. To have a full description of available options please check the lua-resty-http documentation.

Conclusion

Writing tests for Kong plugins is at the end quite easy thanks to the good helpers provided by Kong. The helpers have good documentation into the source, not on the website. Adding on this Pongo you can easily run your test.

So no more excuse to not do TDD on your Kong plugins.