Files
sky_phone/tests/server_notifications.lua
Leon.Schmidt 30559048fd FIX - complete LB custom app compatibility (#11)
* FIX - preserve LB custom app lifecycle

* FIX - reset LB export aliases before startup

* FIX - report competing custom app providers

* FIX - identify LB app frames as live NUI

* FIX - complete LB custom app compatibility

* FIX - provide LB PicChat runtime exports

* FIX - route LB PicChat through Sky Phone

* FIX - return cached LB equipped phone number

* FIX - harden PicChat compatibility migration

* ENH - complete modular phone provider and Creator APIs

* DOC - document the Sky Phone Creator API

---------

Co-authored-by: DerEchteAlec <bycraky@gmail.com>
Co-authored-by: DerEchteAlec <alec.schitzkat@luwan.io>
2026-08-20 18:19:16 +02:00

241 lines
8.7 KiB
Lua

local migration_callback
local client_events = {}
local registered_callbacks = {}
local players = {}
local source_lookup_counts = {}
local volatile_source
Config = {
Sim = {
NumberLength = 6,
NumberPrefix = "",
},
}
local online_by_source = {
[10] = {
imei = "123456789012347",
phoneNumber = "555010",
source = 10,
},
[30] = {
imei = "223456789012346",
phoneNumber = "555030",
source = 30,
},
}
Bridge = {
Callbacks = {
Register = function(name, handler)
registered_callbacks[name] = handler
end,
},
Database = {
AfterMigration = function(name, callback)
assert(name == "sky_phone")
migration_callback = callback
end,
Query = function(query, parameters)
assert(query:find("sky_phone_devices", 1, true), "device context query changed")
local imei = parameters[1]
local identities = {
["123456789012347"] = {
device_name = "Phone 10",
imei = "123456789012347",
settings = [[{"version":1,"source":10}]],
},
["223456789012346"] = {
device_name = "Phone 30",
imei = "223456789012346",
settings = [[{"version":1,"source":30}]],
},
["323456789012345"] = {
device_name = "Phone 40",
imei = "323456789012345",
settings = [[{"version":1,"source":40}]],
},
}
return identities[imei] and { identities[imei] } or {}
end,
},
Debug = function() end,
Framework = {
GetPlayers = function()
return players
end,
},
}
SkyPhoneApps = {
ValidateAppId = function(app_id)
return type(app_id) == "string"
and #app_id <= 64
and app_id:match("^[a-z0-9][a-z0-9._-]+$") ~= nil
end,
}
SkyPhoneDeviceDirectory = {
GetOnlineBySource = function(player_source)
source_lookup_counts[player_source] = (source_lookup_counts[player_source] or 0) + 1
if player_source == volatile_source and source_lookup_counts[player_source] >= 3 then
return nil, "device_not_equipped"
end
return online_by_source[player_source]
end,
GetOnlineByPhoneNumber = function(phone_number)
if phone_number == "555030" then
return online_by_source[30]
end
if phone_number == "555099" then
return {
imei = "223456789012346",
phoneNumber = "555099",
source = 30,
}
end
return nil, "player_unavailable"
end,
GetOnlineByImei = function(imei)
if imei == "123456789012347" then
return online_by_source[10]
end
return nil, "player_unavailable"
end,
}
function TriggerClientEvent(name, target, payload)
client_events[#client_events + 1] = {
name = name,
payload = payload,
target = target,
}
end
assert(loadfile("sky_phone/source/shared/imei.lua"))()
assert(loadfile("sky_phone/source/shared/sim_number.lua"))()
assert(loadfile("sky_phone/source/server/notifications.lua"))()
assert(type(migration_callback) == "function", "notification router must wait for migrations")
migration_callback()
local self_notification = assert(registered_callbacks["sky_phone:notifications:self"],
"self notification callback must be registered after migration")
local notification = {
app_id = "messages",
body = " Body ",
title = " Title ",
url = "https://invalid.example",
}
local result, error_code = SkyPhoneNotifications.Send(
{ kind = "source", value = "10" },
notification
)
assert(result == nil and error_code == "invalid_target", "source targets must remain strictly typed")
assert(#client_events == 0, "invalid targets must not deliver")
result, error_code = SkyPhoneNotifications.Send(
{ kind = "number", value = "not-a-number" },
notification
)
assert(result == nil and error_code == "invalid_target", "malformed numbers must be rejected")
result, error_code = SkyPhoneNotifications.Send(
{ kind = "device", value = "invalid-imei" },
notification
)
assert(result == nil and error_code == "invalid_target", "malformed IMEIs must be rejected")
result, error_code = SkyPhoneNotifications.Send(
{ kind = "source", value = 10 },
{
appId = "messages",
title = string.rep("x", 161),
text = "Body",
}
)
assert(result == nil and error_code == "invalid_title", "server payload bounds must be enforced")
source_lookup_counts = {}
result = assert(SkyPhoneNotifications.Send({ kind = "source", value = 10 }, notification))
assert(result.delivered == 1, "equipped source target must receive one notification")
assert(client_events[1].name == "sky_phone:notifications:show", "notification event name changed")
assert(client_events[1].target == 10, "notification source target changed")
assert(client_events[1].payload.appId == "messages", "app_id alias was not normalized")
assert(client_events[1].payload.title == "Title" and client_events[1].payload.text == "Body",
"notification text was not normalized")
assert(client_events[1].payload.url == nil, "unsafe fields must not cross the server boundary")
assert(client_events[1].payload.device.imei == "123456789012347",
"the final equipped IMEI must be attached")
assert(client_events[1].payload.device.name == "Phone 10",
"the final device name must be attached")
assert(client_events[1].payload.device.settings == [[{"version":1,"source":10}]],
"the final device settings must be attached")
assert(source_lookup_counts[10] == 3, "delivery must revalidate after loading device context")
source_lookup_counts = {}
local self_result = self_notification(10, notification)
assert(self_result.success and self_result.data.delivered == 1,
"self notifications must resolve the caller's equipped device")
assert(client_events[#client_events].payload.device.imei == "123456789012347",
"self notifications must carry the authoritative equipped IMEI")
client_events[#client_events] = nil
local unavailable_self_result = self_notification(20, notification)
assert(not unavailable_self_result.success and unavailable_self_result.error == "device_not_equipped",
"self notifications without an equipped device must fail visibly")
source_lookup_counts = {}
result = assert(SkyPhoneNotifications.Send({ kind = "source", value = 20 }, notification))
assert(result.delivered == 0, "players without an equipped device must not receive notifications")
assert(#client_events == 1, "no-delivery results must not emit an event")
source_lookup_counts = {}
result = assert(SkyPhoneNotifications.Send({ kind = "number", value = "555099" }, notification))
assert(result.delivered == 0, "changed phone-number ownership must fail revalidation")
assert(#client_events == 1, "failed number revalidation must not deliver")
source_lookup_counts = {}
result = assert(SkyPhoneNotifications.Send(
{ kind = "device", value = "123456789012347" },
notification
))
assert(result.delivered == 1, "equipped device targets must deliver")
assert(client_events[2].target == 10, "device target resolved to the wrong source")
assert(client_events[2].payload.device.imei == "123456789012347")
players = { 40 }
online_by_source[40] = {
imei = "323456789012345",
phoneNumber = "555040",
source = 40,
}
source_lookup_counts = {}
volatile_source = 40
result = assert(SkyPhoneNotifications.Send({ kind = "all" }, notification))
assert(result.delivered == 0, "a device switch during delivery must fail final revalidation")
assert(#client_events == 2, "stale broadcast candidates must not receive notifications")
players = { "10", 10, 20, 30, "30" }
source_lookup_counts = {}
volatile_source = nil
result = assert(SkyPhoneNotifications.Send({ kind = "all" }, notification))
assert(result.delivered == 2, "broadcast must deduplicate equipped online players")
assert(#client_events == 4, "broadcast count must match emitted events")
local broadcast_targets = {}
for index = 1, #client_events do
local event = client_events[index]
assert(event.target ~= -1, "notification broadcasts must never use blind -1 delivery")
if index >= 3 then
broadcast_targets[event.target] = (broadcast_targets[event.target] or 0) + 1
end
end
assert(broadcast_targets[10] == 1 and broadcast_targets[30] == 1,
"broadcast recipients must be unique and equipped")
result, error_code = SkyPhoneNotifications.Send({ kind = "all", value = true }, notification)
assert(result == nil and error_code == "invalid_target", "all targets must reject extra values")
print("Server notification router tests passed")