mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
3b66b13e43
Run smaller server-authoritative market ticks every few seconds, update portfolio and chart values from batched prices, and mirror the live feed in the browser test server.
1151 lines
32 KiB
JavaScript
1151 lines
32 KiB
JavaScript
const assert = require('node:assert/strict')
|
|
const { once } = require('node:events')
|
|
|
|
const { app } = require('./index.cjs')
|
|
|
|
const browserDataRequests = [
|
|
['development:bootstrap', {}],
|
|
['account:devices', {}],
|
|
['banking:overview', {}],
|
|
['crypto:bootstrap', {}],
|
|
['crypto:market-tick', {}],
|
|
['crypto:recipient', { walletKey: 'VX-DEAD-BEEF-C0DE-2026' }],
|
|
['crypto:quote', { marketId: 'aurora', quantity: '1', side: 'buy' }],
|
|
['health:overview', {}],
|
|
['billing:overview', {}],
|
|
['billing:list', { filter: 'all', limit: 20, offset: 0 }],
|
|
['calendar:list', { endsAt: 4_102_444_800, startsAt: 0 }],
|
|
['calls:recents', {}],
|
|
['companies:list', {}],
|
|
['companies:my-requests', { limit: 20, offset: 0 }],
|
|
['companies:work-context', {}],
|
|
['companies:work-queue', { limit: 20, offset: 0 }],
|
|
['contacts:list', {}],
|
|
['crewlink:login', { password: 'CrewLink123!' }],
|
|
['crewlink:bootstrap', {}],
|
|
['crewlink:live', {}],
|
|
['crewlink:nearby', {}],
|
|
['darkchat:bootstrap', {}],
|
|
['easyshare:bootstrap', {}],
|
|
['easyshare:own-contact', {}],
|
|
['feather:bootstrap', {}],
|
|
['feather:feed', { limit: 20 }],
|
|
['feather:explore', { limit: 20 }],
|
|
['flare:bootstrap', {}],
|
|
['fliptok:bootstrap', {}],
|
|
['fliptok:feed', { limit: 20 }],
|
|
['fliptok:discover', { limit: 20 }],
|
|
['fliptok:activities', {}],
|
|
['gallery:list', {}],
|
|
['gallery:counts', {}],
|
|
['garage:vehicles', {}],
|
|
['garage:valet-state', {}],
|
|
['housing:overview', {}],
|
|
['housing:key-candidates', { action: 'give' }],
|
|
['mail:counts', {}],
|
|
['mail:list', { folder: 'inbox' }],
|
|
['mail:mailboxes', {}],
|
|
['map:getPlayerCoords', {}],
|
|
['map:markers', {}],
|
|
['marketplace:counts', {}],
|
|
['marketplace:list', {}],
|
|
['marketplace:list-own', {}],
|
|
['marketplace:list-inquiries', {}],
|
|
['marketplace:profile', {}],
|
|
['messages:conversations', {}],
|
|
['messages:gifs', { query: 'party' }],
|
|
['memos:list', {}],
|
|
['music:bootstrap', {}],
|
|
['notes:list', {}],
|
|
['pages:list', {}],
|
|
['pages:list-own', {}],
|
|
['pages:profile', {}],
|
|
['picstagram:bootstrap', {}],
|
|
['picstagram:feed', { limit: 20 }],
|
|
['picstagram:explore', { limit: 20 }],
|
|
['picstagram:saved', {}],
|
|
['picstagram:stories', {}],
|
|
['picstagram:activities', {}],
|
|
['radio:get', {}],
|
|
['skyride:bootstrap', {}],
|
|
['skyride:history', {}],
|
|
['skyride:get-player-coords', {}],
|
|
['weather:get', {}],
|
|
['weazel-news:context', {}],
|
|
['weazel-news:list', { category: null, offset: 0, search: '' }],
|
|
['weazel-news:manage-list', { offset: 0, search: '', status: 'all' }],
|
|
]
|
|
|
|
async function post(baseUrl, endpoint, body = {}) {
|
|
const response = await fetch(`${baseUrl}/api/${endpoint}`, {
|
|
body: JSON.stringify(body),
|
|
headers: { 'Content-Type': 'application/json' },
|
|
method: 'POST',
|
|
})
|
|
assert.equal(response.status, 200, endpoint)
|
|
return response.json()
|
|
}
|
|
|
|
async function expectSuccess(baseUrl, endpoint, body = {}, data = false) {
|
|
const result = await post(baseUrl, endpoint, body)
|
|
assert.equal(result.success, true, `${endpoint}: ${result.error ?? 'failed'}`)
|
|
if (data) assert.notEqual(result.data, undefined, `${endpoint}: missing data`)
|
|
return result.data
|
|
}
|
|
|
|
function expectItems(value, label, minimum = 1) {
|
|
assert(
|
|
Array.isArray(value) && value.length >= minimum,
|
|
`${label} did not include enough browser test data`,
|
|
)
|
|
}
|
|
|
|
function verifyBrowserTestData(dataByEndpoint) {
|
|
const development = dataByEndpoint.get('development:bootstrap')
|
|
expectItems(development.device.data.alarms.payload, 'clock alarms', 3)
|
|
expectItems(
|
|
development.device.data.media.payload.captures,
|
|
'camera captures',
|
|
2,
|
|
)
|
|
expectItems(development.memos, 'memos', 3)
|
|
expectItems(development.notes, 'notes', 4)
|
|
assert(
|
|
Object.keys(development.device.data.games.payload).length >= 7,
|
|
'games did not include saved browser test progress',
|
|
)
|
|
|
|
expectItems(dataByEndpoint.get('health:overview').days, 'health history', 7)
|
|
const crypto = dataByEndpoint.get('crypto:bootstrap')
|
|
expectItems(crypto.markets, 'crypto markets', 24)
|
|
assert.equal(typeof crypto.profile.priceAlerts, 'boolean')
|
|
assert.match(crypto.profile.walletKey, /^VX-(?:[A-F0-9]{4}-){3}[A-F0-9]{4}$/)
|
|
assert.equal(typeof crypto.markets[0].issuedSupply, 'string')
|
|
assert(crypto.markets.every((market) => typeof market.logo === 'string'))
|
|
assert(
|
|
crypto.markets.every(
|
|
(market) =>
|
|
Array.isArray(market.priceHistory) &&
|
|
market.priceHistory.length >= 2 &&
|
|
market.priceHistory.at(-1) === Number(market.price).toFixed(2),
|
|
),
|
|
)
|
|
assert(
|
|
Math.max(...crypto.markets.map((market) => Number(market.price))) >=
|
|
1000000,
|
|
)
|
|
assert(
|
|
Math.min(...crypto.markets.map((market) => Number(market.price))) <= 0.01,
|
|
)
|
|
const cryptoTick = dataByEndpoint.get('crypto:market-tick')
|
|
expectItems(cryptoTick, 'live crypto market tick', 24)
|
|
assert(
|
|
cryptoTick.every(
|
|
(market) =>
|
|
typeof market.updatedAt === 'number' &&
|
|
market.priceHistory.at(-1) === market.price,
|
|
),
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('billing:list').invoices,
|
|
'billing invoices',
|
|
3,
|
|
)
|
|
expectItems(dataByEndpoint.get('calendar:list'), 'calendar events', 5)
|
|
expectItems(dataByEndpoint.get('calls:recents'), 'recent calls', 5)
|
|
expectItems(dataByEndpoint.get('companies:list').companies, 'companies', 3)
|
|
expectItems(dataByEndpoint.get('contacts:list'), 'contacts', 10)
|
|
expectItems(
|
|
dataByEndpoint.get('crewlink:bootstrap').groups,
|
|
'CrewLink groups',
|
|
2,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('darkchat:bootstrap').conversations,
|
|
'DarkChat conversations',
|
|
)
|
|
expectItems(dataByEndpoint.get('feather:feed').items, 'Feather posts', 5)
|
|
expectItems(
|
|
dataByEndpoint.get('flare:bootstrap').suggestions,
|
|
'Flare suggestions',
|
|
5,
|
|
)
|
|
expectItems(dataByEndpoint.get('fliptok:feed').items, 'FlipTok videos', 2)
|
|
expectItems(dataByEndpoint.get('gallery:list'), 'gallery media', 30)
|
|
expectItems(
|
|
dataByEndpoint.get('garage:vehicles').vehicles,
|
|
'garage vehicles',
|
|
4,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('housing:overview').properties,
|
|
'housing properties',
|
|
5,
|
|
)
|
|
expectItems(dataByEndpoint.get('mail:list').items, 'inbox mail', 2)
|
|
expectItems(
|
|
dataByEndpoint.get('mail:mailboxes').mailboxes,
|
|
'custom mailboxes',
|
|
)
|
|
expectItems(dataByEndpoint.get('map:markers'), 'map markers')
|
|
expectItems(
|
|
dataByEndpoint.get('marketplace:list').items,
|
|
'CityMarkt listings',
|
|
4,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('messages:conversations'),
|
|
'message conversations',
|
|
5,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('music:bootstrap').serverTracks,
|
|
'music tracks',
|
|
3,
|
|
)
|
|
expectItems(dataByEndpoint.get('pages:list').items, 'Local Pages posts', 4)
|
|
expectItems(
|
|
dataByEndpoint.get('picstagram:feed').items,
|
|
'Picstagram posts',
|
|
2,
|
|
)
|
|
expectItems(dataByEndpoint.get('radio:get').history, 'radio history', 2)
|
|
expectItems(dataByEndpoint.get('skyride:history').items, 'SkyRide history', 2)
|
|
expectItems(
|
|
dataByEndpoint.get('weazel-news:list').items,
|
|
'Weazel News articles',
|
|
5,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('weazel-news:manage-list').items,
|
|
'Weazel News editorial articles',
|
|
7,
|
|
)
|
|
}
|
|
|
|
async function verifyStatefulActions(baseUrl) {
|
|
const cryptoBeforeTransfer = await expectSuccess(
|
|
baseUrl,
|
|
'crypto:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
const auroraBeforeTransfer = cryptoBeforeTransfer.holdings.find(
|
|
(holding) => holding.assetId === 'aurora',
|
|
)
|
|
const cryptoAfterTransfer = await expectSuccess(
|
|
baseUrl,
|
|
'crypto:transfer',
|
|
{
|
|
idempotencyKey: 'smoke-transfer-1',
|
|
marketId: 'aurora',
|
|
password: 'VaultX123!',
|
|
quantity: '0.5',
|
|
walletKey: 'VX-DEAD-BEEF-C0DE-2026',
|
|
},
|
|
true,
|
|
)
|
|
const auroraAfterTransfer = cryptoAfterTransfer.holdings.find(
|
|
(holding) => holding.assetId === 'aurora',
|
|
)
|
|
assert.equal(
|
|
Number(auroraAfterTransfer.quantity),
|
|
Number(auroraBeforeTransfer.quantity) - 0.5,
|
|
)
|
|
assert.equal(cryptoAfterTransfer.activity[0].type, 'transfer_out')
|
|
assert.equal(
|
|
cryptoAfterTransfer.activity[0].counterpartyKey,
|
|
'VX-DEAD-BEEF-C0DE-2026',
|
|
)
|
|
|
|
const companyCall = await expectSuccess(
|
|
baseUrl,
|
|
'companies:dial-service-line',
|
|
{ phoneNumber: '5551110001' },
|
|
true,
|
|
)
|
|
assert.equal(companyCall.direction, 'outgoing')
|
|
assert.equal(companyCall.otherNumber, '5551110001')
|
|
assert.equal(companyCall.state, 'ringing')
|
|
|
|
let gallery = await expectSuccess(baseUrl, 'gallery:list', {}, true)
|
|
assert(gallery.length >= 10, 'gallery:list did not include enough test media')
|
|
assert(
|
|
gallery.filter((item) => item.mediaType === 'video').length >= 3,
|
|
'gallery:list did not include enough test videos',
|
|
)
|
|
assert(
|
|
gallery
|
|
.filter((item) => item.mediaType === 'video')
|
|
.every((item) => typeof item.thumbnailUrl === 'string'),
|
|
'gallery:list returned a test video without a thumbnail',
|
|
)
|
|
const flipTokPhotos = gallery
|
|
.filter((item) => item.mediaType === 'photo')
|
|
.slice(0, 3)
|
|
const createdFlipTok = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:publish',
|
|
{
|
|
caption: 'Browser mock photo slideshow',
|
|
commentsEnabled: true,
|
|
coverTimeMs: 0,
|
|
customMusicUrl: '',
|
|
draft: false,
|
|
mediaId: flipTokPhotos[0].id,
|
|
mediaIds: flipTokPhotos.map((item) => item.id),
|
|
mediaType: 'photo',
|
|
musicTrack: '',
|
|
musicVolume: 0,
|
|
originalVolume: 0,
|
|
trimEndMs: null,
|
|
trimStartMs: 0,
|
|
visibility: 'public',
|
|
},
|
|
true,
|
|
)
|
|
let flipTokFeed = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:feed',
|
|
{ mode: 'for-you', offset: 0 },
|
|
true,
|
|
)
|
|
const photoFlipTok = flipTokFeed.items.find(
|
|
(item) => item.id === createdFlipTok.id,
|
|
)
|
|
assert.equal(photoFlipTok.media_type, 'photo')
|
|
assert.deepEqual(
|
|
photoFlipTok.media.map((item) => item.id),
|
|
flipTokPhotos.map((item) => item.id),
|
|
'fliptok:publish did not preserve photo order',
|
|
)
|
|
await expectSuccess(baseUrl, 'fliptok:comment', {
|
|
body: 'Browser mock scoped comment',
|
|
id: createdFlipTok.id,
|
|
})
|
|
const createdComments = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:comments',
|
|
{ id: createdFlipTok.id },
|
|
true,
|
|
)
|
|
assert(
|
|
createdComments.some((comment) => comment.body.includes('scoped comment')),
|
|
'fliptok:comment did not persist for its video',
|
|
)
|
|
const otherComments = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:comments',
|
|
{ id: 'fliptok-1' },
|
|
true,
|
|
)
|
|
assert(
|
|
!otherComments.some((comment) => comment.body.includes('scoped comment')),
|
|
'fliptok:comments leaked comments from another video',
|
|
)
|
|
await expectSuccess(baseUrl, 'fliptok:delete', { id: createdFlipTok.id })
|
|
flipTokFeed = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:feed',
|
|
{ mode: 'for-you', offset: 0 },
|
|
true,
|
|
)
|
|
assert(
|
|
!flipTokFeed.items.some((item) => item.id === createdFlipTok.id),
|
|
'fliptok:delete kept the removed video in the feed',
|
|
)
|
|
await expectSuccess(
|
|
baseUrl,
|
|
'gallery:favorite',
|
|
{ favorite: true, id: gallery[0].id },
|
|
true,
|
|
)
|
|
gallery = await expectSuccess(baseUrl, 'gallery:list', {}, true)
|
|
assert.equal(
|
|
gallery.find((item) => item.id === gallery[0].id)?.favorite,
|
|
true,
|
|
)
|
|
const articlePhotos = gallery
|
|
.filter((item) => item.mediaType === 'photo')
|
|
.slice(0, 7)
|
|
assert.equal(
|
|
articlePhotos.length,
|
|
7,
|
|
'gallery:list did not include enough photos for Weazel News',
|
|
)
|
|
const weazelContext = await expectSuccess(
|
|
baseUrl,
|
|
'weazel-news:context',
|
|
{},
|
|
true,
|
|
)
|
|
assert.equal(weazelContext.maximumImages, 6)
|
|
const createdArticleResponse = await expectSuccess(
|
|
baseUrl,
|
|
'weazel-news:create',
|
|
{
|
|
body: 'Created by the browser mock smoke test with several photos.',
|
|
category: 'news',
|
|
imageMediaIds: articlePhotos.slice(0, 3).map((item) => item.id),
|
|
status: 'published',
|
|
title: 'Browser test Weazel article',
|
|
},
|
|
true,
|
|
)
|
|
const createdArticle = createdArticleResponse.article
|
|
assert.deepEqual(
|
|
createdArticle.images.map((image) => image.mediaId),
|
|
articlePhotos.slice(0, 3).map((item) => item.id),
|
|
'weazel-news:create did not preserve image order',
|
|
)
|
|
assert.equal(createdArticle.imageMediaId, articlePhotos[0].id)
|
|
|
|
const updatedArticleResponse = await expectSuccess(
|
|
baseUrl,
|
|
'weazel-news:update',
|
|
{
|
|
body: createdArticle.body,
|
|
category: 'business',
|
|
id: createdArticle.id,
|
|
imageMediaIds: [articlePhotos[2].id, articlePhotos[0].id],
|
|
revision: createdArticle.revision,
|
|
status: 'draft',
|
|
title: 'Updated browser test Weazel article',
|
|
},
|
|
true,
|
|
)
|
|
const updatedArticle = updatedArticleResponse.article
|
|
assert.deepEqual(
|
|
updatedArticle.images.map((image) => image.mediaId),
|
|
[articlePhotos[2].id, articlePhotos[0].id],
|
|
'weazel-news:update did not preserve the reordered images',
|
|
)
|
|
assert.equal(updatedArticle.imageMediaId, articlePhotos[2].id)
|
|
|
|
const loadedArticleResponse = await expectSuccess(
|
|
baseUrl,
|
|
'weazel-news:get',
|
|
{ id: updatedArticle.id, manage: true },
|
|
true,
|
|
)
|
|
assert.deepEqual(loadedArticleResponse.article.images, updatedArticle.images)
|
|
|
|
const tooManyImages = await post(baseUrl, 'weazel-news:create', {
|
|
body: 'This article must be rejected because it has too many photos.',
|
|
category: 'news',
|
|
imageMediaIds: articlePhotos.map((item) => item.id),
|
|
status: 'draft',
|
|
title: 'Invalid Weazel article',
|
|
})
|
|
assert.equal(tooManyImages.success, false)
|
|
assert.equal(tooManyImages.error, 'invalid_attachment')
|
|
|
|
const firstSmsPhoto = await expectSuccess(
|
|
baseUrl,
|
|
'messages:send',
|
|
{
|
|
body: '',
|
|
mediaAssetId: String(articlePhotos[0].id),
|
|
messageType: 'image',
|
|
phoneNumber: '5558675309',
|
|
},
|
|
true,
|
|
)
|
|
const captionedSmsPhoto = await expectSuccess(
|
|
baseUrl,
|
|
'messages:send',
|
|
{
|
|
body: 'Two photos from one composer draft.',
|
|
mediaAssetId: String(articlePhotos[1].id),
|
|
messageType: 'image',
|
|
phoneNumber: '5558675309',
|
|
},
|
|
true,
|
|
)
|
|
assert.notEqual(firstSmsPhoto.id, captionedSmsPhoto.id)
|
|
assert.equal(captionedSmsPhoto.body, 'Two photos from one composer draft.')
|
|
const smsPhotoThread = await expectSuccess(
|
|
baseUrl,
|
|
'messages:thread',
|
|
{ phoneNumber: '5558675309' },
|
|
true,
|
|
)
|
|
assert(smsPhotoThread.some((message) => message.id === firstSmsPhoto.id))
|
|
assert(smsPhotoThread.some((message) => message.id === captionedSmsPhoto.id))
|
|
|
|
await expectSuccess(baseUrl, 'weazel-news:delete', {
|
|
id: updatedArticle.id,
|
|
revision: updatedArticle.revision,
|
|
})
|
|
|
|
const memoBootstrap = await expectSuccess(
|
|
baseUrl,
|
|
'development:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert(
|
|
Array.isArray(memoBootstrap.memos) && memoBootstrap.memos.length >= 3,
|
|
'development:bootstrap did not include realistic memo data',
|
|
)
|
|
assert(
|
|
memoBootstrap.memos.every(
|
|
(memo) =>
|
|
typeof memo.url === 'string' &&
|
|
memo.url.length > 0 &&
|
|
Array.isArray(memo.waveform) &&
|
|
memo.waveform.length >= 8,
|
|
),
|
|
'development:bootstrap returned an invalid memo response shape',
|
|
)
|
|
|
|
let memos = await expectSuccess(baseUrl, 'memos:list', {}, true)
|
|
const capturedMemo = await expectSuccess(
|
|
baseUrl,
|
|
'memos:devCapture',
|
|
{
|
|
audioDataUrl: 'data:audio/webm;base64,T2dnUw==',
|
|
correlationId: 'browser-smoke-memo',
|
|
durationMs: 1_250,
|
|
mimeType: 'audio/webm;codecs=opus',
|
|
note: 'Recorded in the browser preview.',
|
|
pinned: false,
|
|
title: 'Browser recording',
|
|
waveform: Array(16).fill(0.35),
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(capturedMemo.title, 'Browser recording')
|
|
assert.equal(capturedMemo.mimeType, 'audio/webm')
|
|
assert.equal(capturedMemo.sizeBytes, 4)
|
|
assert.match(capturedMemo.id, /^[0-9a-f-]{36}$/)
|
|
memos = await expectSuccess(baseUrl, 'memos:list', {}, true)
|
|
assert(
|
|
memos.some((item) => item.id === capturedMemo.id),
|
|
'memos:devCapture did not persist',
|
|
)
|
|
await expectSuccess(baseUrl, 'memos:delete', { id: capturedMemo.id }, true)
|
|
memos = await expectSuccess(baseUrl, 'memos:list', {}, true)
|
|
|
|
const memo = memos.find((item) => !item.pinned)
|
|
assert(memo, 'memos:list did not return an editable memo')
|
|
const updatedMemo = await expectSuccess(
|
|
baseUrl,
|
|
'memos:update',
|
|
{
|
|
id: memo.id,
|
|
note: 'Updated by the browser mock smoke test.',
|
|
pinned: true,
|
|
revision: memo.revision,
|
|
title: 'Updated browser memo',
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(updatedMemo.id, memo.id)
|
|
assert.equal(updatedMemo.title, 'Updated browser memo')
|
|
assert.equal(updatedMemo.note, 'Updated by the browser mock smoke test.')
|
|
assert.equal(updatedMemo.pinned, true)
|
|
assert.equal(updatedMemo.revision, memo.revision + 1)
|
|
assert(Array.isArray(updatedMemo.waveform) && updatedMemo.waveform.length > 0)
|
|
|
|
const deletedMemo = await expectSuccess(
|
|
baseUrl,
|
|
'memos:delete',
|
|
{ id: updatedMemo.id },
|
|
true,
|
|
)
|
|
assert.deepEqual(deletedMemo, { id: updatedMemo.id })
|
|
memos = await expectSuccess(baseUrl, 'memos:list', {}, true)
|
|
assert(
|
|
!memos.some((item) => item.id === updatedMemo.id),
|
|
'memos:delete did not persist',
|
|
)
|
|
|
|
const noteId = `browser-note-${Date.now()}`
|
|
let notes = await expectSuccess(
|
|
baseUrl,
|
|
'notes:create',
|
|
{
|
|
body: 'Created by the browser mock smoke test.',
|
|
id: noteId,
|
|
title: 'Browser test',
|
|
},
|
|
true,
|
|
)
|
|
assert(
|
|
notes.some((note) => note.id === noteId),
|
|
'notes:create did not persist',
|
|
)
|
|
notes = await expectSuccess(
|
|
baseUrl,
|
|
'notes:update',
|
|
{
|
|
body: 'Updated browser test note.',
|
|
id: noteId,
|
|
title: 'Browser test updated',
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(
|
|
notes.find((note) => note.id === noteId)?.title,
|
|
'Browser test updated',
|
|
)
|
|
notes = await expectSuccess(baseUrl, 'notes:delete', { id: noteId }, true)
|
|
assert(
|
|
!notes.some((note) => note.id === noteId),
|
|
'notes:delete did not persist',
|
|
)
|
|
|
|
const contact = await expectSuccess(
|
|
baseUrl,
|
|
'contacts:save',
|
|
{
|
|
email: 'browser.tester',
|
|
name: 'Browser Tester',
|
|
phoneNumber: '5552223333',
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(contact.email, 'browser.tester@ifruit.com')
|
|
await expectSuccess(
|
|
baseUrl,
|
|
'contacts:favorite',
|
|
{ favorite: true, id: contact.id },
|
|
true,
|
|
)
|
|
let contacts = await expectSuccess(baseUrl, 'contacts:list', {}, true)
|
|
const savedContact = contacts.find((item) => item.id === contact.id)
|
|
assert.equal(savedContact?.favorite, true)
|
|
assert.equal(savedContact?.email, 'browser.tester@ifruit.com')
|
|
await expectSuccess(baseUrl, 'contacts:delete', { id: contact.id })
|
|
contacts = await expectSuccess(baseUrl, 'contacts:list', {}, true)
|
|
assert(
|
|
!contacts.some((item) => item.id === contact.id),
|
|
'contacts:delete did not persist',
|
|
)
|
|
|
|
const event = await expectSuccess(
|
|
baseUrl,
|
|
'calendar:create',
|
|
{
|
|
allDay: false,
|
|
description: 'Stateful browser mock check',
|
|
endsAt: 2_000_003_600,
|
|
location: 'Legion Square',
|
|
reminderMinutes: 15,
|
|
startsAt: 2_000_000_000,
|
|
title: 'Browser test event',
|
|
},
|
|
true,
|
|
)
|
|
let events = await expectSuccess(
|
|
baseUrl,
|
|
'calendar:list',
|
|
{ endsAt: 4_102_444_800, startsAt: 0 },
|
|
true,
|
|
)
|
|
const storedEvent = events.find((item) => item.id === event.id)
|
|
assert(storedEvent, 'calendar:create did not persist')
|
|
await expectSuccess(baseUrl, 'calendar:update', {
|
|
...storedEvent,
|
|
endsAt: storedEvent.endsAt / 1000,
|
|
startsAt: storedEvent.startsAt / 1000,
|
|
title: 'Updated browser test event',
|
|
})
|
|
events = await expectSuccess(
|
|
baseUrl,
|
|
'calendar:list',
|
|
{ endsAt: 4_102_444_800, startsAt: 0 },
|
|
true,
|
|
)
|
|
assert.equal(
|
|
events.find((item) => item.id === event.id)?.title,
|
|
'Updated browser test event',
|
|
)
|
|
await expectSuccess(baseUrl, 'calendar:delete', { id: event.id })
|
|
|
|
const marker = await expectSuccess(
|
|
baseUrl,
|
|
'map:create-marker',
|
|
{
|
|
color: '#2dd4bf',
|
|
coords: { x: 215.2, y: -810.1, z: 30.7 },
|
|
icon: 'pin',
|
|
label: 'Browser test marker',
|
|
},
|
|
true,
|
|
)
|
|
let markers = await expectSuccess(baseUrl, 'map:markers', {}, true)
|
|
assert(
|
|
markers.some((item) => item.id === marker.id),
|
|
'map:create-marker did not persist',
|
|
)
|
|
await expectSuccess(baseUrl, 'map:delete-marker', { id: marker.id })
|
|
markers = await expectSuccess(baseUrl, 'map:markers', {}, true)
|
|
assert(
|
|
!markers.some((item) => item.id === marker.id),
|
|
'map:delete-marker did not persist',
|
|
)
|
|
|
|
const bankingBefore = await expectSuccess(
|
|
baseUrl,
|
|
'banking:overview',
|
|
{},
|
|
true,
|
|
)
|
|
const bankingAfter = await expectSuccess(
|
|
baseUrl,
|
|
'banking:transfer',
|
|
{ amount: 125, phoneNumber: '5551110001' },
|
|
true,
|
|
)
|
|
assert.equal(bankingAfter.bank, bankingBefore.bank - 125)
|
|
|
|
const health = await expectSuccess(baseUrl, 'health:overview', {}, true)
|
|
assert.equal(health.dailyStepGoal, 8000)
|
|
assert.equal(health.days.length, 7)
|
|
assert.equal(health.days.at(-1).steps, 6420)
|
|
assert.equal('snapshot' in health, false)
|
|
|
|
const medicalId = await expectSuccess(
|
|
baseUrl,
|
|
'health:save-profile',
|
|
{
|
|
allergies: 'Penicillin',
|
|
bloodType: 'A+',
|
|
conditions: 'Asthma',
|
|
emergencyName: 'Jamie Morgan',
|
|
emergencyPhone: '5550102211',
|
|
emergencyRelation: 'Sibling',
|
|
medication: 'Inhaler',
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(medicalId.bloodType, 'A+')
|
|
assert.equal(medicalId.allergies, 'Penicillin')
|
|
|
|
let radio = await expectSuccess(
|
|
baseUrl,
|
|
'radio:connect',
|
|
{ frequency: 42.5, secondaryFrequency: 7.25 },
|
|
true,
|
|
)
|
|
assert.equal(radio.frequency, 42.5)
|
|
radio = await expectSuccess(baseUrl, 'radio:set-volume', { volume: 44 }, true)
|
|
assert.equal(radio.volume, 44)
|
|
radio = await expectSuccess(
|
|
baseUrl,
|
|
'radio:set-speaker',
|
|
{ enabled: true },
|
|
true,
|
|
)
|
|
assert.equal(radio.speakerEnabled, true)
|
|
await expectSuccess(baseUrl, 'radio:disconnect')
|
|
|
|
const playlistState = await expectSuccess(
|
|
baseUrl,
|
|
'music:create-playlist',
|
|
{ name: 'Browser Test Mix' },
|
|
true,
|
|
)
|
|
const playlist = playlistState.playlists.find(
|
|
(item) => item.name === 'Browser Test Mix',
|
|
)
|
|
assert(playlist, 'music:create-playlist did not persist')
|
|
await expectSuccess(
|
|
baseUrl,
|
|
'music:rename-playlist',
|
|
{ id: playlist.id, name: 'Updated Browser Mix' },
|
|
true,
|
|
)
|
|
await expectSuccess(
|
|
baseUrl,
|
|
'music:delete-playlist',
|
|
{ id: playlist.id },
|
|
true,
|
|
)
|
|
|
|
await expectSuccess(baseUrl, 'sim:eject')
|
|
let bootstrap = await expectSuccess(
|
|
baseUrl,
|
|
'development:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert.equal(bootstrap.device.sim, null)
|
|
const simConfirmation = await post(baseUrl, 'sim:insert', {
|
|
imei: '356938035643810',
|
|
})
|
|
assert.deepEqual(simConfirmation, {
|
|
error: 'confirmation_required',
|
|
success: false,
|
|
})
|
|
await expectSuccess(baseUrl, 'sim:insert', {
|
|
confirmed: true,
|
|
imei: '356938035643810',
|
|
})
|
|
bootstrap = await expectSuccess(baseUrl, 'development:bootstrap', {}, true)
|
|
assert.equal(bootstrap.device.sim.number, '5559876543')
|
|
|
|
const payphoneCall = await expectSuccess(
|
|
baseUrl,
|
|
'payphone:dial',
|
|
{ phoneNumber: '5551110001' },
|
|
true,
|
|
)
|
|
assert.equal(payphoneCall.state, 'connected')
|
|
await expectSuccess(baseUrl, 'payphone:hangup')
|
|
|
|
const mailbox = await expectSuccess(
|
|
baseUrl,
|
|
'mail:create-mailbox',
|
|
{ name: 'Browser Test' },
|
|
true,
|
|
)
|
|
const mailboxes = await expectSuccess(baseUrl, 'mail:mailboxes', {}, true)
|
|
assert(
|
|
mailboxes.mailboxes.some((item) => item.id === mailbox.id),
|
|
'mail:create-mailbox did not update the mock mailbox list',
|
|
)
|
|
await expectSuccess(baseUrl, 'mail:move', {
|
|
id: 2,
|
|
mailboxId: mailbox.id,
|
|
})
|
|
const mailboxItems = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: `mailbox:${mailbox.id}` },
|
|
true,
|
|
)
|
|
assert(
|
|
mailboxItems.items.some((message) => message.id === 2),
|
|
'mail:move did not add the message to the custom mailbox',
|
|
)
|
|
await expectSuccess(baseUrl, 'mail:move', { id: 2, mailboxId: 0 })
|
|
await expectSuccess(baseUrl, 'mail:delete-mailbox', { id: mailbox.id })
|
|
|
|
const draft = await expectSuccess(
|
|
baseUrl,
|
|
'mail:save-draft',
|
|
{
|
|
body: 'Browser test body',
|
|
recipients: ['alex@ifruit.com'],
|
|
subject: 'Browser test mail',
|
|
},
|
|
true,
|
|
)
|
|
const storedDraft = await expectSuccess(
|
|
baseUrl,
|
|
'mail:get-draft',
|
|
{ id: draft.id },
|
|
true,
|
|
)
|
|
assert.equal(storedDraft.subject, 'Browser test mail')
|
|
await expectSuccess(baseUrl, 'mail:delete-many', {
|
|
folder: 'drafts',
|
|
ids: [draft.id],
|
|
})
|
|
const drafts = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'drafts' },
|
|
true,
|
|
)
|
|
assert.equal(drafts.items.length, 0, 'mail:delete-many kept selected draft')
|
|
|
|
await expectSuccess(baseUrl, 'mail:delete-many', {
|
|
folder: 'inbox',
|
|
ids: [1],
|
|
})
|
|
const inbox = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'inbox' },
|
|
true,
|
|
)
|
|
assert(
|
|
inbox.items.some((message) => message.id === 2),
|
|
'mail:delete-many removed a non-selected inbox message',
|
|
)
|
|
const trash = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'trash' },
|
|
true,
|
|
)
|
|
assert(
|
|
trash.items.some((message) => message.id === 1),
|
|
'mail:delete-many did not move selected inbox mail to trash',
|
|
)
|
|
|
|
await expectSuccess(baseUrl, 'mail:delete-many', {
|
|
folder: 'sent',
|
|
ids: [3],
|
|
})
|
|
const sent = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'sent' },
|
|
true,
|
|
)
|
|
assert.equal(sent.items.length, 0, 'mail:delete-many kept selected sent mail')
|
|
|
|
await expectSuccess(baseUrl, 'mail:delete-many', {
|
|
folder: 'trash',
|
|
ids: [1],
|
|
})
|
|
const remainingTrash = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'trash' },
|
|
true,
|
|
)
|
|
assert(
|
|
!remainingTrash.items.some((message) => message.id === 1),
|
|
'mail:delete-many kept permanently deleted trash mail',
|
|
)
|
|
assert(
|
|
remainingTrash.items.some((message) => message.id === 4),
|
|
'mail:delete-many removed non-selected trash mail',
|
|
)
|
|
|
|
const flareBeforeDelete = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert(flareBeforeDelete.profile, 'flare:bootstrap did not include a profile')
|
|
assert(
|
|
flareBeforeDelete.profile.photoMediaIds.length >= 1 &&
|
|
flareBeforeDelete.profile.photoMediaIds.length <= 6,
|
|
'flare:bootstrap profile did not include one to six gallery photos',
|
|
)
|
|
assert.equal(
|
|
flareBeforeDelete.profile.photoMediaIds.length,
|
|
flareBeforeDelete.profile.photoUrls.length,
|
|
'flare:bootstrap profile photo IDs and URLs were out of sync',
|
|
)
|
|
const galleryPhotoUrls = new Set(
|
|
gallery
|
|
.filter((item) => item.mediaType === 'photo')
|
|
.map((item) => item.url),
|
|
)
|
|
assert(
|
|
flareBeforeDelete.profile.photoUrls.every((url) =>
|
|
galleryPhotoUrls.has(url),
|
|
),
|
|
'flare:bootstrap profile used photos outside the phone gallery',
|
|
)
|
|
assert(
|
|
flareBeforeDelete.suggestions.every(
|
|
(profile) =>
|
|
profile.photoUrls.length >= 1 &&
|
|
profile.photoUrls.every((url) => galleryPhotoUrls.has(url)),
|
|
),
|
|
'Flare suggestions used photos outside the phone gallery',
|
|
)
|
|
assert(
|
|
flareBeforeDelete.matches.length > 0,
|
|
'flare:bootstrap did not include a deletable match',
|
|
)
|
|
assert(
|
|
flareBeforeDelete.matches.every(
|
|
(match) =>
|
|
match.profile.photoUrls.length >= 1 &&
|
|
match.profile.photoUrls.every((url) => galleryPhotoUrls.has(url)),
|
|
),
|
|
'Flare matches used photos outside the phone gallery',
|
|
)
|
|
const [removedProfilePhotoId, retainedProfilePhotoId] =
|
|
flareBeforeDelete.profile.photoMediaIds
|
|
await expectSuccess(baseUrl, 'gallery:delete', {
|
|
id: removedProfilePhotoId,
|
|
})
|
|
const flareAfterGalleryDelete = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert.deepEqual(
|
|
flareAfterGalleryDelete.profile.photoMediaIds,
|
|
[retainedProfilePhotoId],
|
|
'gallery:delete did not remove the deleted photo from the Flare profile',
|
|
)
|
|
assert.deepEqual(
|
|
flareAfterGalleryDelete.profile.photoUrls,
|
|
[gallery.find((item) => item.id === retainedProfilePhotoId)?.url],
|
|
'gallery:delete left Flare photo IDs and URLs out of sync',
|
|
)
|
|
const rejectedLastPhotoDelete = await post(baseUrl, 'gallery:delete', {
|
|
id: retainedProfilePhotoId,
|
|
})
|
|
assert.deepEqual(rejectedLastPhotoDelete, {
|
|
error: 'profile_photo_required',
|
|
success: false,
|
|
})
|
|
const nonProfilePhotoId = gallery.find(
|
|
(item) =>
|
|
item.mediaType === 'photo' &&
|
|
!flareBeforeDelete.profile.photoMediaIds.includes(item.id),
|
|
)?.id
|
|
const rejectedBulkLastPhotoDelete = await post(
|
|
baseUrl,
|
|
'gallery:delete-many',
|
|
{
|
|
correlationId: 'flare-last-photo-protection',
|
|
ids: [retainedProfilePhotoId, nonProfilePhotoId],
|
|
},
|
|
)
|
|
assert.deepEqual(rejectedBulkLastPhotoDelete, {
|
|
error: 'profile_photo_required',
|
|
success: false,
|
|
})
|
|
const swipedProfile = flareBeforeDelete.suggestions[0]
|
|
await expectSuccess(baseUrl, 'flare:swipe', {
|
|
choice: 'pass',
|
|
targetId: swipedProfile.id,
|
|
})
|
|
const flareAfterSwipe = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert(
|
|
!flareAfterSwipe.suggestions.some(
|
|
(profile) => profile.id === swipedProfile.id,
|
|
),
|
|
'flare:swipe did not filter the swiped profile',
|
|
)
|
|
await expectSuccess(baseUrl, 'flare:delete-profile')
|
|
const flareAfterDelete = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert.equal(flareAfterDelete.profile, null)
|
|
assert.deepEqual(flareAfterDelete.suggestions, [])
|
|
assert.deepEqual(flareAfterDelete.likes, [])
|
|
assert.deepEqual(flareAfterDelete.matches, [])
|
|
const repeatedFlareDelete = await post(baseUrl, 'flare:delete-profile')
|
|
assert.deepEqual(repeatedFlareDelete, {
|
|
error: 'profile_not_found',
|
|
success: false,
|
|
})
|
|
const rejectedEmptyFlare = await post(baseUrl, 'flare:save-profile', {
|
|
...flareBeforeDelete.profile,
|
|
photoMediaIds: [],
|
|
photoUrls: undefined,
|
|
})
|
|
assert.deepEqual(rejectedEmptyFlare, {
|
|
error: 'invalid_profile_photos',
|
|
success: false,
|
|
})
|
|
const flareStillDeleted = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert.equal(
|
|
flareStillDeleted.profile,
|
|
null,
|
|
'an invalid empty Flare profile was persisted',
|
|
)
|
|
const recreatedFlare = await expectSuccess(
|
|
baseUrl,
|
|
'flare:save-profile',
|
|
{
|
|
...flareBeforeDelete.profile,
|
|
photoMediaIds: [retainedProfilePhotoId],
|
|
photoUrls: undefined,
|
|
},
|
|
true,
|
|
)
|
|
assert(
|
|
recreatedFlare.suggestions.some(
|
|
(profile) => profile.id === swipedProfile.id,
|
|
),
|
|
'recreated Flare profile retained a deleted swipe filter',
|
|
)
|
|
assert.deepEqual(
|
|
recreatedFlare.profile.photoMediaIds,
|
|
[retainedProfilePhotoId],
|
|
'recreated Flare profile did not retain its valid gallery photo',
|
|
)
|
|
|
|
await expectSuccess(baseUrl, 'account:logout')
|
|
const signedOutFlareBootstrap = await post(baseUrl, 'flare:bootstrap')
|
|
assert.deepEqual(signedOutFlareBootstrap, {
|
|
error: 'not_authenticated',
|
|
success: false,
|
|
})
|
|
const signedOutFlareDelete = await post(baseUrl, 'flare:delete-profile')
|
|
assert.deepEqual(signedOutFlareDelete, {
|
|
error: 'not_authenticated',
|
|
success: false,
|
|
})
|
|
}
|
|
|
|
async function main() {
|
|
const server = app.listen(0, '127.0.0.1')
|
|
await once(server, 'listening')
|
|
const address = server.address()
|
|
const baseUrl = `http://127.0.0.1:${address.port}`
|
|
|
|
try {
|
|
const dataByEndpoint = new Map()
|
|
for (const [endpoint, body] of browserDataRequests) {
|
|
dataByEndpoint.set(
|
|
endpoint,
|
|
await expectSuccess(baseUrl, endpoint, body, true),
|
|
)
|
|
}
|
|
verifyBrowserTestData(dataByEndpoint)
|
|
|
|
await verifyStatefulActions(baseUrl)
|
|
|
|
const lifecycleEndpoints = [
|
|
'camera:setActive',
|
|
'camera:setLocked',
|
|
'camera:setFacing',
|
|
'camera:setFlash',
|
|
'camera:setFocus',
|
|
'camera:setOrientation',
|
|
'camera:setZoom',
|
|
'close',
|
|
'custom-app:lifecycle',
|
|
'device:notification-open',
|
|
'notification:focus',
|
|
'sim:picker-close',
|
|
'ui:opened',
|
|
'ui:ready',
|
|
]
|
|
for (const endpoint of lifecycleEndpoints) {
|
|
await expectSuccess(baseUrl, endpoint)
|
|
}
|
|
|
|
const unknown = await post(baseUrl, 'development:missing-mock', {})
|
|
assert.deepEqual(unknown, {
|
|
error: 'mock_endpoint_missing',
|
|
success: false,
|
|
})
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()))
|
|
})
|
|
}
|
|
|
|
console.log(
|
|
`Verified ${browserDataRequests.length} browser data endpoints and stateful app actions.`,
|
|
)
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exitCode = 1
|
|
})
|