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) => { d.game.game.ID = 8 }, (d: ReturnType) => { d.music.gamePlaylistId = 100 }, (d: ReturnType) => { d.music.playlists[1]!.tracks[0]!.filename = '' }, (d: ReturnType) => { d.plan.plan.steps[2]!.timerDurationSeconds = 'bad' }, (d: ReturnType) => { d.plan.plan.steps[0]!.mode = 'SCREEN_MODE_UNKNOWN' }, (d: ReturnType) => { 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: '' }), /токен/) })