refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,398 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Config} from '@fluxer/integration/Config';
import type {
GatewayDispatch,
GatewayHelloPayload,
GatewayPayload,
GatewayResumeState,
ReadyPayload,
VoiceServerUpdate,
VoiceStateUpdate,
} from '@fluxer/integration/gateway/GatewayTypes';
import {GatewayOpcode} from '@fluxer/integration/gateway/GatewayTypes';
import WebSocket from 'ws';
export type EventMatcher = (data: unknown) => boolean;
export class GatewayClient {
private ws: WebSocket | null = null;
private token: string;
private gatewayUrl: string;
private sessionId: string | null = null;
private sequence: number = 0;
private heartbeatInterval: NodeJS.Timeout | null = null;
private events: Array<GatewayDispatch> = [];
private eventListeners: Map<string, Array<(dispatch: GatewayDispatch) => void>> = new Map();
private readyPromise: Promise<ReadyPayload> | null = null;
private readyResolve: ((payload: ReadyPayload) => void) | null = null;
private resumedPromise: Promise<void> | null = null;
private resumedResolve: (() => void) | null = null;
private closed: boolean = false;
constructor(token: string) {
this.token = token;
this.gatewayUrl = Config.gatewayUrl;
}
async connect(): Promise<ReadyPayload> {
return new Promise((resolve, reject) => {
this.setupWebSocket(reject, undefined);
this.readyPromise!.then(resolve).catch(reject);
});
}
async resume(resumeState: GatewayResumeState): Promise<void> {
return new Promise((resolve, reject) => {
this.sessionId = resumeState.sessionId;
this.sequence = resumeState.sequence;
this.resumedPromise = new Promise((res) => {
this.resumedResolve = res;
});
this.setupWebSocket(reject, resumeState);
this.resumedPromise.then(resolve).catch(reject);
});
}
private setupWebSocket(reject: (error: Error) => void, resumeState?: GatewayResumeState): void {
const url = `${this.gatewayUrl}?v=1&encoding=json`;
const headers: Record<string, string> = {
'User-Agent': 'FluxerIntegrationTests/1.0',
};
if (Config.webAppOrigin) {
headers['Origin'] = Config.webAppOrigin;
}
if (Config.testToken) {
headers['X-Test-Token'] = Config.testToken;
}
this.ws = new WebSocket(url, {headers});
this.readyPromise = new Promise((res) => {
this.readyResolve = res;
});
this.ws.on('open', () => {});
this.ws.on('message', (data: WebSocket.Data) => {
this.handleMessage(data.toString(), resumeState);
});
this.ws.on('error', (error) => {
reject(error);
});
this.ws.on('close', (code, reason) => {
this.closed = true;
this.stopHeartbeat();
if (!this.readyResolve && !this.resumedResolve) {
reject(new Error(`WebSocket closed: ${code} ${reason.toString()}`));
}
});
}
private handleMessage(data: string, resume?: GatewayResumeState): void {
const payload: GatewayPayload = JSON.parse(data);
if (payload.s != null) {
this.sequence = payload.s;
}
switch (payload.op) {
case GatewayOpcode.HELLO:
this.handleHello(payload.d as GatewayHelloPayload, resume);
break;
case GatewayOpcode.DISPATCH:
this.handleDispatch(payload);
break;
case GatewayOpcode.HEARTBEAT_ACK:
break;
case GatewayOpcode.HEARTBEAT:
this.sendHeartbeat();
break;
case GatewayOpcode.INVALID_SESSION:
console.error('Invalid session received');
break;
case GatewayOpcode.RECONNECT:
console.log('Reconnect requested');
break;
}
}
private handleHello(hello: GatewayHelloPayload, resume?: GatewayResumeState): void {
this.startHeartbeat(hello.heartbeat_interval);
if (resume) {
this.sendResume(resume);
} else {
this.sendIdentify();
}
}
private handleDispatch(payload: GatewayPayload): void {
const dispatch: GatewayDispatch = {
type: payload.t!,
data: payload.d,
sequence: payload.s ?? 0,
};
this.events.push(dispatch);
const listeners = this.eventListeners.get(dispatch.type);
if (listeners) {
for (const listener of listeners) {
listener(dispatch);
}
}
if (dispatch.type === 'READY') {
const ready = dispatch.data as ReadyPayload;
this.sessionId = ready.session_id;
if (this.readyResolve) {
this.readyResolve(ready);
this.readyResolve = null;
}
} else if (dispatch.type === 'RESUMED') {
if (this.resumedResolve) {
this.resumedResolve();
this.resumedResolve = null;
}
}
}
private sendIdentify(): void {
this.send({
op: GatewayOpcode.IDENTIFY,
d: {
token: this.token,
properties: {
os: 'linux',
browser: 'FluxerIntegrationTests',
device: 'FluxerIntegrationTests',
},
compress: false,
large_threshold: 250,
},
});
}
private sendResume(resume: GatewayResumeState): void {
this.send({
op: GatewayOpcode.RESUME,
d: {
token: this.token,
session_id: resume.sessionId,
seq: resume.sequence,
},
});
}
private sendHeartbeat(): void {
this.send({
op: GatewayOpcode.HEARTBEAT,
d: this.sequence,
});
}
private startHeartbeat(interval: number): void {
this.heartbeatInterval = setInterval(() => {
if (!this.closed) {
this.sendHeartbeat();
}
}, interval);
}
private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
send(payload: Omit<GatewayPayload, 's' | 't'>): void {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(payload));
}
}
sendVoiceStateUpdate(
guildId: string | null,
channelId: string | null,
connectionId?: string | null,
selfMute: boolean = false,
selfDeaf: boolean = false,
selfVideo: boolean = false,
selfStream: boolean = false,
): void {
this.send({
op: GatewayOpcode.VOICE_STATE_UPDATE,
d: {
guild_id: guildId,
channel_id: channelId,
connection_id: connectionId,
self_mute: selfMute,
self_deaf: selfDeaf,
self_video: selfVideo,
self_stream: selfStream,
},
});
}
activateGuild(guildId: string): void {
this.send({
op: GatewayOpcode.LAZY_REQUEST,
d: {
subscriptions: {
[guildId]: {
active: true,
},
},
},
});
}
waitForEvent(eventType: string, timeoutMs: number = 5000, matcher?: EventMatcher): Promise<GatewayDispatch> {
return new Promise((resolve, reject) => {
const existingIndex = this.events.findIndex((e) => e.type === eventType && (!matcher || matcher(e.data)));
if (existingIndex !== -1) {
const event = this.events.splice(existingIndex, 1)[0]!;
resolve(event);
return;
}
const timeout = setTimeout(() => {
cleanup();
reject(new Error(`Timeout waiting for event: ${eventType}`));
}, timeoutMs);
const listener = (dispatch: GatewayDispatch) => {
if (!matcher || matcher(dispatch.data)) {
cleanup();
const index = this.events.indexOf(dispatch);
if (index !== -1) {
this.events.splice(index, 1);
}
resolve(dispatch);
}
};
const cleanup = () => {
clearTimeout(timeout);
const listeners = this.eventListeners.get(eventType);
if (listeners) {
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
}
};
if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, []);
}
this.eventListeners.get(eventType)!.push(listener);
});
}
waitForVoiceServerUpdate(
timeoutMs: number = 5000,
matcher?: (vsu: VoiceServerUpdate) => boolean,
): Promise<VoiceServerUpdate> {
return this.waitForEvent('VOICE_SERVER_UPDATE', timeoutMs, matcher as EventMatcher).then(
(dispatch) => dispatch.data as VoiceServerUpdate,
);
}
waitForVoiceStateUpdate(
timeoutMs: number = 5000,
matcher?: (vs: VoiceStateUpdate) => boolean,
): Promise<VoiceStateUpdate> {
return this.waitForEvent('VOICE_STATE_UPDATE', timeoutMs, matcher as EventMatcher).then(
(dispatch) => dispatch.data as VoiceStateUpdate,
);
}
assertNoEvent(eventType: string, waitMs: number = 500): Promise<void> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
cleanup();
resolve();
}, waitMs);
const listener = (dispatch: GatewayDispatch) => {
cleanup();
reject(new Error(`Unexpected event received: ${eventType} - ${JSON.stringify(dispatch.data)}`));
};
const cleanup = () => {
clearTimeout(timeout);
const listeners = this.eventListeners.get(eventType);
if (listeners) {
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
}
};
if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, []);
}
this.eventListeners.get(eventType)!.push(listener);
});
}
getSessionId(): string | null {
return this.sessionId;
}
getSequence(): number {
return this.sequence;
}
getResumeState(): GatewayResumeState | null {
if (!this.sessionId) return null;
return {
sessionId: this.sessionId,
sequence: this.sequence,
};
}
close(): void {
this.closed = true;
this.stopHeartbeat();
if (this.ws) {
this.ws.close(1000, 'Test complete');
this.ws = null;
}
}
}
export async function createGatewayClient(token: string): Promise<GatewayClient> {
const client = new GatewayClient(token);
await client.connect();
return client;
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createGatewayClient, GatewayClient} from '@fluxer/integration/gateway/GatewayClient';
import type {TestAccount} from '@fluxer/integration/helpers/AccountHelper';
import {createTestAccount, ensureSessionStarted} from '@fluxer/integration/helpers/AccountHelper';
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
describe('Gateway Connection', () => {
let account: TestAccount;
let gateway: GatewayClient | null = null;
beforeEach(async () => {
account = await createTestAccount();
await ensureSessionStarted(account.token);
});
afterEach(() => {
if (gateway) {
gateway.close();
gateway = null;
}
});
test('should connect to gateway and receive READY', async () => {
gateway = await createGatewayClient(account.token);
expect(gateway.getSessionId()).toBeTruthy();
expect(gateway.getSequence()).toBeGreaterThan(0);
});
test('should receive session_id in READY payload', async () => {
gateway = await createGatewayClient(account.token);
const sessionId = gateway.getSessionId();
expect(sessionId).toBeTruthy();
expect(typeof sessionId).toBe('string');
expect(sessionId!.length).toBeGreaterThan(0);
});
test('should be able to resume session', async () => {
gateway = await createGatewayClient(account.token);
const resumeState = gateway.getResumeState();
expect(resumeState).toBeTruthy();
gateway.close();
gateway = new GatewayClient(account.token);
await gateway.resume(resumeState!);
expect(gateway.getSessionId()).toBe(resumeState!.sessionId);
});
test('should handle multiple concurrent connections for different users', async () => {
const account2 = await createTestAccount();
await ensureSessionStarted(account2.token);
gateway = await createGatewayClient(account.token);
const gateway2 = await createGatewayClient(account2.token);
try {
expect(gateway.getSessionId()).toBeTruthy();
expect(gateway2.getSessionId()).toBeTruthy();
expect(gateway.getSessionId()).not.toBe(gateway2.getSessionId());
} finally {
gateway2.close();
}
});
});

