local registered_callbacks = {} local migration_callbacks = {} local event_handlers = {} Bridge = { Callbacks = { Register = function(name, callback) assert(type(name) == "string" and type(callback) == "function") assert(registered_callbacks[name] == nil, "duplicate callback: " .. name) registered_callbacks[name] = callback end, }, Database = { AfterMigration = function(name, callback) assert(name == "sky_phone") migration_callbacks[#migration_callbacks + 1] = callback end, Query = function() return {} end, Transaction = function() return true end, }, Debug = function() end, Framework = { HasPermission = function() return true end, }, Inventory = { GetResourceName = function() return "test_inventory" end, RegisterUsableItem = function(_, callback) assert(type(callback) == "function") return true end, }, } Config = { AdminPanel = { Command = "phoneadmin", Enabled = true, }, Phone = { DevelopmentCommand = false, DeviceName = "Test Phone", Item = "phone", Unique = true, }, Sim = { Enabled = true, NumberGroups = { 3, 4 }, NumberLength = 7, NumberPrefix = "", }, Server = { PasscodePepper = "test-pepper", }, Security = { AttemptsPerMinute = 5, LockSeconds = 60, MaximumAttempts = 5, }, Mail = { AuthAttemptsPerMinute = 5, Domain = "test.local", LocalPartMinLength = 3, LocalPartMaxLength = 32, PasswordMinLength = 8, PasswordMaxLength = 72, }, } json = { encode = function() return "{}" end, decode = function() return {} end, } function AddEventHandler(name, callback) assert(type(callback) == "function") event_handlers[name] = event_handlers[name] or {} event_handlers[name][#event_handlers[name] + 1] = callback end function RegisterCommand(name, callback, restricted) assert(name == "phoneadmin") assert(type(callback) == "function") assert(restricted == false) end function TriggerClientEvent() end function TriggerEvent() end function GetCurrentResourceName() return "sky_phone" end function GetGameTimer() return 1 end function GetPlayers() return {} end SkyPhoneImei = {} SkyPhoneSimNumber = {} local module_paths = { "sky_phone/source/server/phone_security.lua", "sky_phone/source/server/phone_accounts.lua", "sky_phone/source/server/phone_persistence.lua", "sky_phone/source/server/phone.lua", } for _, path in ipairs(module_paths) do assert(loadfile(path))() end for _, callback in ipairs(migration_callbacks) do callback() end assert(type(SkyPhoneSecurity) == "table") assert(type(SkyPhoneSecurity.Load) == "function") assert(type(SkyPhoneSecurity.Status) == "function") assert(type(SkyPhoneAccounts) == "table") assert(type(SkyPhoneAccounts.List) == "function") for _, method in ipairs({ "EnsureDevice", "FindDeviceSlots", "LoadDevice", "RefreshSource", "GetEquippedPhoneNumber", "GetSourceFromNumber", "FormatNumber", "RequireDeviceSession", "RequireSession", "AllowOperation", "RequireAccount", "NotifyAccount", "NotifyAccountDevices", "RefreshAccount", "RefreshDevice", "OpenDeviceForCall", }) do assert(type(SkyPhone[method]) == "function", "missing SkyPhone API: " .. method) end for _, callback_name in ipairs({ "sky_phone:security:unlock", "sky_phone:security:set-passcode", "sky_phone:security:change-passcode", "sky_phone:security:disable-passcode", "sky_phone:device:save", "sky_phone:notifications:save", "sky_phone:device:factory-reset", "sky_phone:account:login", "sky_phone:mail:login", "sky_phone:account:register", "sky_phone:mail:register", "sky_phone:account:logout", "sky_phone:mail:logout", "sky_phone:account:devices", "sky_phone:account:remove-device", "sky_phone:device:open-request", "sky_phone:device:development-open", "sky_phone:device:close", "sky_phone:device:equipped-number", "sky_phone:device:notification-open", }) do assert(type(registered_callbacks[callback_name]) == "function", "missing callback: " .. callback_name) end for _, callback_name in ipairs({ "sky_phone:security:unlock", "sky_phone:device:save", "sky_phone:device:factory-reset", "sky_phone:account:devices", }) do local response = registered_callbacks[callback_name](1, {}) assert(response.success == false and response.error == "device_not_open", "callback not bound to core: " .. callback_name) end local phone_item = { name = "phone", slot = 4, amount = 1, metadata = { imei = "123456789012345" }, } local opened_event local device_error local hide_phone_during_prepare = false Bridge.Framework.GetIdentifier = function(source) assert(source == 1) return "license:test-player" end Bridge.Framework.GetFirstname = function() return "Test" end Bridge.Framework.GetLastname = function() return "Player" end Bridge.Inventory.GetSlot = function(source, slot) assert(source == 1 and slot == phone_item.slot) return phone_item end Bridge.Inventory.GetSlotsWithItem = function(source, item_name) assert(source == 1 and item_name == Config.Phone.Item) if hide_phone_during_prepare == true then return {} end return { phone_item } end Bridge.Inventory.SetSlotMetadata = function() error("existing phone metadata must not be rewritten") end Bridge.Database.Query = function(query) if query:find("FROM `sky_phone_devices` d", 1, true) then return { { imei = phone_item.metadata.imei, device_name = Config.Phone.DeviceName, account_id = nil, sim_id = nil, }, } end return {} end SkyPhoneImei.IsValid = function(imei) return imei == phone_item.metadata.imei end SkyPhoneSim = { PrepareDevice = function(source, slot, imei) assert(source == 1 and slot == phone_item and imei == phone_item.metadata.imei) if hide_phone_during_prepare == "next" then hide_phone_during_prepare = true end return true end, } SkyPhoneNotes = { List = function() return {} end, } SkyPhoneMemos = { List = function() return {} end, } SkyPhoneCompanies = { ClearCallAvailability = function() end, } TriggerClientEvent = function(event_name, source, payload) if event_name == "sky_phone:device:open" then opened_event = { source = source, payload = payload } elseif event_name == "sky_phone:device:error" then device_error = { source = source, error = payload } end end for _, callback in ipairs(event_handlers.onServerResourceStart or {}) do callback("sky_phone") end local no_sim_open = registered_callbacks["sky_phone:device:open-request"](1, {}) assert(no_sim_open.success == true, "a phone item without a SIM must still open") assert(opened_event and opened_event.source == 1, "no-SIM open must reach the client") assert(opened_event.payload.device.imei == phone_item.metadata.imei) assert(opened_event.payload.device.sim == nil, "no-SIM bootstrap must keep device.sim nullable") opened_event = nil device_error = nil hide_phone_during_prepare = "next" local lost_phone_open = registered_callbacks["sky_phone:device:open-request"](1, {}) assert(lost_phone_open.success == false, "bootstrap ownership loss must fail the open request") assert(lost_phone_open.error == "device_not_owned", "bootstrap ownership loss must return its error code") assert(opened_event == nil, "bootstrap ownership loss must not open the NUI") assert(device_error and device_error.error == "device_not_owned", "bootstrap ownership loss must notify the client") local manifest_file = assert(io.open("sky_phone/fxmanifest.lua", "rb")) local manifest = manifest_file:read("*a") manifest_file:close() local security_position = assert(manifest:find("source/server/phone_security.lua", 1, true)) local accounts_position = assert(manifest:find("source/server/phone_accounts.lua", 1, true)) local persistence_position = assert(manifest:find("source/server/phone_persistence.lua", 1, true)) local phone_position = assert(manifest:find("source/server/phone.lua", 1, true)) local migration_position = assert(manifest:find("source/server/db_migrate.lua", 1, true)) assert(security_position < phone_position) assert(accounts_position < phone_position) assert(persistence_position < phone_position) assert(phone_position < migration_position) print("server phone modules: ok")