Files
evening_detective_screen/electron/screen-package.test.ts
T
2026-10-17 19:08:09 +07:00

239 lines
7.9 KiB
TypeScript

import assert from 'node:assert/strict'
import { test } from 'node:test'
import { buildScreenPackage, prepareScreenPackage } from './screen-package.js'
const server = 'https://example.test'
function fixture() {
const playlist = (ID: number, filename: string) => ({
ID,
name: `Playlist ${ID}`,
tracks: [{ ID: ID * 10, name: 'Track', filename, position: 0 }],
})
return {
game: {
game: {
ID: 7,
name: 'Игра',
teams: [{ password: 'PRIVATE' }],
scenario: {
image: 'cover.png',
music_playlist_id: 1,
story: {
background: '',
introduction: { audio: 'intro.mp3' },
conclusion: { video: 'https://media.test/final.mp4' },
places: [{ image: 'PRIVATE' }],
},
},
},
},
plan: {
plan: {
currentStep: 1,
steps: [
{ mode: 'SCREEN_MODE_COVER' },
{ mode: 'SCREEN_MODE_TEXT', text: 'Привет' },
{ mode: 'SCREEN_MODE_TIMER', timerLabel: 'Перерыв', timerDurationSeconds: '60' },
],
},
},
music: {
scenarioPlaylistId: 1,
gamePlaylistId: 2,
playlists: [
playlist(99, 'PRIVATE'),
playlist(2, 'intro.mp3'),
playlist(1, `${server}/api/files/intro.mp3`),
],
state: {
playlist: playlist(3, 'selected.mp3'),
positionMs: '1000',
playback: 'MUSIC_PLAYBACK_PLAYING',
stateVersion: '4',
},
},
}
}
const build = (data = fixture()) => buildScreenPackage(server, 7, data)
test('пакет содержит только материалы показа и связанные плейлисты', () => {
const result = build()
assert.equal(result.formatVersion, 1)
assert.equal(result.media.background, result.media.cover)
assert.deepEqual(
result.playlists.map((p) => p.id),
[1, 2, 3],
)
assert.equal(result.assets.length, 4)
assert.equal(result.playlists[0]!.tracks[0]!.assetId, result.playlists[1]!.tracks[0]!.assetId)
assert.equal(result.steps[2]!.timerDurationSeconds, 60)
assert.deepEqual(
result.steps.map((s) => s.mode),
[1, 8, 7],
)
for (const excluded of [
'PRIVATE',
'currentStep',
'positionMs',
'playback',
'stateVersion',
'token',
'password',
'teams',
])
assert.ok(!JSON.stringify(result).includes(excluded))
assert.match(result.metadataVersion, /^[a-f0-9]{64}$/)
})
test('отпечаток стабилен, зависит от метаданных, исключает живые команды', () => {
const original = build().metadataVersion
const data = fixture()
data.music.playlists.reverse()
data.music.state.positionMs = '5000'
data.plan.plan.currentStep = 2
assert.equal(build(data).metadataVersion, original)
data.plan.plan.steps[1]!.text = 'Новый текст'
assert.notEqual(build(data).metadataVersion, original)
const other = fixture()
other.game.game.scenario.story.conclusion.video = 'https://media.test/new.mp4'
assert.notEqual(build(other).metadataVersion, original)
assert.equal(buildScreenPackage(`${server}/`, 7, fixture()).metadataVersion, original)
})
test('треки упорядочены по позиции и ID, текущий плейлист не обязан входить в библиотеку', () => {
const data = fixture()
data.music.state.playlist.tracks = [
{ ID: 32, name: 'B', filename: 'b.mp3', position: 1 },
{ ID: 31, name: 'A', filename: 'a.mp3', position: 0 },
]
assert.deepEqual(
build(data).playlists[2]!.tracks.map((t) => t.id),
[31, 32],
)
})
test('пустой план и необязательные материалы допустимы', () => {
const result = buildScreenPackage(server, 7, {
game: { game: { ID: 7, scenario: { story: {} } } },
plan: {},
music: {},
})
assert.deepEqual(result.assets, [])
assert.deepEqual(result.steps, [])
assert.deepEqual(result.playlists, [])
assert.equal(result.media.intro, null)
})
test('неполные или некорректные ответы не становятся пакетом', () => {
for (const mutate of [
(d: ReturnType<typeof fixture>) => {
d.game.game.ID = 8
},
(d: ReturnType<typeof fixture>) => {
d.music.gamePlaylistId = 100
},
(d: ReturnType<typeof fixture>) => {
d.music.playlists[1]!.tracks[0]!.filename = ''
},
(d: ReturnType<typeof fixture>) => {
d.plan.plan.steps[2]!.timerDurationSeconds = 'bad'
},
(d: ReturnType<typeof fixture>) => {
d.plan.plan.steps[0]!.mode = 'SCREEN_MODE_UNKNOWN'
},
(d: ReturnType<typeof fixture>) => {
d.plan.plan.steps[1]!.text = ''
},
]) {
const data = fixture()
mutate(data)
assert.throws(() => build(data))
}
assert.throws(() => buildScreenPackage(server, 7, { game: {}, plan: {}, music: {} }))
assert.throws(() =>
buildScreenPackage(server, 7, { game: { game: { ID: 7, scenario: {} } }, plan: {}, music: {} }),
)
})
test('получает данные через три существующих API без обращения к внешним медиа', async () => {
const data = fixture()
const calls: string[] = []
const mock: typeof fetch = async (input, init) => {
const url = String(input)
calls.push(url)
assert.equal(new Headers(init?.headers).get('Authorization'), 'Bearer SECRET')
assert.equal(init?.redirect, 'error')
assert.equal(init?.credentials, 'omit')
assert.ok(init?.signal)
return Response.json(
url.endsWith('/music') ? data.music : url.endsWith('/screen-plan') ? data.plan : data.game,
)
}
const result = await prepareScreenPackage({ server, gameId: 7, token: 'SECRET' }, mock)
assert.deepEqual(calls.sort(), [
`${server}/api/games/7`,
`${server}/api/games/7/music`,
`${server}/api/games/7/screen-plan`,
])
assert.deepEqual(result, build(data))
assert.ok(!JSON.stringify(result).includes('SECRET'))
})
test('отказы API и некорректный JSON не выдают частичный пакет или ответ с секретом', async () => {
for (const status of [401, 403, 404, 500]) {
await assert.rejects(
prepareScreenPackage(
{ server, gameId: 7, token: 'SECRET' },
async () => new Response('SECRET', { status }),
),
/авторизация|доступа|HTTP/,
)
}
await assert.rejects(
prepareScreenPackage(
{ server, gameId: 7, token: 'SECRET' },
async () => new Response('SECRET'),
),
/Некорректный JSON/,
)
await assert.rejects(
prepareScreenPackage({ server, gameId: 7, token: 'SECRET' }, async () => {
throw new Error('SECRET')
}),
/^Error: Сервер недоступен$/,
)
})
test('отмена и недопустимые адреса обрабатываются до выдачи пакета', async () => {
const controller = new AbortController()
controller.abort()
await assert.rejects(
prepareScreenPackage(
{ server, gameId: 7, token: 'SECRET', signal: controller.signal },
async (_input, init) => {
init?.signal?.throwIfAborted()
return Response.json({})
},
),
/отменена/,
)
for (const address of [
'file:///etc/passwd',
'https://user:password@example.test',
`${server}?token=SECRET`,
]) {
await assert.rejects(
prepareScreenPackage({ server: address, gameId: 7, token: 'SECRET' }),
/Недопустимый|секрет/,
)
}
const data = fixture()
data.game.game.scenario.story.conclusion.video = 'https://media.test/video?access_token=SECRET'
assert.throws(() => build(data), /секрет/)
await assert.rejects(
prepareScreenPackage({ server, gameId: 0, token: 'SECRET' }),
/идентификатор/,
)
await assert.rejects(prepareScreenPackage({ server, gameId: 7, token: '' }), /токен/)
})