View File

@@ -0,0 +1,192 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createGatewayClient, type GatewayClient} from '@fluxer/integration/gateway/GatewayClient';
import type {TestAccount} from '@fluxer/integration/helpers/AccountHelper';
import {createTestAccount, ensureSessionStarted} from '@fluxer/integration/helpers/AccountHelper';
import {apiClient} from '@fluxer/integration/helpers/ApiClient';
import type {GuildResponse} from '@fluxer/integration/helpers/GuildHelper';
import {createGuild, createTextChannel} from '@fluxer/integration/helpers/GuildHelper';
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
interface ChannelCreate {
id: string;
name: string;
type: number;
guild_id: string;
}
interface ChannelUpdate {
id: string;
name?: string;
topic?: string;
guild_id: string;
}
interface ChannelDelete {
id: string;
guild_id: string;
}
interface MessageCreate {
id: string;
channel_id: string;
content: string;
author: {
id: string;
};
}
describe('Gateway Guild Events', () => {
let account: TestAccount;
let guild: GuildResponse;
let gateway: GatewayClient | null = null;
beforeEach(async () => {
account = await createTestAccount();
await ensureSessionStarted(account.token);
guild = await createGuild(account.token, 'Events Test Guild');
});
afterEach(() => {
if (gateway) {
gateway.close();
gateway = null;
}
});
test('should receive CHANNEL_CREATE when creating a channel', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
const channelPromise = gateway.waitForEvent('CHANNEL_CREATE', 10000, (data) => {
const channel = data as ChannelCreate;
return channel.guild_id === guild.id && channel.name === 'new-channel';
});
await createTextChannel(account.token, guild.id, 'new-channel');
const event = await channelPromise;
const channel = event.data as ChannelCreate;
expect(channel.name).toBe('new-channel');
expect(channel.guild_id).toBe(guild.id);
expect(channel.type).toBe(0);
});
test('should receive CHANNEL_UPDATE when updating a channel', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
const channel = await createTextChannel(account.token, guild.id, 'update-test');
const updatePromise = gateway.waitForEvent('CHANNEL_UPDATE', 10000, (data) => {
const update = data as ChannelUpdate;
return update.id === channel.id;
});
await apiClient.patch(
`/channels/${channel.id}`,
{
name: 'updated-channel',
topic: 'New topic',
},
account.token,
);
const event = await updatePromise;
const updated = event.data as ChannelUpdate;
expect(updated.id).toBe(channel.id);
expect(updated.name).toBe('updated-channel');
});
test('should receive CHANNEL_DELETE when deleting a channel', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
const channel = await createTextChannel(account.token, guild.id, 'delete-test');
const deletePromise = gateway.waitForEvent('CHANNEL_DELETE', 10000, (data) => {
const deleted = data as ChannelDelete;
return deleted.id === channel.id;
});
await apiClient.delete(`/channels/${channel.id}`, account.token);
const event = await deletePromise;
const deleted = event.data as ChannelDelete;
expect(deleted.id).toBe(channel.id);
});
test('should receive MESSAGE_CREATE when sending a message', async () => {
gateway = await createGatewayClient(account.token);
const channel = await createTextChannel(account.token, guild.id, 'message-test');
const messagePromise = gateway.waitForEvent('MESSAGE_CREATE', 10000, (data) => {
const msg = data as MessageCreate;
return msg.channel_id === channel.id && msg.content === 'Hello from integration test';
});
await apiClient.post(
`/channels/${channel.id}/messages`,
{
content: 'Hello from integration test',
},
account.token,
);
const event = await messagePromise;
const message = event.data as MessageCreate;
expect(message.content).toBe('Hello from integration test');
expect(message.channel_id).toBe(channel.id);
expect(message.author.id).toBe(account.userId);
});
test('should receive GUILD_UPDATE when updating guild', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
const updatePromise = gateway.waitForEvent('GUILD_UPDATE', 10000, (data) => {
const update = data as {id: string; name: string};
return update.id === guild.id && update.name === 'Updated Guild Name';
});
await apiClient.patch(
`/guilds/${guild.id}`,
{
name: 'Updated Guild Name',
},
account.token,
);
const event = await updatePromise;
const updated = event.data as {id: string; name: string};
expect(updated.id).toBe(guild.id);
expect(updated.name).toBe('Updated Guild Name');
});
});

