Lua Test Framework
This page is the complete reference for Nauthilus Lua test mode.
Goal: after reading this page, you can create deterministic Lua tests for all supported callback types and module mocks.
What test mode does
--test-lua executes one Lua script in an isolated test runtime.
It preloads:
- all Nauthilus mock modules (
nauthilus_*) - built-in helper table (
nauthilus_builtin) gopher-lua-libsglua_cryptoglua_http- real
nauthilus_redisbacked by in-memoryminiredis - an in-memory
dbmock (require("db"))
Important:
- There is no standalone logging module in test mode.
- For logs and status messages, use builtin functions:
nauthilus_builtin.custom_log_add(key, value)nauthilus_builtin.status_message_set(message)
It can validate:
- callback result (
expected_output) - strict call sequence (
expected_calls) per module
CLI reference
go run ./server --test-lua <script.lua> --test-callback <type> [--test-mock <fixture.json>]
Flags:
--test-lua: path to Lua script--test-callback: one offilter|feature|action|backend|hook--test-mock: optional JSON fixture file
Exit codes:
0: test passed- non-zero: callback/runtime/assertion failure
Callback contract
Required global Lua functions by callback type:
filter:nauthilus_call_filter(request)-> integerfeature:nauthilus_call_feature(request)-> booleanaction:nauthilus_call_action(request)-> boolean, integer, ornilbackend:nauthilus_backend_verify_password(request)-> table or userdatahook:nauthilus_run_hook()-> any (test runtime stores boolean-like result as action result)
Action return semantics:
- boolean: used directly
- integer:
0means success, any other value means failure nil: treated as success
Request object (request) in test runtime
If context exists in fixture, fields are mapped into request:
username,passwordclient_ip,client_port,client_host,client_idlocal_ip,local_portservice,protocol,user_agent,sessiondebug,no_auth,authenticated,user_foundaccount,unique_user_id,display_name,status_messagebrute_force_count
Additional derived fields:
log_level(debugwhencontext.debug=true, elseinfo)log_format(json)loggingtable withlog_level,log_format
Fixture schema (top-level)
{
"context": { },
"redis": { },
"ldap": { },
"backend": { },
"misc": { },
"password": { },
"soft_whitelist": { },
"mail": { },
"dns": { },
"opentelemetry": { },
"brute_force": { },
"psnet": { },
"prometheus": { },
"util": { },
"cache": { },
"db": { },
"backend_result": { },
"http_request": { },
"http_response": { },
"expected_output": { }
}
All blocks are optional.
expected_calls reference (common)
Most module blocks support:
"expected_calls": [
{ "method": "<name>", "arg_contains": "<substring>" }
]
Rules:
- call order is strict
- method names are compared case-insensitively
arg_containsis optional and matched case-insensitively- missing calls fail
- extra calls fail
DB uses a dedicated expected-call structure (see DB section).
Complete module reference
context
Fields:
usernamestringpasswordstringclient_ipstringclient_portstringclient_hoststringclient_idstringlocal_ipstringlocal_portstringservicestringprotocolstringuser_agentstringsessionstringdebugboolno_authboolauthenticatedbooluser_foundboolaccountstringunique_user_idstringdisplay_namestringstatus_messagestringattributesobject (map[string]string)brute_force_countintegerexpected_callscommon format
Supported expected_calls.method values:
context_setcontext_getcontext_delete
Lua example:
local ctx = require("nauthilus_context")
ctx.context_set("trace_id", "abc")
local v = ctx.context_get("trace_id")
JSON example:
{
"context": {
"username": "alice",
"client_ip": "192.0.2.10",
"expected_calls": [
{"method": "context_set", "arg_contains": "trace_id"},
{"method": "context_get", "arg_contains": "trace_id"}
]
}
}
redis
Fields:
initial_dataobject:stringsobject (map[string]string)hashesobject (map[string]map[string]string)setsobject (map[string][]string)listsobject (map[string][]string)zsetsobject (map[string][]object) with entries:memberstringscorenumber
hyperloglogsobject (map[string][]string)ttl_secondsobject (map[string]int64) applied viaEXPIRE
expected_callscommon format
Supported expected_calls.method values:
register_redis_pool,get_redis_connectionredis_ping,redis_get,redis_set,redis_incr,redis_del,redis_rename,redis_expire,redis_existsredis_encrypt,redis_decrypt,redis_is_encryption_enabledredis_run_script,redis_upload_script,redis_pipelineredis_mget,redis_mset,redis_keys,redis_scanredis_hget,redis_hset,redis_hdel,redis_hlen,redis_hgetall,redis_hmget,redis_hincrby,redis_hincrbyfloat,redis_hexistsredis_zadd,redis_zrem,redis_zrank,redis_zrange,redis_zrevrange,redis_zrangebyscore,redis_zremrangebyscore,redis_zremrangebyrank,redis_zcount,redis_zscore,redis_zrevrank,redis_zincrbyredis_lpush,redis_rpush,redis_lpop,redis_rpop,redis_lrange,redis_llenredis_pfadd,redis_pfcount,redis_pfmergeredis_sadd,redis_sismember,redis_smembers,redis_srem,redis_scard
Lua example:
local r = require("nauthilus_redis")
local ok, set_err = r.redis_set("default", "counter", "1", 0)
if set_err then error(set_err) end
local v, get_err = r.redis_get("default", "counter")
if get_err then error(get_err) end
JSON example:
{
"redis": {
"initial_data": {
"strings": {
"counter": "1",
"tenant:alice": "acme"
},
"hashes": {
"profile:alice": {
"mail": "alice@example.com"
}
},
"sets": {
"roles:alice": ["admin", "imap"]
},
"lists": {
"mailbox:recent:alice": ["msg1", "msg2"]
},
"zsets": {
"scoreboard": [
{"member": "alice", "score": 10},
{"member": "bob", "score": 20}
]
},
"hyperloglogs": {
"unique_users": ["alice", "bob", "alice"]
},
"ttl_seconds": {
"counter": 300
}
},
"expected_calls": [
{"method": "redis_set", "arg_contains": "counter"},
{"method": "redis_get", "arg_contains": "counter"}
]
}
}
ldap
Fields:
search_resultobject (map[string][]string)search_errorstringmodify_okboolmodify_errorstringendpoint_hoststringendpoint_portintegerendpoint_errorstringexpected_callscommon format
Supported expected_calls.method values:
ldap_searchldap_modifyldap_endpoint
Lua example:
local ldap = require("nauthilus_ldap")
local attrs, err = ldap.ldap_search("dc=example,dc=com", "(uid=alice)", {"mail"})
JSON example:
{
"ldap": {
"search_result": {
"mail": ["alice@example.com"]
},
"endpoint_host": "ldap.internal",
"endpoint_port": 389,
"expected_calls": [
{"method": "ldap_endpoint"},
{"method": "ldap_search"}
]
}
}
backend
Fields:
backend_serversarray of objects:protocolstringhoststringportintegerrequest_uristringtest_usernamestringtest_passwordstringhaproxy_v2booltlsbooltls_skip_verifybooldeep_checkbool
expected_callscommon format
Supported expected_calls.method values:
get_backend_serversselect_backend_serverapply_backend_resultremove_from_backend_result
Lua example:
local backend = require("nauthilus_backend")
local servers = backend.get_backend_servers()
backend.select_backend_server(servers[1].host, servers[1].port)
JSON example:
{
"backend": {
"backend_servers": [
{"protocol": "imap", "host": "10.0.0.20", "port": 993, "tls": true}
],
"expected_calls": [
{"method": "get_backend_servers"},
{"method": "select_backend_server", "arg_contains": "10.0.0.20"}
]
},
"expected_output": {
"used_backend_address": "10.0.0.20",
"used_backend_port": 993,
"error_expected": false
}
}
backend_result
Fields:
authenticatedbooluser_foundboolaccount_fieldstringtotp_secretstringtotp_recoveryarray of stringsunique_user_idstringdisplay_namestringattributesobject (map[string]string)expected_callscommon format
Supported expected_calls.method values:
newauthenticateduser_foundaccount_fieldtotp_secret_fieldtotp_recovery_fieldunique_user_id_fielddisplay_name_fieldwebauthn_credentialsattributes
Lua example:
local br = require("nauthilus_backend_result")
local r = br.new()
r.authenticated(true)
r.user_found(true)
r.account_field("alice")
return r
JSON example:
{
"backend_result": {
"authenticated": true,
"user_found": true,
"account_field": "alice",
"expected_calls": [
{"method": "new"},
{"method": "authenticated"},
{"method": "user_found"},
{"method": "account_field"}
]
}
}
db
Fields:
open_errorstringexec_errorstringquery_errorstringdeclarative_modeboolexpected_callsarray of DB call objects:methodstring (open|stmt|exec|query|close)query_containsstring (optional)rows_affectedint64 (optional, forexec)last_insert_idint64 (optional, forexec)columnsarray of strings (optional, forquery)rowsarray of row arrays (optional, forquery)
Behavior notes:
require("db")always uses this in-memory mock in test mode.declarative_mode=truekeeps behavior fixture-driven without relying on internal expectation plumbing.- if
columnsomitted butrowspresent, fallback names are generated (col_1,col_2, ...).
Lua example:
local db = require("db")
local conn, err = db.open("mysql", "mock://")
if err then return nil end
conn:exec("insert into users(name) values (?)", "alice")
local res = conn:query("select id, name from users")
conn:close()
return res
JSON example:
{
"db": {
"declarative_mode": true,
"expected_calls": [
{"method": "open"},
{"method": "exec", "query_contains": "insert into", "rows_affected": 1, "last_insert_id": 7},
{"method": "query", "query_contains": "select", "columns": ["id", "name"], "rows": [[7, "alice"]]},
{"method": "close"}
]
}
}
http_request
Fields:
methodstringpathstringheadersobject (map[string]string)bodystringexpected_callscommon format
Supported expected_calls.method values:
get_http_methodget_http_pathget_http_bodyget_http_headerget_http_request_header
Lua example:
local req = require("nauthilus_http_request")
local method = req.get_http_method()
local auth = req.get_http_header("Authorization")
JSON example:
{
"http_request": {
"method": "GET",
"path": "/health",
"headers": {"Authorization": "Bearer token"},
"expected_calls": [
{"method": "get_http_method"},
{"method": "get_http_header", "arg_contains": "Authorization"}
]
}
}
http_response
Fields:
status_codeinteger (reserved for fixtures; current mock functions do not consume this field directly)headersobject (map[string]string) (reserved)bodystring (reserved)expected_callscommon format
Supported expected_calls.method values:
htmlset_http_response_headerjson
Lua example:
local resp = require("nauthilus_http_response")
resp.set_http_response_header("X-Test", "1")
resp.json(resp.STATUS_OK, {ok=true})
JSON example:
{
"http_response": {
"expected_calls": [
{"method": "set_http_response_header", "arg_contains": "X-Test"},
{"method": "json", "arg_contains": "200"}
]
}
}
dns
Fields:
lookup_resultobject (map[string]any)expected_callscommon format
Supported expected_calls.method values:
lookup
Lua example:
local dns = require("nauthilus_dns")
local a = dns.lookup("example.com")
JSON example:
{
"dns": {
"lookup_result": {
"example.com": ["93.184.216.34"]
},
"expected_calls": [
{"method": "lookup", "arg_contains": "example.com"}
]
}
}
opentelemetry
Fields:
expected_callscommon format
Supported expected_calls.method values:
tracerdefault_tracerstart_spanset_attributesrecord_errorset_statusfinish
Lua example:
local otel = require("nauthilus_opentelemetry")
local t = otel.default_tracer()
local span = t.start_span("lua_test")
span.set_attributes({k="v"})
span.finish()
JSON example:
{
"opentelemetry": {
"expected_calls": [
{"method": "default_tracer"},
{"method": "start_span"},
{"method": "set_attributes"},
{"method": "finish"}
]
}
}
brute_force
Fields:
is_blockedboolincrement_byintegerexpected_callscommon format
Supported expected_calls.method values:
is_blockedincrement
Lua example:
local bf = require("nauthilus_brute_force")
if not bf.is_blocked() then
bf.increment()
end
JSON example:
{
"brute_force": {
"is_blocked": false,
"increment_by": 3,
"expected_calls": [
{"method": "is_blocked"},
{"method": "increment"}
]
}
}
psnet
Fields:
statsobject (map[string]any)expected_callscommon format
Supported expected_calls.method values:
get_stats
Lua example:
local psnet = require("nauthilus_psnet")
local s = psnet.get_stats("imap://10.0.0.20:993")
JSON example:
{
"psnet": {
"stats": {"connections": 5, "latency_ms": 12},
"expected_calls": [
{"method": "get_stats", "arg_contains": "imap://"}
]
}
}
prometheus
Fields:
expected_callscommon format
Supported expected_calls.method values:
create_summary_veccreate_counter_veccreate_histogram_veccreate_gauge_vecincrement_counterincrement_gaugedecrement_gaugestart_histogram_timerstart_summary_timerstop_timer
Lua example:
local p = require("nauthilus_prometheus")
local t = p.start_histogram_timer("auth_duration", {"imap"})
p.stop_timer(t)
JSON example:
{
"prometheus": {
"expected_calls": [
{"method": "start_histogram_timer"},
{"method": "stop_timer"}
]
}
}
misc
Fields:
expected_callscommon format
Supported expected_calls.method values:
get_country_namewait_randomscoped_ip
Lua example:
local misc = require("nauthilus_misc")
local country, err = misc.get_country_name("DE")
JSON example:
{
"misc": {
"expected_calls": [
{"method": "get_country_name", "arg_contains": "DE"}
]
}
}
password
Fields:
compare_resultboolpolicy_resultboolgenerated_hashstringexpected_callscommon format
Supported expected_calls.method values:
compare_passwordscheck_password_policygenerate_password_hash
Lua example:
local pw = require("nauthilus_password")
local ok = pw.compare_passwords("hash", "secret")
JSON example:
{
"password": {
"compare_result": true,
"generated_hash": "mock$argon2$...",
"expected_calls": [
{"method": "compare_passwords"}
]
}
}
soft_whitelist
Fields:
entriesobject (map[string][]string), key format in mock:<feature>:<username>expected_callscommon format
Supported expected_calls.method values:
soft_whitelist_setsoft_whitelist_getsoft_whitelist_delete
Lua example:
local sw = require("nauthilus_soft_whitelist")
sw.soft_whitelist_set("alice", "192.0.2.0/24", "geo")
local list = sw.soft_whitelist_get("alice", "geo")
JSON example:
{
"soft_whitelist": {
"entries": {
"geo:alice": ["192.0.2.0/24"]
},
"expected_calls": [
{"method": "soft_whitelist_get", "arg_contains": "alice:geo"}
]
}
}
mail
Fields:
send_errorstringexpected_callscommon format
Supported expected_calls.method values:
send_mail
Lua example:
local mail = require("nauthilus_mail")
mail.send_mail({server="smtp.internal", from="a@x", to="b@y", subject="Test", body="ok"})
JSON example:
{
"mail": {
"expected_calls": [
{"method": "send_mail", "arg_contains": "smtp.internal"}
]
}
}
util
Fields:
envsobject (map[string]string)expected_callscommon format
Supported expected_calls.method values:
getenvprint_resultis_tabletable_lengthis_string
Lua example:
local u = require("nauthilus_util")
local mode = u.getenv("MODE", "dev")
JSON example:
{
"util": {
"envs": {"MODE": "prod"},
"expected_calls": [
{"method": "getenv", "arg_contains": "MODE"}
]
}
}
cache
Fields:
entriesobject (map[string]any)expected_callscommon format
Supported expected_calls.method values:
cache_setcache_getcache_deletecache_existscache_updatecache_keyscache_sizecache_flushcache_pushcache_pop_all
Lua example:
local c = require("nauthilus_cache")
c.cache_set("tenant:alice", "acme")
local v = c.cache_get("tenant:alice")
JSON example:
{
"cache": {
"entries": {"tenant:alice": "acme"},
"expected_calls": [
{"method": "cache_get", "arg_contains": "tenant:alice"}
]
}
}
Builtin table (nauthilus_builtin)
The test runtime provides the global builtin table used in production scripts.
Commonly used functions:
nauthilus_builtin.custom_log_add(key, value): appends structured test log outputnauthilus_builtin.status_message_set(message): records a status message in test output
Assertions:
custom_log_add(...)->expected_output.logs_contain/logs_not_containstatus_message_set(...)->expected_output.status_message_contain/status_message_not_contain
expected_output reference
Fields:
filter_resultintfeature_resultboolaction_resultboolbackend_resultboolbackend_authenticatedboolbackend_user_foundboolbackend_account_fieldstringbackend_display_namestringbackend_unique_user_idstringused_backend_addressstringused_backend_portintstatus_message_containarray of stringsstatus_message_not_containarray of stringslogs_containarray of stringslogs_not_containarray of stringserror_expectedbool
Example:
{
"expected_output": {
"feature_result": true,
"status_message_not_contain": ["Access denied"],
"logs_contain": ["policy accepted"],
"logs_not_contain": ["panic"],
"error_expected": false
}
}
Full end-to-end example
Lua script (example_feature.lua):
local ctx = require("nauthilus_context")
local redis = require("nauthilus_redis")
function nauthilus_call_feature(request)
local user = ctx.context_get("username") or request.username
local key = "tenant:" .. user
local tenant, err = redis.redis_get("default", key)
if err ~= nil then
nauthilus_builtin.custom_log_add("tenant_error", tostring(err))
return false
end
if tenant == nil then
nauthilus_builtin.custom_log_add("tenant_status", "missing")
return false
end
nauthilus_builtin.custom_log_add("tenant_status", "found")
nauthilus_builtin.custom_log_add("tenant_value", tostring(tenant))
return true
end
Fixture (example_feature_test.json):
{
"context": {
"username": "alice",
"expected_calls": [
{"method": "context_get", "arg_contains": "username"}
]
},
"redis": {
"initial_data": {
"strings": {
"tenant:alice": "acme"
}
},
"expected_calls": [
{"method": "redis_get", "arg_contains": "tenant:alice"}
]
},
"expected_output": {
"feature_result": true,
"logs_contain": ["tenant_status: found", "tenant_value: acme"],
"error_expected": false
}
}
Run:
go run ./server --test-lua example_feature.lua --test-callback feature --test-mock example_feature_test.json
Plugin regression suite
Repository fixtures for core plugins:
testdata/lua/plugins/*.json- wrappers:
testdata/lua/plugins/*_wrapper.lua
Run all plugin tests:
./scripts/run-lua-plugin-tests.sh
CI pattern
set -euo pipefail
./scripts/run-lua-plugin-tests.sh
go run ./server --test-lua testdata/lua/example_filter.lua --test-callback filter --test-mock testdata/lua/filter_test.json
go run ./server --test-lua testdata/lua/example_feature.lua --test-callback feature --test-mock testdata/lua/feature_test.json
go run ./server --test-lua testdata/lua/example_action.lua --test-callback action --test-mock testdata/lua/action_test.json
go run ./server --test-lua testdata/lua/example_backend.lua --test-callback backend --test-mock testdata/lua/backend_test.json
go run ./server --test-lua testdata/lua/example_hook.lua --test-callback hook --test-mock testdata/lua/hook_test.json
Troubleshooting
function not found: ensure script defines the callback function matching--test-callback.method mismatchinexpected_calls: check exact method name and order.requires an active request binding: ensure you call Redis functions through test runtime callback functions (nauthilus_call_*), not at top-level script load time.query mismatchin DB: adjustquery_containsto real SQL substring.logs_containmismatch: check exact custom log output (including prefixes/content).status_message_containmismatch: verify script really callsnauthilus_builtin.status_message_set(...).