return function(suite, test, skip, assert_eq, assert_true, assert_nil, assert_not_nil)
local client_mod = require("beachcomber.client")
local Client = client_mod.Client
local Result = client_mod.Result
local json = require("beachcomber.json")
suite("Result accessors")
test("is_hit returns false on miss", function()
local r = Result.new({ ok = true })
assert_true(not r:is_hit(), "empty response should be a miss")
assert_nil(r.data)
assert_eq(r.age_ms, 0)
assert_eq(r.stale, false)
end)
test("is_hit returns true on hit", function()
local r = Result.new({ ok = true, data = "main", age_ms = 500, stale = false })
assert_true(r:is_hit())
assert_eq(r.data, "main")
assert_eq(r.age_ms, 500)
assert_eq(r.stale, false)
end)
test("is_hit true for numeric 0 data", function()
local r = Result.new({ ok = true, data = 0, age_ms = 10, stale = false })
assert_true(r:is_hit())
end)
test("is_hit true for boolean false data", function()
local r = Result.new({ ok = true, data = false, age_ms = 10, stale = false })
assert_true(r:is_hit())
end)
test("get_str on object data", function()
local r = Result.new({ ok = true, data = { branch = "main", dirty = false }, age_ms = 100 })
local v, err = r:get_str("branch")
assert_nil(err)
assert_eq(v, "main")
end)
test("get_str missing field", function()
local r = Result.new({ ok = true, data = { branch = "main" } })
local v, err = r:get_str("dirty")
assert_nil(v)
assert_not_nil(err)
end)
test("get_str on non-object data errors", function()
local r = Result.new({ ok = true, data = "main" })
local v, err = r:get_str("anything")
assert_nil(v)
assert_not_nil(err)
end)
test("stale defaults to false", function()
local r = Result.new({ ok = true, data = "main", age_ms = 1000 })
assert_eq(r.stale, false)
end)
test("stale propagates from response", function()
local r = Result.new({ ok = true, data = "main", age_ms = 99999, stale = true })
assert_true(r.stale)
end)
local function make_mock_handle(responses)
local idx = 0
local sent = {}
return {
_sent = sent,
send_line = function(_, line)
sent[#sent + 1] = line
return true
end,
recv_line = function(_)
idx = idx + 1
local resp = responses[idx]
if not resp then
return nil, "mock handle: no more responses"
end
return resp
end,
close = function(_) end,
}
end
suite("Client (mock handle)")
test("get returns Result on hit", function()
local handle = make_mock_handle({
json.encode({ ok = true, data = "main", age_ms = 500, stale = false }),
})
local c = Client.new(handle)
local r, err = c:get("git.branch", "/repo")
assert_nil(err)
assert_not_nil(r)
assert_true(r:is_hit())
assert_eq(r.data, "main")
assert_eq(r.age_ms, 500)
end)
test("get sends correct JSON", function()
local handle = make_mock_handle({
json.encode({ ok = true }),
})
local c = Client.new(handle)
c:get("git.branch", "/myrepo")
local sent_line = handle._sent[1]
assert_not_nil(sent_line)
local req = json.decode(sent_line)
assert_eq(req.op, "get")
assert_eq(req.key, "git.branch")
assert_eq(req.path, "/myrepo")
end)
test("get without path omits path field", function()
local handle = make_mock_handle({
json.encode({ ok = true }),
})
local c = Client.new(handle)
c:get("git.branch")
local req = json.decode(handle._sent[1])
assert_nil(req.path)
end)
test("get returns miss Result when no data field", function()
local handle = make_mock_handle({
json.encode({ ok = true }),
})
local c = Client.new(handle)
local r = c:get("git.branch")
assert_not_nil(r)
assert_true(not r:is_hit())
end)
test("get returns nil+error when server returns ok=false", function()
local handle = make_mock_handle({
json.encode({ ok = false, error = "unknown provider: bad" }),
})
local c = Client.new(handle)
local r, err = c:get("bad.field")
assert_nil(r)
assert_not_nil(err)
assert_true(err:find("unknown provider", 1, true) ~= nil, "error should mention provider: " .. err)
end)
test("poke sends correct op", function()
local handle = make_mock_handle({
json.encode({ ok = true }),
})
local c = Client.new(handle)
local ok, err = c:poke("git", "/repo")
assert_nil(err)
assert_true(ok)
local req = json.decode(handle._sent[1])
assert_eq(req.op, "poke")
assert_eq(req.key, "git")
assert_eq(req.path, "/repo")
end)
test("set_context sends correct op", function()
local handle = make_mock_handle({
json.encode({ ok = true }),
})
local c = Client.new(handle)
local ok, err = c:set_context("/some/dir")
assert_nil(err)
assert_true(ok)
local req = json.decode(handle._sent[1])
assert_eq(req.op, "context")
assert_eq(req.path, "/some/dir")
end)
test("list returns provider array", function()
local handle = make_mock_handle({
json.encode({
ok = true,
data = {
{ name = "git", global = false, fields = {"branch", "dirty"} },
{ name = "hostname", global = true, fields = {"value"} },
}
}),
})
local c = Client.new(handle)
local providers, err = c:list()
assert_nil(err)
assert_not_nil(providers)
assert_eq(#providers, 2)
assert_eq(providers[1].name, "git")
assert_eq(providers[2].name, "hostname")
end)
test("status returns data table", function()
local handle = make_mock_handle({
json.encode({ ok = true, data = { queue_depth = 0, cache_entries = 42 } }),
})
local c = Client.new(handle)
local s, err = c:status()
assert_nil(err)
assert_not_nil(s)
assert_eq(s.cache_entries, 42)
end)
suite("Client (mock Unix socket server)")
local socket_ok, socket_mod = pcall(require, "socket")
local unix_ok, unix_mod = pcall(require, "socket.unix")
if not socket_ok or not unix_ok then
skip("all mock-server tests", "luasocket not available")
return
end
local function with_mock_server(tmppath, response_line, client_fn, verify_request_fn)
os.remove(tmppath)
local server = unix_mod()
server:bind(tmppath)
server:listen(1)
server:settimeout(2)
local luasocket_backend = require("beachcomber.socket_luasocket")
local sock_handle, conn_err = luasocket_backend.connect(tmppath)
local csock, acc_err = server:accept()
if not csock then
if sock_handle then sock_handle:close() end
server:close()
os.remove(tmppath)
error("mock server accept failed: " .. tostring(acc_err))
end
csock:settimeout(2)
if not sock_handle then
csock:close()
server:close()
os.remove(tmppath)
error("mock client connect failed: " .. tostring(conn_err))
end
csock:send(response_line .. "\n")
local ok, err = pcall(client_fn, sock_handle)
if verify_request_fn and ok then
local request_line = csock:receive("*l")
local vok, verr = pcall(verify_request_fn, request_line)
if not vok then
csock:close()
sock_handle:close()
server:close()
os.remove(tmppath)
error(verr)
end
end
csock:close()
sock_handle:close()
server:close()
os.remove(tmppath)
if not ok then error(err) end
end
local function tmppath()
local tmpdir = os.getenv("TMPDIR") or "/tmp"
tmpdir = tmpdir:gsub("/+$", "")
return tmpdir .. "/beachcomber_test_" .. tostring(math.random(100000, 999999)) .. ".sock"
end
test("get over real Unix socket", function()
local path = tmppath()
with_mock_server(
path,
json.encode({ ok = true, data = "feature-branch", age_ms = 250, stale = false }),
function(handle)
local c = Client.new(handle)
local r, err = c:get("git.branch", "/testrepo")
assert_nil(err)
assert_not_nil(r)
assert_true(r:is_hit())
assert_eq(r.data, "feature-branch")
assert_eq(r.age_ms, 250)
end,
function(request_line)
assert_not_nil(request_line)
local req = json.decode(request_line)
assert_eq(req.op, "get")
end
)
end)
test("list over real Unix socket", function()
local path = tmppath()
with_mock_server(
path,
json.encode({ ok = true, data = { { name = "git", global = false, fields = {"branch"} } } }),
function(handle)
local c = Client.new(handle)
local providers, err = c:list()
assert_nil(err)
assert_eq(#providers, 1)
assert_eq(providers[1].name, "git")
end
)
end)
test("server error propagates", function()
local path = tmppath()
with_mock_server(
path,
json.encode({ ok = false, error = "unknown provider: nope" }),
function(handle)
local c = Client.new(handle)
local r, err = c:get("nope.field")
assert_nil(r)
assert_not_nil(err)
assert_true(err:find("unknown provider", 1, true) ~= nil)
end
)
end)
end