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>
145 lines
5.9 KiB
Lua
145 lines
5.9 KiB
Lua
-- ---------------------------------------------------------------------------
|
|
-- End-to-end check of the account and character data path.
|
|
--
|
|
-- The first run creates a marker account. Every later run finds it already
|
|
-- there and verifies it came back byte-for-byte, which is what "persists
|
|
-- between restarts" actually means. Run `selftest_reset` to clear it.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local MARKER = 'zz_selftest'
|
|
local PASSWORD = 'correct horse 7'
|
|
|
|
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
|
|
|
|
local function hash(pw)
|
|
local p = promise.new()
|
|
exports.rp_core:hashPassword(pw, function(h, err) p:resolve({ h = h, err = err }) end)
|
|
local r = Citizen.Await(p)
|
|
return r.h, r.err
|
|
end
|
|
|
|
local function verify(pw, stored)
|
|
local p = promise.new()
|
|
exports.rp_core:verifyPassword(pw, stored, function(ok, err) p:resolve({ ok = ok, err = err }) end)
|
|
return Citizen.Await(p).ok
|
|
end
|
|
|
|
CreateThread(function()
|
|
if not DB.WaitReady(30000) then
|
|
print('^1[selftest] database never became ready^7')
|
|
return
|
|
end
|
|
|
|
print('^5[selftest]^7 ---- account + character data path ----')
|
|
|
|
-- 1. password hashing ----------------------------------------------------
|
|
local h, herr = hash(PASSWORD)
|
|
check('scrypt hash produced', type(h) == 'string' and h:sub(1, 7) == 'scrypt$', tostring(herr))
|
|
check('correct password verifies', verify(PASSWORD, h))
|
|
check('wrong password rejected', not verify(PASSWORD .. 'x', h))
|
|
check('empty password rejected', not verify('', h))
|
|
|
|
local h2 = hash(PASSWORD)
|
|
check('same password hashes differently (salted)', h ~= h2)
|
|
check('second hash also verifies', verify(PASSWORD, h2))
|
|
check('corrupt hash rejected safely', not verify(PASSWORD, 'not-a-hash'))
|
|
|
|
-- 2. the account row ------------------------------------------------------
|
|
local existing = DB.Single('SELECT id, password_hash, created_at FROM accounts WHERE username = ?', { MARKER })
|
|
local firstRun = existing == nil
|
|
local accountId
|
|
|
|
if firstRun then
|
|
accountId = DB.Insert(
|
|
'INSERT INTO accounts (username, password_hash, license) VALUES (?, ?, ?)',
|
|
{ MARKER, h, 'selftest-license' })
|
|
check('account created', type(accountId) == 'number' and accountId > 0)
|
|
print('^3[selftest]^7 marker account created - restart the server to prove persistence')
|
|
else
|
|
accountId = existing.id
|
|
check('account survived restart', true)
|
|
check('stored hash still verifies after restart', verify(PASSWORD, existing.password_hash))
|
|
print(('^5[selftest]^7 marker account has existed since %s'):format(tostring(existing.created_at)))
|
|
end
|
|
|
|
-- 3. the character row, including the appearance JSON ---------------------
|
|
local appearance = Appearance.Sanitise({
|
|
model = 'f',
|
|
parents = { father = 12, mother = 33, shapeMix = 0.62, skinMix = 0.25 },
|
|
hair = { style = 14, colour = 9, highlight = 2 },
|
|
features = { ['0'] = 0.4, ['13'] = -0.75 },
|
|
overlays = { eyebrows = { index = 5, opacity = 0.8, colour = 3 } },
|
|
components = { jacket = { drawable = 42, texture = 3 } },
|
|
})
|
|
|
|
local charId = DB.Scalar('SELECT id FROM characters WHERE account_id = ? AND deleted = 0 LIMIT 1', { accountId })
|
|
|
|
if not charId then
|
|
charId = DB.Insert([[
|
|
INSERT INTO characters (account_id, slot, first_name, last_name, dob, gender, backstory, appearance)
|
|
VALUES (?, 0, ?, ?, ?, 'f', ?, ?)
|
|
]], { accountId, 'Zztest', 'Persistence', '1991-04-18',
|
|
'Created by the self test.', json.encode(appearance) })
|
|
check('character created', type(charId) == 'number' and charId > 0)
|
|
else
|
|
check('character survived restart', true)
|
|
end
|
|
|
|
local row = DB.Single('SELECT * FROM characters WHERE id = ?', { charId })
|
|
check('character reads back', row ~= nil)
|
|
|
|
if row then
|
|
local back = json.decode(row.appearance)
|
|
check('appearance JSON round trip', json.encode(Appearance.Sanitise(back)) == json.encode(appearance),
|
|
'appearance differs after a round trip')
|
|
check('float precision kept', math.abs(back.parents.shapeMix - 0.62) < 0.0001,
|
|
tostring(back.parents.shapeMix))
|
|
check('negative feature kept', math.abs(back.features['13'] + 0.75) < 0.0001,
|
|
tostring(back.features['13']))
|
|
check('starting balances applied', tonumber(row.cash) == Config.Money.startingCash
|
|
and tonumber(row.bank) == Config.Money.startingBank,
|
|
('cash=%s bank=%s'):format(tostring(row.cash), tostring(row.bank)))
|
|
end
|
|
|
|
-- 4. the constraints the session code relies on ---------------------------
|
|
local dupe, dupeErr = DB.Insert(
|
|
'INSERT INTO accounts (username, password_hash) VALUES (?, ?)', { MARKER, h })
|
|
check('duplicate username refused by the database', dupe == nil, tostring(dupe))
|
|
|
|
local dupeName = DB.Insert([[
|
|
INSERT INTO characters (account_id, slot, first_name, last_name, dob, gender)
|
|
VALUES (?, 1, 'Zztest', 'Persistence', '1991-04-18', 'f')
|
|
]], { accountId })
|
|
check('duplicate character name refused by the database', dupeName == nil)
|
|
|
|
local orphan = DB.Insert([[
|
|
INSERT INTO characters (account_id, slot, first_name, last_name, dob, gender)
|
|
VALUES (999999, 0, 'Zzorphan', 'Nobody', '1991-04-18', 'm')
|
|
]], {})
|
|
check('character cannot reference a missing account', orphan == nil)
|
|
|
|
print(('^5[selftest]^7 ==== %d passed, %d failed ===='):format(passed, failed))
|
|
if failed == 0 then
|
|
print(firstRun and '^2[selftest] data path OK (first run)^7' or '^2[selftest] data path OK and persistent^7')
|
|
else
|
|
print('^1[selftest] PROBLEMS FOUND^7')
|
|
end
|
|
end)
|
|
|
|
RegisterCommand('selftest_reset', function()
|
|
CreateThread(function()
|
|
DB.Update('DELETE FROM accounts WHERE username = ?', { MARKER }) -- characters cascade
|
|
print('^3[selftest]^7 marker account removed')
|
|
end)
|
|
end, true)
|