'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const net = require('node:net'); const { PlaytubeClient, PlaytubeError, pipePath } = require('../src/playtubeClient'); const uniqueName = () => `Test${process.pid}${Date.now()}${Math.floor(Math.random() * 1e6)}`; const until = async (condition, timeoutMs = 3000) => { const start = Date.now(); while (!condition()) { if (Date.now() - start > timeoutMs) throw new Error('Zeitueberschreitung im Test'); await new Promise((resolve) => setTimeout(resolve, 10)); } }; /** Fake-Playtube: beantwortet zeilenweise JSON-Befehle wie playtube/remote_control.py. */ function startServer(name, handler) { const sockets = new Set(); const server = net.createServer((socket) => { sockets.add(socket); socket.setEncoding('utf8'); let buffer = ''; socket.on('data', (chunk) => { buffer += chunk; let index; while ((index = buffer.indexOf('\n')) >= 0) { const line = buffer.slice(0, index); buffer = buffer.slice(index + 1); const request = JSON.parse(line); const reply = handler(request); if (reply) socket.write(`${JSON.stringify({ id: request.id, ...reply })}\n`); } }); socket.on('close', () => sockets.delete(socket)); socket.on('error', () => {}); }); return new Promise((resolve) => { server.listen(pipePath(name), () => resolve({ dropClients: () => sockets.forEach((socket) => socket.destroy()), close: () => new Promise((done) => { sockets.forEach((s) => s.destroy()); server.close(done); }), })); }); } test('pipePath bildet den Windows-Pipe-Namen', () => { assert.equal(pipePath('Playtube', 'win32'), '\\\\.\\pipe\\Playtube.Remote.Playtube'); }); test('verbindet sich, sendet Befehle und liefert Antworten', async () => { const name = uniqueName(); const fake = await startServer(name, (request) => { if (request.cmd === 'status') return { ok: true, state: { activeTab: 'music' } }; if (request.cmd === 'list_playlists') return { ok: true, playlists: [{ id: 'LM', title: 'Liked' }] }; if (request.cmd === 'boom') return { ok: false, error: 'kaputt' }; return { ok: true, echo: request }; }); const client = new PlaytubeClient({ pipeNames: [name] }); client.start(); try { await until(() => client.connected); assert.deepEqual(await client.status(), { activeTab: 'music' }); assert.deepEqual(await client.listPlaylists(), [{ id: 'LM', title: 'Liked' }]); const echoed = await client.request('volume_change', { target: 'auto', value: 5, skip: undefined }); assert.deepEqual(echoed.echo, { id: echoed.id, cmd: 'volume_change', target: 'auto', value: 5 }); await assert.rejects(client.request('boom'), (error) => error instanceof PlaytubeError && error.code === 'remote' && /kaputt/.test(error.message)); } finally { client.stop(); await fake.close(); } }); test('ohne Verbindung wird sofort abgelehnt', async () => { const client = new PlaytubeClient({ pipeNames: [uniqueName()] }); await assert.rejects(client.request('status'), (error) => error.code === 'not_connected'); }); test('Timeout, wenn Playtube nicht antwortet', async () => { const name = uniqueName(); const fake = await startServer(name, () => null); const client = new PlaytubeClient({ pipeNames: [name], requestTimeoutMs: 80 }); client.start(); try { await until(() => client.connected); await assert.rejects(client.request('status'), (error) => error.code === 'timeout'); } finally { client.stop(); await fake.close(); } }); test('Verbindungsabbruch: offene Anfragen scheitern, danach automatische Wiederverbindung', async () => { const name = uniqueName(); const fake = await startServer(name, () => null); const client = new PlaytubeClient({ pipeNames: [name], backoffMs: [20] }); const events = []; client.on('connected', () => events.push('connected')); client.on('disconnected', () => events.push('disconnected')); client.start(); try { await until(() => client.connected); const pending = client.request('status'); fake.dropClients(); await assert.rejects(pending, (error) => error.code === 'disconnected'); await until(() => events.filter((e) => e === 'connected').length === 2); assert.deepEqual(events, ['connected', 'disconnected', 'connected']); } finally { client.stop(); await fake.close(); } }); test('probiert mehrere Pipe-Namen der Reihe nach und verbindet Playtube spaeter', async () => { const missing = uniqueName(); const present = uniqueName(); const client = new PlaytubeClient({ pipeNames: [missing, present], backoffMs: [20] }); client.start(); await new Promise((resolve) => setTimeout(resolve, 60)); assert.equal(client.connected, false); const fake = await startServer(present, () => ({ ok: true })); try { await until(() => client.connected); assert.equal(client.pipeName, present); } finally { client.stop(); await fake.close(); } });