107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { once } from 'node:events'
|
|
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { test } from 'node:test'
|
|
import { WebSocket } from 'ws'
|
|
import { Controller } from './controller.js'
|
|
import { LanServer } from './lan.js'
|
|
|
|
test('LAN: код, подтверждение, cookie, WebSocket и восстановление без повтора', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'screen-test-'))
|
|
await writeFile(join(root, 'index.html'), '<html>local</html>')
|
|
let count = 0
|
|
const controller = new Controller((state) => {
|
|
count++
|
|
server.broadcast(state)
|
|
})
|
|
const server = new LanServer(
|
|
root,
|
|
() => controller.state,
|
|
(input) => controller.command(input),
|
|
)
|
|
const sockets: WebSocket[] = []
|
|
try {
|
|
await server.start('127.0.0.1')
|
|
const origin = server.url
|
|
assert.equal((await fetch(origin)).status, 200)
|
|
assert.equal((await fetch(origin + '/package.json')).status, 404)
|
|
assert.equal(
|
|
(
|
|
await fetch(origin + '/pair', {
|
|
method: 'POST',
|
|
headers: { Origin: origin },
|
|
body: JSON.stringify({ code: 'wrong' }),
|
|
})
|
|
).status,
|
|
403,
|
|
)
|
|
assert.equal(
|
|
(
|
|
await fetch(origin + '/pair', {
|
|
method: 'POST',
|
|
headers: { Origin: 'http://evil.example' },
|
|
body: '{}',
|
|
})
|
|
).status,
|
|
403,
|
|
)
|
|
const denied = new WebSocket(origin.replace('http', 'ws') + '/ws', { origin })
|
|
sockets.push(denied)
|
|
await new Promise<void>((resolve) => denied.on('error', () => resolve()))
|
|
const paired = await fetch(origin + '/pair', {
|
|
method: 'POST',
|
|
headers: { Origin: origin },
|
|
body: JSON.stringify({ code: server.code }),
|
|
})
|
|
const { id } = (await paired.json()) as { id: string }
|
|
assert.equal((await fetch(origin + '/pair/' + id)).status, 202)
|
|
server.approve(id)
|
|
const approved = await fetch(origin + '/pair/' + id)
|
|
assert.equal(approved.status, 200)
|
|
const cookie = approved.headers.get('set-cookie')!.split(';')[0]
|
|
assert.equal((await fetch(origin + '/pair/' + id)).status, 404)
|
|
async function connect() {
|
|
const ws = new WebSocket(origin.replace('http', 'ws') + '/ws', {
|
|
origin,
|
|
headers: { Cookie: cookie },
|
|
})
|
|
sockets.push(ws)
|
|
const [data] = await once(ws, 'message')
|
|
return { ws, state: JSON.parse(data.toString()) }
|
|
}
|
|
const first = await connect()
|
|
assert.equal(first.state.revision, 0)
|
|
let received = once(first.ws, 'message')
|
|
const command = { id: 'phone-1', action: 'play', media: 'audio', position: 0 }
|
|
first.ws.send(JSON.stringify(command))
|
|
await received
|
|
first.ws.send(JSON.stringify(command))
|
|
// A fresh command is a barrier proving that the duplicate was processed first.
|
|
received = once(first.ws, 'message')
|
|
first.ws.send(JSON.stringify({ ...command, id: 'phone-2', action: 'pause' }))
|
|
await received
|
|
assert.equal(count, 2)
|
|
first.ws.close()
|
|
await once(first.ws, 'close')
|
|
const restored = await connect()
|
|
assert.equal(restored.state.revision, 2)
|
|
assert.equal(count, 2)
|
|
const closed = once(restored.ws, 'close')
|
|
restored.ws.send(JSON.stringify({ action: 'delete' }))
|
|
await closed
|
|
assert.equal(count, 2)
|
|
const oversized = await connect()
|
|
const oversizedClosed = once(oversized.ws, 'close')
|
|
oversized.ws.send('x'.repeat(3000))
|
|
await oversizedClosed
|
|
assert.equal(count, 2)
|
|
assert.equal((await fetch(origin)).status, 200)
|
|
} finally {
|
|
for (const ws of sockets) ws.terminate()
|
|
await server.close()
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|
|
})
|