mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-16 03:23:29 +01:00
test(voice): fix tests
This commit is contained in:
@@ -1,72 +0,0 @@
|
||||
import { SILENCE_FRAME } from '../../audio/AudioPlayer';
|
||||
import { AudioReceiveStream, EndBehaviorType } from '../AudioReceiveStream';
|
||||
|
||||
const DUMMY_BUFFER = Buffer.allocUnsafe(16);
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function stepSilence(stream: AudioReceiveStream, increment: number) {
|
||||
stream.push(SILENCE_FRAME);
|
||||
await wait(increment);
|
||||
expect(stream.readable).toBe(true);
|
||||
}
|
||||
|
||||
describe('AudioReceiveStream', () => {
|
||||
test('Manual end behavior', async () => {
|
||||
const stream = new AudioReceiveStream({ end: { behavior: EndBehaviorType.Manual } });
|
||||
stream.push(DUMMY_BUFFER);
|
||||
expect(stream.readable).toBe(true);
|
||||
await wait(200);
|
||||
stream.push(DUMMY_BUFFER);
|
||||
expect(stream.readable).toBe(true);
|
||||
});
|
||||
|
||||
// TODO: Fix this test
|
||||
// test('AfterSilence end behavior', async () => {
|
||||
// const duration = 100;
|
||||
// const increment = 20;
|
||||
|
||||
// const stream = new AudioReceiveStream({ end: { behavior: EndBehaviorType.AfterSilence, duration: 100 } });
|
||||
// stream.resume();
|
||||
|
||||
// for (let i = increment; i < duration / 2; i += increment) {
|
||||
// await stepSilence(stream, increment);
|
||||
// }
|
||||
|
||||
// stream.push(DUMMY_BUFFER);
|
||||
|
||||
// for (let i = increment; i < duration; i += increment) {
|
||||
// await stepSilence(stream, increment);
|
||||
// }
|
||||
|
||||
// await wait(increment);
|
||||
// expect(stream.readableEnded).toBe(true);
|
||||
// });
|
||||
|
||||
test('AfterInactivity end behavior', async () => {
|
||||
const duration = 100;
|
||||
const increment = 20;
|
||||
|
||||
const stream = new AudioReceiveStream({ end: { behavior: EndBehaviorType.AfterInactivity, duration: 100 } });
|
||||
stream.resume();
|
||||
|
||||
for (let i = increment; i < duration / 2; i += increment) {
|
||||
await stepSilence(stream, increment);
|
||||
}
|
||||
|
||||
stream.push(DUMMY_BUFFER);
|
||||
|
||||
for (let i = increment; i < duration; i += increment) {
|
||||
await stepSilence(stream, increment);
|
||||
}
|
||||
|
||||
await wait(increment);
|
||||
expect(stream.readableEnded).toBe(false);
|
||||
|
||||
await wait(duration - increment);
|
||||
|
||||
expect(stream.readableEnded).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import EventEmitter, { once } from 'node:events';
|
||||
import { SSRCMap, VoiceUserData } from '../SSRCMap';
|
||||
|
||||
function onceOrThrow<T extends EventEmitter>(target: T, event: string, after: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
target.on(event, resolve);
|
||||
setTimeout(() => reject(new Error('Time up')), after);
|
||||
});
|
||||
}
|
||||
|
||||
describe('SSRCMap', () => {
|
||||
test('update persists data and emits correctly', async () => {
|
||||
const fixture1: VoiceUserData = {
|
||||
audioSSRC: 1,
|
||||
userId: '123',
|
||||
};
|
||||
|
||||
const fixture2: VoiceUserData = {
|
||||
...fixture1,
|
||||
videoSSRC: 2,
|
||||
};
|
||||
|
||||
const map = new SSRCMap();
|
||||
process.nextTick(() => map.update(fixture1));
|
||||
let [oldData, newData] = await once(map, 'update');
|
||||
expect(oldData).toBeUndefined();
|
||||
expect(newData).toMatchObject(fixture1);
|
||||
expect(map.get(fixture1.audioSSRC)).toMatchObject(fixture1);
|
||||
|
||||
process.nextTick(() => map.update(fixture2));
|
||||
[oldData, newData] = await once(map, 'update');
|
||||
expect(oldData).toMatchObject(fixture1);
|
||||
expect(newData).toMatchObject(fixture2);
|
||||
expect(map.get(fixture1.userId)).toMatchObject(fixture2);
|
||||
});
|
||||
|
||||
test('delete removes data and emits correctly', async () => {
|
||||
const fixture1: VoiceUserData = {
|
||||
audioSSRC: 1,
|
||||
userId: '123',
|
||||
};
|
||||
const map = new SSRCMap();
|
||||
|
||||
map.delete(fixture1.audioSSRC);
|
||||
await expect(onceOrThrow(map, 'delete', 5)).rejects.toThrow();
|
||||
|
||||
map.update(fixture1);
|
||||
process.nextTick(() => map.delete(fixture1.audioSSRC));
|
||||
await expect(once(map, 'delete')).resolves.toMatchObject([fixture1]);
|
||||
|
||||
map.delete(fixture1.audioSSRC);
|
||||
await expect(onceOrThrow(map, 'delete', 5)).rejects.toThrow();
|
||||
|
||||
map.update(fixture1);
|
||||
process.nextTick(() => map.delete(fixture1.userId));
|
||||
await expect(once(map, 'delete')).resolves.toMatchObject([fixture1]);
|
||||
expect(map.get(fixture1.audioSSRC)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { noop } from '../../util/util';
|
||||
import { SpeakingMap } from '../SpeakingMap';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('SpeakingMap', () => {
|
||||
test('Emits start and end', () => {
|
||||
const speaking = new SpeakingMap();
|
||||
const userId = '123';
|
||||
|
||||
const starts: string[] = [];
|
||||
const ends: string[] = [];
|
||||
|
||||
speaking.on('start', (userId) => void starts.push(userId));
|
||||
speaking.on('end', (userId) => void ends.push(userId));
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
speaking.onPacket(userId);
|
||||
setTimeout(noop, SpeakingMap.DELAY / 2);
|
||||
jest.advanceTimersToNextTimer();
|
||||
|
||||
expect(starts).toEqual([userId]);
|
||||
expect(ends).toEqual([]);
|
||||
}
|
||||
jest.advanceTimersToNextTimer();
|
||||
expect(ends).toEqual([userId]);
|
||||
|
||||
speaking.onPacket(userId);
|
||||
jest.advanceTimersToNextTimer();
|
||||
expect(starts).toEqual([userId, userId]);
|
||||
});
|
||||
});
|
||||
@@ -1,210 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/dot-notation */
|
||||
import { VoiceReceiver } from '../VoiceReceiver';
|
||||
import { VoiceConnection as _VoiceConnection, VoiceConnectionStatus } from '../../VoiceConnection';
|
||||
import { RTP_PACKET_DESKTOP, RTP_PACKET_CHROME, RTP_PACKET_ANDROID } from './fixtures/rtp';
|
||||
import { once } from 'node:events';
|
||||
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
|
||||
import { methods } from '../../util/Secretbox';
|
||||
|
||||
jest.mock('../../VoiceConnection');
|
||||
jest.mock('../SSRCMap');
|
||||
|
||||
const openSpy = jest.spyOn(methods, 'open');
|
||||
|
||||
openSpy.mockImplementation((buffer) => buffer);
|
||||
|
||||
const VoiceConnection = _VoiceConnection as unknown as jest.Mocked<typeof _VoiceConnection>;
|
||||
|
||||
function nextTick() {
|
||||
return new Promise((resolve) => process.nextTick(resolve));
|
||||
}
|
||||
|
||||
function* rangeIter(start: number, end: number) {
|
||||
for (let i = start; i <= end; i++) {
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
|
||||
function range(start: number, end: number) {
|
||||
return Buffer.from([...rangeIter(start, end)]);
|
||||
}
|
||||
|
||||
describe('VoiceReceiver', () => {
|
||||
let voiceConnection: _VoiceConnection;
|
||||
let receiver: VoiceReceiver;
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
voiceConnection = new VoiceConnection({} as any, {} as any);
|
||||
voiceConnection.state = {
|
||||
status: VoiceConnectionStatus.Signalling,
|
||||
} as any;
|
||||
receiver = new VoiceReceiver(voiceConnection);
|
||||
receiver['connectionData'] = {
|
||||
encryptionMode: 'dummy',
|
||||
nonceBuffer: Buffer.alloc(0),
|
||||
secretKey: Buffer.alloc(0),
|
||||
};
|
||||
});
|
||||
|
||||
test.each([
|
||||
['RTP Packet Desktop', RTP_PACKET_DESKTOP],
|
||||
['RTP Packet Chrome', RTP_PACKET_CHROME],
|
||||
['RTP Packet Android', RTP_PACKET_ANDROID],
|
||||
])('onUdpMessage: %s', async (testName, RTP_PACKET) => {
|
||||
receiver['decrypt'] = jest.fn().mockImplementationOnce(() => RTP_PACKET.decrypted);
|
||||
|
||||
const spy = jest.spyOn(receiver.ssrcMap, 'get');
|
||||
spy.mockImplementation(() => ({
|
||||
audioSSRC: RTP_PACKET.ssrc,
|
||||
userId: '123',
|
||||
}));
|
||||
|
||||
const stream = receiver.subscribe('123');
|
||||
|
||||
receiver['onUdpMessage'](RTP_PACKET.packet);
|
||||
await nextTick();
|
||||
expect(stream.read()).toEqual(RTP_PACKET.opusFrame);
|
||||
});
|
||||
|
||||
test('onUdpMessage: <8 bytes packet', () => {
|
||||
expect(() => receiver['onUdpMessage'](Buffer.alloc(4))).not.toThrow();
|
||||
});
|
||||
|
||||
test('onUdpMessage: destroys stream on decrypt failure', async () => {
|
||||
receiver['decrypt'] = jest.fn().mockImplementationOnce(() => null);
|
||||
|
||||
const spy = jest.spyOn(receiver.ssrcMap, 'get');
|
||||
spy.mockImplementation(() => ({
|
||||
audioSSRC: RTP_PACKET_DESKTOP.ssrc,
|
||||
userId: '123',
|
||||
}));
|
||||
|
||||
const stream = receiver.subscribe('123');
|
||||
|
||||
const errorEvent = once(stream, 'error');
|
||||
|
||||
receiver['onUdpMessage'](RTP_PACKET_DESKTOP.packet);
|
||||
await nextTick();
|
||||
await expect(errorEvent).resolves.toMatchObject([expect.any(Error)]);
|
||||
expect(receiver.subscriptions.size).toBe(0);
|
||||
});
|
||||
|
||||
test('subscribe: only allows one subscribe stream per SSRC', () => {
|
||||
const spy = jest.spyOn(receiver.ssrcMap, 'get');
|
||||
spy.mockImplementation(() => ({
|
||||
audioSSRC: RTP_PACKET_DESKTOP.ssrc,
|
||||
userId: '123',
|
||||
}));
|
||||
|
||||
const stream = receiver.subscribe('123');
|
||||
expect(receiver.subscribe('123')).toBe(stream);
|
||||
});
|
||||
|
||||
describe('onWsPacket', () => {
|
||||
test('CLIENT_DISCONNECT packet', () => {
|
||||
const spy = jest.spyOn(receiver.ssrcMap, 'delete');
|
||||
receiver['onWsPacket']({
|
||||
op: VoiceOpcodes.ClientDisconnect,
|
||||
d: {
|
||||
user_id: '123abc',
|
||||
},
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith('123abc');
|
||||
});
|
||||
|
||||
test('SPEAKING packet', () => {
|
||||
const spy = jest.spyOn(receiver.ssrcMap, 'update');
|
||||
receiver['onWsPacket']({
|
||||
op: VoiceOpcodes.Speaking,
|
||||
d: {
|
||||
ssrc: 123,
|
||||
user_id: '123abc',
|
||||
speaking: 1,
|
||||
},
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
audioSSRC: 123,
|
||||
userId: '123abc',
|
||||
});
|
||||
});
|
||||
|
||||
test('CLIENT_CONNECT packet', () => {
|
||||
const spy = jest.spyOn(receiver.ssrcMap, 'update');
|
||||
receiver['onWsPacket']({
|
||||
op: VoiceOpcodes.ClientConnect,
|
||||
d: {
|
||||
audio_ssrc: 123,
|
||||
video_ssrc: 43,
|
||||
user_id: '123abc',
|
||||
},
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
audioSSRC: 123,
|
||||
videoSSRC: 43,
|
||||
userId: '123abc',
|
||||
});
|
||||
receiver['onWsPacket']({
|
||||
op: VoiceOpcodes.ClientConnect,
|
||||
d: {
|
||||
audio_ssrc: 123,
|
||||
video_ssrc: 0,
|
||||
user_id: '123abc',
|
||||
},
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
audioSSRC: 123,
|
||||
videoSSRC: undefined,
|
||||
userId: '123abc',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('decrypt', () => {
|
||||
const secretKey = new Uint8Array([1, 2, 3, 4]);
|
||||
|
||||
beforeEach(() => {
|
||||
openSpy.mockClear();
|
||||
});
|
||||
|
||||
test('decrypt: xsalsa20_poly1305_lite', () => {
|
||||
// Arrange
|
||||
const buffer = range(1, 32);
|
||||
const nonce = Buffer.alloc(4);
|
||||
|
||||
// Act
|
||||
const decrypted = receiver['decrypt'](buffer, 'xsalsa20_poly1305_lite', nonce, secretKey);
|
||||
|
||||
// Assert
|
||||
expect(nonce.equals(range(29, 32))).toBe(true);
|
||||
expect(decrypted.equals(range(13, 28))).toBe(true);
|
||||
});
|
||||
|
||||
test('decrypt: xsalsa20_poly1305_suffix', () => {
|
||||
// Arrange
|
||||
const buffer = range(1, 64);
|
||||
const nonce = Buffer.alloc(24);
|
||||
|
||||
// Act
|
||||
const decrypted = receiver['decrypt'](buffer, 'xsalsa20_poly1305_suffix', nonce, secretKey);
|
||||
|
||||
// Assert
|
||||
expect(nonce.equals(range(41, 64))).toBe(true);
|
||||
expect(decrypted.equals(range(13, 40))).toBe(true);
|
||||
});
|
||||
|
||||
test('decrypt: xsalsa20_poly1305', () => {
|
||||
// Arrange
|
||||
const buffer = range(1, 64);
|
||||
const nonce = Buffer.alloc(12);
|
||||
|
||||
// Act
|
||||
const decrypted = receiver['decrypt'](buffer, 'xsalsa20_poly1305', nonce, secretKey);
|
||||
|
||||
// Assert
|
||||
expect(nonce.equals(range(1, 12))).toBe(true);
|
||||
expect(decrypted.equals(range(13, 64))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
export const RTP_PACKET_DESKTOP = {
|
||||
ssrc: 341124,
|
||||
packet: Buffer.from([
|
||||
0x90, 0x78, 0x27, 0xe9, 0xf7, 0xcb, 0xbc, 0xd1, 0x0, 0x5, 0x34, 0x84, 0x8a, 0xbb, 0xe2, 0x97, 0x21, 0x9f, 0x1f,
|
||||
0x67, 0xcd, 0x17, 0x91, 0x56, 0x43, 0xa0, 0x98, 0xfd, 0xa9, 0x25, 0x81, 0x63, 0x13, 0xb4, 0x1e, 0xae, 0x88, 0xe4,
|
||||
0x0, 0xed, 0x0, 0x0, 0x0,
|
||||
]),
|
||||
decrypted: Buffer.from([0xbe, 0xde, 0x0, 0x1, 0x10, 0xff, 0x90, 0x0, 0xf8, 0xff, 0xfe]),
|
||||
opusFrame: Buffer.from([0xf8, 0xff, 0xfe]),
|
||||
};
|
||||
|
||||
export const RTP_PACKET_CHROME = {
|
||||
ssrc: 172360,
|
||||
packet: Buffer.from([
|
||||
0x80, 0x78, 0x46, 0xdf, 0x27, 0x59, 0x2a, 0xd7, 0x0, 0x2, 0xa1, 0x48, 0x42, 0x9e, 0x53, 0xec, 0x73, 0xc1, 0x71,
|
||||
0x22, 0x71, 0x60, 0x90, 0xff, 0x1b, 0x20, 0x47, 0x2c, 0xdc, 0x86, 0xc4, 0x9a, 0x0, 0x0, 0x0,
|
||||
]),
|
||||
decrypted: Buffer.from([0xf8, 0xff, 0xfe]),
|
||||
opusFrame: Buffer.from([0xf8, 0xff, 0xfe]),
|
||||
};
|
||||
|
||||
export const RTP_PACKET_ANDROID = {
|
||||
ssrc: 172596,
|
||||
packet: Buffer.from([
|
||||
0x90, 0x78, 0x39, 0xd0, 0xe0, 0x59, 0xf5, 0x47, 0x0, 0x2, 0xa2, 0x34, 0x12, 0x6d, 0x87, 0x56, 0x25, 0xc8, 0x3e,
|
||||
0x96, 0xc0, 0x71, 0x9a, 0x1, 0x83, 0xe, 0x1, 0x62, 0x91, 0x95, 0x1f, 0x76, 0x57, 0x15, 0x41, 0xab, 0xee, 0x5b, 0xac,
|
||||
0x8b, 0x0, 0x0, 0x0,
|
||||
]),
|
||||
decrypted: Buffer.from([0xbe, 0xde, 0x0, 0x1, 0x10, 0xff, 0x90, 0x0, 0xf8, 0xff, 0xfe]),
|
||||
opusFrame: Buffer.from([0xf8, 0xff, 0xfe]),
|
||||
};
|
||||
Reference in New Issue
Block a user