View File

@@ -0,0 +1,146 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
export const GatewayOpcode = {
DISPATCH: 0,
HEARTBEAT: 1,
IDENTIFY: 2,
PRESENCE_UPDATE: 3,
VOICE_STATE_UPDATE: 4,
RESUME: 6,
RECONNECT: 7,
REQUEST_GUILD_MEMBERS: 8,
INVALID_SESSION: 9,
HELLO: 10,
HEARTBEAT_ACK: 11,
GATEWAY_ERROR: 12,
LAZY_REQUEST: 14,
} as const;
export type GatewayOpcodeType = ValueOf<typeof GatewayOpcode>;
export interface GatewayPayload {
op: GatewayOpcodeType;
d: unknown;
s?: number | null;
t?: string | null;
}
export interface GatewayHelloPayload {
heartbeat_interval: number;
}
export interface GatewayIdentifyPayload {
token: string;
properties: {
os: string;
browser: string;
device: string;
};
compress?: boolean;
large_threshold?: number;
presence?: {
status: string;
since?: number | null;
activities?: Array<unknown>;
afk?: boolean;
};
}
export interface GatewayResumePayload {
token: string;
session_id: string;
seq: number;
}
export interface GatewayVoiceStateUpdatePayload {
guild_id: string | null;
channel_id: string | null;
connection_id?: string | null;
self_mute: boolean;
self_deaf: boolean;
self_video?: boolean;
self_stream?: boolean;
}
export interface GatewayDispatch {
type: string;
data: unknown;
sequence: number;
}
export interface VoiceServerUpdate {
token: string;
guild_id?: string | null;
channel_id?: string | null;
endpoint: string;
connection_id: string;
}
export interface VoiceStateUpdate {
guild_id?: string | null;
channel_id?: string | null;
user_id: string;
member?: unknown;
session_id: string;
deaf: boolean;
mute: boolean;
self_deaf: boolean;
self_mute: boolean;
self_stream?: boolean;
self_video?: boolean;
suppress: boolean;
connection_id?: string | null;
}
export interface ReadyPayload {
v: number;
user: {
id: string;
username: string;
discriminator: string;
};
guilds: Array<{
id: string;
unavailable?: boolean;
}>;
session_id: string;
resume_gateway_url?: string;
private_channels: Array<unknown>;
relationships: Array<unknown>;
}
export interface MessageCreatePayload {
id: string;
channel_id: string;
guild_id?: string;
author: {
id: string;
username: string;
};
content: string;
timestamp: string;
}
export interface GatewayResumeState {
sessionId: string;
sequence: number;
}

