mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 22:01:40 +00:00
FIX - enforce configured app availability
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
PHONE_APPS,
|
||||
isExternalPhoneApp,
|
||||
isLaunchablePhoneApp,
|
||||
} from '@/config/apps'
|
||||
|
||||
const repositoryRoot = join(import.meta.dirname, '../..')
|
||||
const source = (path: string) =>
|
||||
readFileSync(join(repositoryRoot, path), 'utf8')
|
||||
|
||||
const app = source('frontend/src/App.vue')
|
||||
const appStore = source('frontend/src/stores/app-store.ts')
|
||||
const appStoreView = source('frontend/src/views/apps/AppStoreApp.vue')
|
||||
const appWindow = source('frontend/src/views/PhoneAppWindow.vue')
|
||||
const client = source('sky_phone/source/client/main.lua')
|
||||
const config = source('sky_phone/config/config.lua')
|
||||
const configDefault = source('sky_phone/source/shared/config_default.lua')
|
||||
const crypto = source('sky_phone/source/server/crypto.lua')
|
||||
const phone = source('sky_phone/source/server/phone.lua')
|
||||
const router = source('frontend/src/router/index.ts')
|
||||
|
||||
describe('configured app availability contract', () => {
|
||||
it('keeps every built-in app in the file and runtime defaults', () => {
|
||||
const builtInAppIds = PHONE_APPS.filter(
|
||||
(app) =>
|
||||
isLaunchablePhoneApp(app) && !app.adminOnly && !isExternalPhoneApp(app),
|
||||
).map((app) => app.id)
|
||||
|
||||
for (const appId of builtInAppIds) {
|
||||
const entry = appId.includes('-')
|
||||
? `["${appId}"] = true`
|
||||
: `${appId} = true`
|
||||
expect(config).toContain(entry)
|
||||
expect(configDefault).toContain(entry)
|
||||
}
|
||||
})
|
||||
|
||||
it('sends disabled apps and reapplies them to an open phone after live saves', () => {
|
||||
expect(phone).toContain('disabledApps = SkyPhone.GetDisabledApps()')
|
||||
expect(phone).toMatch(
|
||||
/function SkyPhone\.GetDisabledApps\(\)[\s\S]*?enabled == false/,
|
||||
)
|
||||
expect(client).toMatch(
|
||||
/configurator:updated[\s\S]*?apply_disabled_apps\(device_payload\)[\s\S]*?device:updated/,
|
||||
)
|
||||
expect(app).toContain(
|
||||
'appStore.hydrate(payload.device?.data.apps?.payload, payload.disabledApps)',
|
||||
)
|
||||
})
|
||||
|
||||
it('removes disabled apps from every launch path without changing device installs', () => {
|
||||
expect(appStore).toContain('disabledApps: [] as LaunchablePhoneAppId[]')
|
||||
expect(appStore).toContain('if (!this.isAvailable(appId)) return false')
|
||||
expect(appStoreView).toContain('appStore.isAvailable(app.id)')
|
||||
expect(router).toContain('useAppStoreStore().isInstalled(to.params.appId)')
|
||||
expect(appWindow).toContain('appStore.isInstalled(app.id)')
|
||||
expect(app).toMatch(
|
||||
/isPhoneAppId\(currentAppId\)[\s\S]*?!appStore\.isInstalled\(currentAppId\)[\s\S]*?router\.push\('\/'\)/,
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects crypto operations after the app is disabled at runtime', () => {
|
||||
expect(crypto).toMatch(
|
||||
/local function require_phone\(source\)[\s\S]*?not Config\.Crypto\.Enabled or not SkyPhone\.IsAppEnabled\("crypto"\)/,
|
||||
)
|
||||
expect(crypto).toMatch(
|
||||
/local function refresh_crypto_runtime\(\)[\s\S]*?not Config\.Crypto\.Enabled or not SkyPhone\.IsAppEnabled\("crypto"\)[\s\S]*?sessions = \{\}/,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from 'vue-router'
|
||||
|
||||
import { isPhoneAppId } from '@/config/apps'
|
||||
import { useAppStoreStore } from '@/stores/app-store'
|
||||
import PhoneAppWindow from '@/views/PhoneAppWindow.vue'
|
||||
import SpringboardView from '@/views/SpringboardView.vue'
|
||||
|
||||
@@ -34,7 +35,9 @@ export default createRouter({
|
||||
},
|
||||
{
|
||||
beforeEnter: (to) =>
|
||||
typeof to.params.appId === 'string' && isPhoneAppId(to.params.appId)
|
||||
typeof to.params.appId === 'string' &&
|
||||
isPhoneAppId(to.params.appId) &&
|
||||
useAppStoreStore().isInstalled(to.params.appId)
|
||||
? true
|
||||
: '/',
|
||||
component: PhoneAppWindow,
|
||||
|
||||
@@ -276,6 +276,31 @@ describe('app store', () => {
|
||||
expect(apps.isInstalled('phone')).toBe(true)
|
||||
})
|
||||
|
||||
it('temporarily blocks server-disabled apps without changing their installation', () => {
|
||||
const apps = useAppStoreStore()
|
||||
apps.hydrate({ claimedApps: ['crypto'] }, [
|
||||
'crypto',
|
||||
'citywarn',
|
||||
'not-an-app',
|
||||
])
|
||||
mocks.phone.saveDeviceNamespace.mockClear()
|
||||
|
||||
expect(apps.disabledApps).toEqual(['crypto', 'citywarn'])
|
||||
expect(apps.isAvailable('crypto')).toBe(false)
|
||||
expect(apps.isInstalled('crypto')).toBe(false)
|
||||
expect(apps.isInstalled('citywarn')).toBe(false)
|
||||
expect(apps.claimedApps).toEqual(['crypto'])
|
||||
|
||||
apps.installApp('crypto')
|
||||
expect(apps.installingApps).toEqual({})
|
||||
expect(mocks.phone.saveDeviceNamespace).not.toHaveBeenCalled()
|
||||
|
||||
apps.hydrate({ claimedApps: ['crypto'] })
|
||||
expect(apps.isAvailable('crypto')).toBe(true)
|
||||
expect(apps.isInstalled('crypto')).toBe(true)
|
||||
expect(apps.isInstalled('citywarn')).toBe(true)
|
||||
})
|
||||
|
||||
it('protects every default app from full uninstallation', () => {
|
||||
const apps = useAppStoreStore()
|
||||
apps.hydrate(null)
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import CustomAppFrame from '@/components/CustomAppFrame.vue'
|
||||
import { getPhoneApp, isExternalPhoneApp } from '@/config/apps'
|
||||
import { useAppStoreStore } from '@/stores/app-store'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { getCustomAppFrameKey } from '@/utils/customAppLifecycle'
|
||||
import AppStoreApp from '@/views/apps/AppStoreApp.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const appStore = useAppStoreStore()
|
||||
const phone = usePhoneStore()
|
||||
const app = computed(() => getPhoneApp(route.params.appId))
|
||||
const builtinAppComponent = computed(() =>
|
||||
@@ -28,7 +30,7 @@ const launchStyle = computed(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="app && !app.adminOnly"
|
||||
v-if="app && !app.adminOnly && appStore.isInstalled(app.id)"
|
||||
class="app-window"
|
||||
:class="{ 'app-window--citywarn': app.id === 'citywarn' }"
|
||||
:style="launchStyle"
|
||||
|
||||
@@ -371,6 +371,9 @@ local function initialize_markets()
|
||||
end
|
||||
|
||||
local function require_phone(source)
|
||||
if not Config.Crypto.Enabled or not SkyPhone.IsAppEnabled("crypto") then
|
||||
return nil, nil, { success = false, error = "service_unavailable" }
|
||||
end
|
||||
local phone_session, error_response = SkyPhone.RequireSession(source)
|
||||
if not phone_session then
|
||||
return nil, nil, error_response
|
||||
@@ -661,7 +664,7 @@ local function with_exchange_lock(callback)
|
||||
end
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:crypto:bootstrap", function(source)
|
||||
if not Config.Crypto.Enabled then
|
||||
if not Config.Crypto.Enabled or not SkyPhone.IsAppEnabled("crypto") then
|
||||
return { success = false, error = "service_unavailable" }
|
||||
end
|
||||
local profile, error_response = authenticated_profile(source)
|
||||
@@ -1647,6 +1650,9 @@ end
|
||||
local function refresh_crypto_runtime()
|
||||
initialize_markets()
|
||||
start_crypto_schedulers()
|
||||
if not Config.Crypto.Enabled or not SkyPhone.IsAppEnabled("crypto") then
|
||||
sessions = {}
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler("sky_phone:configurator:serverUpdated", refresh_crypto_runtime)
|
||||
|
||||
Reference in New Issue
Block a user