rp_core (sessions, scrypt auth, self-written MySQL-wire driver rp_db), county-records NUI: residency-file auth, identity-record character intake, survey-grid spawn, procedural night-city loading screen. Secrets replaced with .example files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
123 lines
5.8 KiB
Lua
123 lines
5.8 KiB
Lua
-- Self-test for the hand-written MySQL driver. Run with: ensure rp_dbtest
|
|
|
|
local passed, failed = 0, 0
|
|
|
|
local function check(name, ok, detail)
|
|
if ok then
|
|
passed = passed + 1
|
|
print(('^2 PASS^7 %s'):format(name))
|
|
else
|
|
failed = failed + 1
|
|
print(('^1 FAIL^7 %s^1 %s^7'):format(name, detail or ''))
|
|
end
|
|
end
|
|
|
|
CreateThread(function()
|
|
print('^5[dbtest]^7 waiting for the pool...')
|
|
if not DB.WaitReady(30000) then
|
|
print('^1[dbtest] database never became ready^7')
|
|
return
|
|
end
|
|
|
|
-- 1. round trip through the text protocol -------------------------------
|
|
local two = DB.Scalar('SELECT 1 + 1')
|
|
check('scalar arithmetic', two == 2, ('got %s (%s)'):format(tostring(two), type(two)))
|
|
|
|
local row = DB.Single('SELECT ? AS s, ? AS n, ? AS f', { 'hello', 42, 1.5 })
|
|
check('typed columns', row and row.s == 'hello' and row.n == 42 and row.f == 1.5,
|
|
row and json.encode(row) or 'nil')
|
|
|
|
-- 2. hostile strings must survive verbatim ------------------------------
|
|
DB.Update("DELETE FROM accounts WHERE username LIKE 'ztest%'")
|
|
|
|
local nasty = [[O'Brien \ "quoted" ]] .. '\n\t' .. [[ 100% ` ; -- /* */ ?]]
|
|
local id = DB.Insert(
|
|
'INSERT INTO accounts (username, password_hash) VALUES (?, ?)',
|
|
{ 'ztest_a', nasty })
|
|
check('insert returns id', type(id) == 'number' and id > 0, tostring(id))
|
|
|
|
local back = DB.Scalar('SELECT password_hash FROM accounts WHERE id = ?', { id })
|
|
check('hostile string round trip', back == nasty,
|
|
('stored %q'):format(tostring(back)))
|
|
|
|
-- 3. a parameter can never become SQL -----------------------------------
|
|
local inject = "'); DROP TABLE accounts; -- "
|
|
DB.Insert('INSERT INTO accounts (username, password_hash) VALUES (?, ?)',
|
|
{ 'ztest_b', inject })
|
|
local stillThere = DB.Scalar(
|
|
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'accounts'")
|
|
check('injection is inert', stillThere == 1, 'accounts table missing!')
|
|
local storedInject = DB.Scalar("SELECT password_hash FROM accounts WHERE username = 'ztest_b'")
|
|
check('injection stored literally', storedInject == inject, tostring(storedInject))
|
|
|
|
-- 4. question marks inside literals must not shift binding ---------------
|
|
local q = DB.Single("SELECT 'what? really?' AS lit, ? AS bound", { 'bound-value' })
|
|
check('literal ? not treated as placeholder',
|
|
q and q.lit == 'what? really?' and q.bound == 'bound-value',
|
|
q and json.encode(q) or 'nil')
|
|
|
|
-- 5. NULL handling -------------------------------------------------------
|
|
DB.Update('UPDATE accounts SET last_ip = ? WHERE id = ?', { DB.NULL, id })
|
|
local isNull = DB.Scalar('SELECT last_ip IS NULL FROM accounts WHERE id = ?', { id })
|
|
check('DB.NULL writes a real NULL', isNull == 1, tostring(isNull))
|
|
local nilRow = DB.Single('SELECT last_ip FROM accounts WHERE id = ?', { id })
|
|
check('NULL reads back as nil', nilRow ~= nil and nilRow.last_ip == nil,
|
|
nilRow and json.encode(nilRow) or 'nil')
|
|
|
|
-- 6. unicode -------------------------------------------------------------
|
|
local uni = 'Ünïcødé — 日本語 — 🚓'
|
|
DB.Update('UPDATE accounts SET password_hash = ? WHERE id = ?', { uni, id })
|
|
check('utf8mb4 round trip', DB.Scalar('SELECT password_hash FROM accounts WHERE id = ?', { id }) == uni)
|
|
|
|
-- 7. empty result sets ---------------------------------------------------
|
|
local none = DB.Query('SELECT * FROM accounts WHERE id = ?', { -1 })
|
|
check('empty select returns empty table', type(none) == 'table' and #none == 0)
|
|
check('missing single returns nil', DB.Single('SELECT * FROM accounts WHERE id = ?', { -1 }) == nil)
|
|
|
|
-- 8. transactions --------------------------------------------------------
|
|
local ok = DB.Transaction({
|
|
{ query = 'UPDATE accounts SET role = ? WHERE id = ?', values = { 'admin', id } },
|
|
{ query = 'UPDATE accounts SET banned = ? WHERE id = ?', values = { 1, id } },
|
|
})
|
|
local after = DB.Single('SELECT role, banned FROM accounts WHERE id = ?', { id })
|
|
check('transaction commits', ok and after.role == 'admin' and after.banned == 1,
|
|
json.encode(after or {}))
|
|
|
|
local bad, err = DB.Transaction({
|
|
{ query = 'UPDATE accounts SET role = ? WHERE id = ?', values = { 'mod', id } },
|
|
{ query = 'UPDATE accounts SET nonexistent_column = 1 WHERE id = ?', values = { id } },
|
|
})
|
|
local rolled = DB.Scalar('SELECT role FROM accounts WHERE id = ?', { id })
|
|
check('failed transaction rolls back', bad == false and rolled == 'admin',
|
|
('role is now %s'):format(tostring(rolled)))
|
|
|
|
-- 9. errors surface instead of failing silently --------------------------
|
|
local res, qerr = DB.Query('SELECT * FROM table_that_does_not_exist')
|
|
check('bad query returns an error', res == nil and type(qerr) == 'string' and #qerr > 0,
|
|
tostring(qerr))
|
|
|
|
-- 10. wrong parameter count is caught, not guessed -----------------------
|
|
local _, cerr = DB.Query('SELECT ?, ?', { 1 })
|
|
check('parameter count mismatch reported', cerr ~= nil, tostring(cerr))
|
|
|
|
-- 11. a large payload exercises multi-packet framing in both directions --
|
|
local big = string.rep('x', 700000)
|
|
check('700KB payload round trip', DB.Scalar('SELECT ? AS big', { big }) == big)
|
|
|
|
-- 12. concurrency: many queries in flight across the pool ----------------
|
|
local done, results = 0, {}
|
|
for i = 1, 40 do
|
|
DB.ScalarAsync('SELECT ?', { i }, function(v) done = done + 1; results[i] = v end)
|
|
end
|
|
local waited = 0
|
|
while done < 40 and waited < 10000 do Wait(50); waited = waited + 50 end
|
|
local allGood = done == 40
|
|
for i = 1, 40 do if results[i] ~= i then allGood = false end end
|
|
check('40 concurrent queries all answered correctly', allGood, ('done=%d'):format(done))
|
|
|
|
DB.Update("DELETE FROM accounts WHERE username LIKE 'ztest%'")
|
|
|
|
print(('^5[dbtest]^7 ==== %d passed, %d failed ===='):format(passed, failed))
|
|
if failed == 0 then print('^2[dbtest] driver OK^7') else print('^1[dbtest] DRIVER HAS PROBLEMS^7') end
|
|
end)
|