View File

@@ -0,0 +1,216 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createGatewayClient, type GatewayClient} from '@fluxer/integration/gateway/GatewayClient';
import type {VoiceStateUpdate} from '@fluxer/integration/gateway/GatewayTypes';
import type {TestAccount} from '@fluxer/integration/helpers/AccountHelper';
import {createTestAccount, ensureSessionStarted} from '@fluxer/integration/helpers/AccountHelper';
import type {ChannelResponse, GuildResponse} from '@fluxer/integration/helpers/GuildHelper';
import {createGuild, createVoiceChannel} from '@fluxer/integration/helpers/GuildHelper';
import {confirmVoiceConnectionForTest} from '@fluxer/integration/helpers/VoiceHelper';
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
describe('Gateway Voice State', () => {
let account: TestAccount;
let guild: GuildResponse;
let voiceChannel: ChannelResponse;
let gateway: GatewayClient | null = null;
beforeEach(async () => {
account = await createTestAccount();
await ensureSessionStarted(account.token);
guild = await createGuild(account.token, 'Voice Test Guild');
voiceChannel = await createVoiceChannel(account.token, guild.id, 'test-voice');
});
afterEach(() => {
if (gateway) {
gateway.close();
gateway = null;
}
});
test('should join guild voice channel and receive VOICE_SERVER_UPDATE', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
gateway.sendVoiceStateUpdate(guild.id, voiceChannel.id, null, false, false, false, false);
const serverUpdate = await gateway.waitForVoiceServerUpdate(10000);
expect(serverUpdate.token).toBeTruthy();
expect(serverUpdate.endpoint).toBeTruthy();
expect(serverUpdate.guild_id).toBe(guild.id);
expect(serverUpdate.connection_id).toBeTruthy();
});
test('should receive VOICE_STATE_UPDATE when joining voice channel', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
gateway.sendVoiceStateUpdate(guild.id, voiceChannel.id, null, false, false, false, false);
const serverUpdate = await gateway.waitForVoiceServerUpdate(10000);
await confirmVoiceConnectionForTest({
guildId: guild.id,
channelId: voiceChannel.id,
connectionId: serverUpdate.connection_id,
});
const stateUpdate = await gateway.waitForVoiceStateUpdate(10000, (vs: VoiceStateUpdate) => {
return (
vs.user_id === account.userId &&
vs.channel_id === voiceChannel.id &&
vs.connection_id === serverUpdate.connection_id
);
});
expect(stateUpdate.user_id).toBe(account.userId);
expect(stateUpdate.channel_id).toBe(voiceChannel.id);
expect(stateUpdate.guild_id).toBe(guild.id);
expect(stateUpdate.self_mute).toBe(false);
expect(stateUpdate.self_deaf).toBe(false);
});
test('should disconnect from voice channel when sending null channel_id', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
gateway.sendVoiceStateUpdate(guild.id, voiceChannel.id, null, false, false, false, false);
const serverUpdate = await gateway.waitForVoiceServerUpdate(10000);
const connectionId = serverUpdate.connection_id;
await confirmVoiceConnectionForTest({
guildId: guild.id,
channelId: voiceChannel.id,
connectionId,
});
await gateway.waitForVoiceStateUpdate(10000, (vs: VoiceStateUpdate) => {
return vs.user_id === account.userId && vs.channel_id === voiceChannel.id && vs.connection_id === connectionId;
});
gateway.sendVoiceStateUpdate(guild.id, null, connectionId, false, false, false, false);
const disconnectUpdate = await gateway.waitForVoiceStateUpdate(10000, (vs: VoiceStateUpdate) => {
return vs.user_id === account.userId && vs.channel_id === null;
});
expect(disconnectUpdate.channel_id).toBeNull();
});
test('should update self_mute state', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
gateway.sendVoiceStateUpdate(guild.id, voiceChannel.id, null, true, false, false, false);
const serverUpdate = await gateway.waitForVoiceServerUpdate(10000);
await confirmVoiceConnectionForTest({
guildId: guild.id,
channelId: voiceChannel.id,
connectionId: serverUpdate.connection_id,
});
const stateUpdate = await gateway.waitForVoiceStateUpdate(10000, (vs: VoiceStateUpdate) => {
return (
vs.user_id === account.userId &&
vs.channel_id === voiceChannel.id &&
vs.connection_id === serverUpdate.connection_id
);
});
expect(stateUpdate.self_mute).toBe(true);
expect(stateUpdate.self_deaf).toBe(false);
});
test('should update self_deaf state', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
gateway.sendVoiceStateUpdate(guild.id, voiceChannel.id, null, false, true, false, false);
const serverUpdate = await gateway.waitForVoiceServerUpdate(10000);
await confirmVoiceConnectionForTest({
guildId: guild.id,
channelId: voiceChannel.id,
connectionId: serverUpdate.connection_id,
});
const stateUpdate = await gateway.waitForVoiceStateUpdate(10000, (vs: VoiceStateUpdate) => {
return (
vs.user_id === account.userId &&
vs.channel_id === voiceChannel.id &&
vs.connection_id === serverUpdate.connection_id
);
});
expect(stateUpdate.self_deaf).toBe(true);
});
test('should update self_video state', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
gateway.sendVoiceStateUpdate(guild.id, voiceChannel.id, null, false, false, true, false);
const serverUpdate = await gateway.waitForVoiceServerUpdate(10000);
await confirmVoiceConnectionForTest({
guildId: guild.id,
channelId: voiceChannel.id,
connectionId: serverUpdate.connection_id,
});
const stateUpdate = await gateway.waitForVoiceStateUpdate(10000, (vs: VoiceStateUpdate) => {
return (
vs.user_id === account.userId &&
vs.channel_id === voiceChannel.id &&
vs.connection_id === serverUpdate.connection_id
);
});
expect(stateUpdate.self_video).toBe(true);
});
test('should include connection_id in voice updates', async () => {
gateway = await createGatewayClient(account.token);
gateway.activateGuild(guild.id);
await new Promise((r) => setTimeout(r, 100));
gateway.sendVoiceStateUpdate(guild.id, voiceChannel.id, null, false, false, false, false);
const serverUpdate = await gateway.waitForVoiceServerUpdate(10000);
expect(serverUpdate.connection_id).toBeTruthy();
expect(typeof serverUpdate.connection_id).toBe('string');
await confirmVoiceConnectionForTest({
guildId: guild.id,
channelId: voiceChannel.id,
connectionId: serverUpdate.connection_id,
});
const stateUpdate = await gateway.waitForVoiceStateUpdate(10000, (vs: VoiceStateUpdate) => {
return vs.user_id === account.userId && vs.connection_id === serverUpdate.connection_id;
});
expect(stateUpdate.connection_id).toBe(serverUpdate.connection_id);
});
});