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,452 @@
/*
* 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 {GuildID, UserID} from '@fluxer/api/src/BrandedTypes';
import type {VoiceAccessContext} from '@fluxer/api/src/voice/VoiceAvailabilityService';
import {VoiceAvailabilityService} from '@fluxer/api/src/voice/VoiceAvailabilityService';
import type {VoiceRegionRecord, VoiceServerRecord} from '@fluxer/api/src/voice/VoiceModel';
import type {VoiceTopology} from '@fluxer/api/src/voice/VoiceTopology';
import {GuildFeatures} from '@fluxer/constants/src/GuildConstants';
import {describe, expect, it} from 'vitest';
function createMockRegion(overrides: Partial<VoiceRegionRecord> = {}): VoiceRegionRecord {
return {
id: 'us-default',
name: 'US Default',
emoji: 'flag',
latitude: 39.8283,
longitude: -98.5795,
isDefault: true,
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
};
}
function createMockServer(overrides: Partial<VoiceServerRecord> = {}): VoiceServerRecord {
return {
regionId: 'us-default',
serverId: 'server-1',
endpoint: 'wss://voice.example.com',
apiKey: 'test-key',
apiSecret: 'test-secret',
isActive: true,
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
};
}
function createMockTopology(
regions: Array<VoiceRegionRecord>,
serversByRegion: Map<string, Array<VoiceServerRecord>>,
): VoiceTopology {
return {
getAllRegions: () => regions,
getServersForRegion: (regionId: string) => serversByRegion.get(regionId) ?? [],
getRegionMetadataList: () =>
regions.map((r) => ({
id: r.id,
name: r.name,
emoji: r.emoji,
latitude: r.latitude,
longitude: r.longitude,
isDefault: r.isDefault,
vipOnly: r.restrictions.vipOnly,
requiredGuildFeatures: Array.from(r.restrictions.requiredGuildFeatures),
})),
} as VoiceTopology;
}
describe('VoiceAvailabilityService', () => {
let service: VoiceAvailabilityService;
describe('isRegionAccessible', () => {
it('returns true for unrestricted region', () => {
const region = createMockRegion();
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
expect(service.isRegionAccessible(region, context)).toBe(true);
});
it('returns false when user is not in allowedUserIds', () => {
const region = createMockRegion({
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set([456n as UserID]),
},
});
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
expect(service.isRegionAccessible(region, context)).toBe(false);
});
it('returns true when user is in allowedUserIds', () => {
const region = createMockRegion({
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set([123n as UserID]),
},
});
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
expect(service.isRegionAccessible(region, context)).toBe(true);
});
it('returns false for vipOnly region without VIP_VOICE feature', () => {
const region = createMockRegion({
restrictions: {
vipOnly: true,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
});
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
guildId: 456n as GuildID,
guildFeatures: new Set(),
};
expect(service.isRegionAccessible(region, context)).toBe(false);
});
it('returns true for vipOnly region with VIP_VOICE feature', () => {
const region = createMockRegion({
restrictions: {
vipOnly: true,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
});
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
guildId: 456n as GuildID,
guildFeatures: new Set([GuildFeatures.VIP_VOICE]),
};
expect(service.isRegionAccessible(region, context)).toBe(true);
});
it('returns false for vipOnly region without guildId', () => {
const region = createMockRegion({
restrictions: {
vipOnly: true,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
});
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
expect(service.isRegionAccessible(region, context)).toBe(false);
});
it('returns true for guild in allowedGuildIds', () => {
const region = createMockRegion({
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set([456n as GuildID]),
allowedUserIds: new Set(),
},
});
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
guildId: 456n as GuildID,
};
expect(service.isRegionAccessible(region, context)).toBe(true);
});
it('returns false for guild not in allowedGuildIds', () => {
const region = createMockRegion({
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set([789n as GuildID]),
allowedUserIds: new Set(),
},
});
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
guildId: 456n as GuildID,
};
expect(service.isRegionAccessible(region, context)).toBe(false);
});
it('returns true for guild with required feature', () => {
const region = createMockRegion({
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(['PREMIUM']),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
});
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
guildId: 456n as GuildID,
guildFeatures: new Set(['PREMIUM']),
};
expect(service.isRegionAccessible(region, context)).toBe(true);
});
it('returns false for guild without required feature', () => {
const region = createMockRegion({
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(['PREMIUM']),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
});
const topology = createMockTopology([region], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
guildId: 456n as GuildID,
guildFeatures: new Set(),
};
expect(service.isRegionAccessible(region, context)).toBe(false);
});
});
describe('isServerAccessible', () => {
it('returns false for inactive server', () => {
const region = createMockRegion();
const server = createMockServer({isActive: false});
const topology = createMockTopology([region], new Map([['us-default', [server]]]));
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
expect(service.isServerAccessible(server, context)).toBe(false);
});
it('returns true for active unrestricted server', () => {
const region = createMockRegion();
const server = createMockServer();
const topology = createMockTopology([region], new Map([['us-default', [server]]]));
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
expect(service.isServerAccessible(server, context)).toBe(true);
});
it('returns false when user is not in server allowedUserIds', () => {
const region = createMockRegion();
const server = createMockServer({
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set([456n as UserID]),
},
});
const topology = createMockTopology([region], new Map([['us-default', [server]]]));
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
expect(service.isServerAccessible(server, context)).toBe(false);
});
});
describe('getAvailableRegions', () => {
it('returns regions with accessibility status', () => {
const region1 = createMockRegion({id: 'us-default', isDefault: true});
const region2 = createMockRegion({
id: 'eu-vip',
isDefault: false,
restrictions: {
vipOnly: true,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
});
const server1 = createMockServer({regionId: 'us-default'});
const topology = createMockTopology(
[region1, region2],
new Map([
['us-default', [server1]],
['eu-vip', []],
]),
);
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
const regions = service.getAvailableRegions(context);
expect(regions).toHaveLength(2);
expect(regions[0].id).toBe('us-default');
expect(regions[0].isAccessible).toBe(true);
expect(regions[1].id).toBe('eu-vip');
expect(regions[1].isAccessible).toBe(false);
});
it('marks region as not accessible if no servers are available', () => {
const region = createMockRegion();
const topology = createMockTopology([region], new Map([['us-default', []]]));
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
const regions = service.getAvailableRegions(context);
expect(regions).toHaveLength(1);
expect(regions[0].isAccessible).toBe(false);
expect(regions[0].serverCount).toBe(0);
});
});
describe('selectServer', () => {
it('returns server from specified region', () => {
const region = createMockRegion();
const server = createMockServer();
const topology = createMockTopology([region], new Map([['us-default', [server]]]));
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
const selected = service.selectServer('us-default', context);
expect(selected).not.toBeNull();
expect(selected!.serverId).toBe('server-1');
});
it('returns null for region with no servers', () => {
const region = createMockRegion();
const topology = createMockTopology([region], new Map([['us-default', []]]));
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
const selected = service.selectServer('us-default', context);
expect(selected).toBeNull();
});
it('returns null for non-existent region', () => {
const topology = createMockTopology([], new Map());
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
const selected = service.selectServer('non-existent', context);
expect(selected).toBeNull();
});
it('rotates between accessible servers', () => {
const region = createMockRegion();
const server1 = createMockServer({serverId: 'server-1'});
const server2 = createMockServer({serverId: 'server-2'});
const topology = createMockTopology([region], new Map([['us-default', [server1, server2]]]));
service = new VoiceAvailabilityService(topology);
const context: VoiceAccessContext = {
requestingUserId: 123n as UserID,
};
const first = service.selectServer('us-default', context);
const second = service.selectServer('us-default', context);
const third = service.selectServer('us-default', context);
expect(first!.serverId).toBe('server-1');
expect(second!.serverId).toBe('server-2');
expect(third!.serverId).toBe('server-1');
});
});
});

View File

@@ -0,0 +1,139 @@
/*
* 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 {createTestAccount, unclaimAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {
acceptInvite,
createChannelInvite,
createDmChannel,
createFriendship,
createGuild,
getChannel,
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {updateUserSettings} from '@fluxer/api/src/user/tests/UserTestUtils';
import {IncomingCallFlags} from '@fluxer/constants/src/UserConstants';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
describe('Voice Call Eligibility', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
async function setupUsersWithMutualGuild() {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
const guild = await createGuild(harness, user1.token, 'Mutual Guild');
const invite = await createChannelInvite(harness, user1.token, guild.system_channel_id!);
await acceptInvite(harness, user2.token, invite.code);
return {user1, user2, guild};
}
describe('DM call eligibility', () => {
it('returns ringable true for DM between friends', async () => {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
await createFriendship(harness, user1, user2);
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
const callData = await createBuilder<{ringable: boolean; silent?: boolean}>(harness, user1.token)
.get(`/channels/${dmChannel.id}/call`)
.execute();
expect(callData.ringable).toBe(true);
});
it('returns ringable true for DM with mutual guild membership', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
await updateUserSettings(harness, user2.token, {
incoming_call_flags: IncomingCallFlags.GUILD_MEMBERS,
});
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
const callData = await createBuilder<{ringable: boolean}>(harness, user1.token)
.get(`/channels/${dmChannel.id}/call`)
.execute();
expect(callData.ringable).toBe(true);
});
it('returns ringable false for unclaimed account trying DM call', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await unclaimAccount(harness, user1.userId);
const callData = await createBuilder<{ringable: boolean}>(harness, user1.token)
.get(`/channels/${dmChannel.id}/call`)
.execute();
expect(callData.ringable).toBe(false);
});
});
describe('Channel type validation', () => {
it('returns 404 for non-existent channel', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.get('/channels/999999999999999999/call')
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
.execute();
});
it('returns error for text channel call eligibility check', async () => {
const user = await createTestAccount(harness);
await ensureSessionStarted(harness, user.token);
const guild = await createGuild(harness, user.token, 'Test Guild');
const textChannel = await getChannel(harness, user.token, guild.system_channel_id!);
await createBuilder(harness, user.token)
.get(`/channels/${textChannel.id}/call`)
.expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_CHANNEL_TYPE_FOR_CALL')
.execute();
});
});
});

View File

@@ -0,0 +1,157 @@
/*
* 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 {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {
acceptInvite,
createChannelInvite,
createDmChannel,
createFriendship,
createGuild,
getChannel,
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterAll, beforeAll, beforeEach, describe, it} from 'vitest';
describe('Voice Call Ringing', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
async function setupUsersWithMutualGuild() {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
const guild = await createGuild(harness, user1.token, 'Mutual Guild');
const invite = await createChannelInvite(harness, user1.token, guild.system_channel_id!);
await acceptInvite(harness, user2.token, invite.code);
return {user1, user2, guild};
}
describe('Ring call', () => {
it('rings call recipients in DM channel', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await createBuilder(harness, user1.token)
.post(`/channels/${dmChannel.id}/call/ring`)
.body({recipients: [user2.userId]})
.expect(HTTP_STATUS.NO_CONTENT)
.execute();
});
it('rings call without specifying recipients', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await createBuilder(harness, user1.token)
.post(`/channels/${dmChannel.id}/call/ring`)
.body({})
.expect(HTTP_STATUS.NO_CONTENT)
.execute();
});
it('rings call for friends DM', async () => {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
await createFriendship(harness, user1, user2);
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await createBuilder(harness, user1.token)
.post(`/channels/${dmChannel.id}/call/ring`)
.body({recipients: [user2.userId]})
.expect(HTTP_STATUS.NO_CONTENT)
.execute();
});
it('returns 404 for non-existent channel', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.post('/channels/999999999999999999/call/ring')
.body({recipients: []})
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
.execute();
});
it('returns error for text channel ring', async () => {
const user = await createTestAccount(harness);
await ensureSessionStarted(harness, user.token);
const guild = await createGuild(harness, user.token, 'Test Guild');
const textChannel = await getChannel(harness, user.token, guild.system_channel_id!);
await createBuilder(harness, user.token)
.post(`/channels/${textChannel.id}/call/ring`)
.body({})
.expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_CHANNEL_TYPE_FOR_CALL')
.execute();
});
});
describe('Stop ringing', () => {
it('returns 404 when stopping ring for non-existent call', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await createBuilder(harness, user1.token)
.post(`/channels/${dmChannel.id}/call/stop-ringing`)
.body({recipients: [user2.userId]})
.expect(HTTP_STATUS.NOT_FOUND, 'NO_ACTIVE_CALL')
.execute();
});
it('returns 404 when stopping ring without recipients for non-existent call', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await createBuilder(harness, user1.token)
.post(`/channels/${dmChannel.id}/call/stop-ringing`)
.body({})
.expect(HTTP_STATUS.NOT_FOUND, 'NO_ACTIVE_CALL')
.execute();
});
});
});

View File

@@ -0,0 +1,138 @@
/*
* 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 {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {
acceptInvite,
createChannelInvite,
createDmChannel,
createGuild,
getChannel,
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterAll, beforeAll, beforeEach, describe, it} from 'vitest';
describe('Voice Call Update', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
async function setupUsersWithMutualGuild() {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
const guild = await createGuild(harness, user1.token, 'Mutual Guild');
const invite = await createChannelInvite(harness, user1.token, guild.system_channel_id!);
await acceptInvite(harness, user2.token, invite.code);
return {user1, user2, guild};
}
describe('Update call region', () => {
it('returns 404 when updating region for non-existent call', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await createBuilder(harness, user1.token)
.patch(`/channels/${dmChannel.id}/call`)
.body({region: 'us-west'})
.expect(HTTP_STATUS.NOT_FOUND, 'NO_ACTIVE_CALL')
.execute();
});
it('returns 404 when updating call without body', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await createBuilder(harness, user1.token)
.patch(`/channels/${dmChannel.id}/call`)
.body({})
.expect(HTTP_STATUS.NOT_FOUND, 'NO_ACTIVE_CALL')
.execute();
});
it('returns 404 for non-existent channel update', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.patch('/channels/999999999999999999/call')
.body({region: 'us-west'})
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
.execute();
});
it('returns error for text channel update', async () => {
const user = await createTestAccount(harness);
await ensureSessionStarted(harness, user.token);
const guild = await createGuild(harness, user.token, 'Test Guild');
const textChannel = await getChannel(harness, user.token, guild.system_channel_id!);
await createBuilder(harness, user.token)
.patch(`/channels/${textChannel.id}/call`)
.body({region: 'us-west'})
.expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_CHANNEL_TYPE_FOR_CALL')
.execute();
});
});
describe('End call', () => {
it('ends call for DM channel', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await createBuilder(harness, user1.token)
.post(`/channels/${dmChannel.id}/call/end`)
.body(null)
.expect(HTTP_STATUS.NO_CONTENT)
.execute();
});
it('ends call for channel without active call', async () => {
const {user1, user2} = await setupUsersWithMutualGuild();
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await createBuilder(harness, user1.token)
.post(`/channels/${dmChannel.id}/call/end`)
.body(null)
.expect(HTTP_STATUS.NO_CONTENT)
.execute();
});
});
});

View File

@@ -0,0 +1,264 @@
/*
* 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 {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {
acceptInvite,
addMemberRole,
createChannel,
createChannelInvite,
createGuild,
createPermissionOverwrite,
createRole,
getChannel,
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants';
import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
describe('Voice Channel Permissions', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
describe('Voice channel creation', () => {
it('owner can create voice channel', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
expect(voiceChannel.type).toBe(ChannelTypes.GUILD_VOICE);
expect(voiceChannel.name).toBe('voice-test');
});
it('member without permission cannot create voice channel', async () => {
const owner = await createTestAccount(harness);
const member = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, member.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!);
await acceptInvite(harness, member.token, invite.code);
await createBuilder(harness, member.token)
.post(`/guilds/${guild.id}/channels`)
.body({name: 'voice-test', type: ChannelTypes.GUILD_VOICE})
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_PERMISSIONS')
.execute();
});
it('member with MANAGE_CHANNELS permission can create voice channel', async () => {
const owner = await createTestAccount(harness);
const member = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, member.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!);
await acceptInvite(harness, member.token, invite.code);
const role = await createRole(harness, owner.token, guild.id, {
name: 'Channel Manager',
permissions: Permissions.MANAGE_CHANNELS.toString(),
});
await addMemberRole(harness, owner.token, guild.id, member.userId, role.id);
const voiceChannel = await createBuilder<ChannelResponse>(harness, member.token)
.post(`/guilds/${guild.id}/channels`)
.body({name: 'voice-test', type: ChannelTypes.GUILD_VOICE})
.execute();
expect(voiceChannel.type).toBe(ChannelTypes.GUILD_VOICE);
});
});
describe('Voice channel access', () => {
it('member can view voice channel by default', async () => {
const owner = await createTestAccount(harness);
const member = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, member.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!);
await acceptInvite(harness, member.token, invite.code);
const channel = await getChannel(harness, member.token, voiceChannel.id);
expect(channel.id).toBe(voiceChannel.id);
expect(channel.type).toBe(ChannelTypes.GUILD_VOICE);
});
it('member cannot view voice channel when VIEW_CHANNEL is denied', async () => {
const owner = await createTestAccount(harness);
const member = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, member.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!);
await acceptInvite(harness, member.token, invite.code);
await createPermissionOverwrite(harness, owner.token, voiceChannel.id, member.userId, {
type: 1,
allow: '0',
deny: Permissions.VIEW_CHANNEL.toString(),
});
await createBuilder(harness, member.token)
.get(`/channels/${voiceChannel.id}`)
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_PERMISSIONS')
.execute();
});
});
describe('Voice channel modification', () => {
it('owner can update voice channel name', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
const updated = await createBuilder<ChannelResponse>(harness, owner.token)
.patch(`/channels/${voiceChannel.id}`)
.body({name: 'updated-voice'})
.execute();
expect(updated.name).toBe('updated-voice');
});
it('owner can update voice channel bitrate', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
const updated = await createBuilder<ChannelResponse>(harness, owner.token)
.patch(`/channels/${voiceChannel.id}`)
.body({bitrate: 64000})
.execute();
expect(updated.bitrate).toBe(64000);
});
it('owner can update voice channel user_limit', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
const updated = await createBuilder<ChannelResponse>(harness, owner.token)
.patch(`/channels/${voiceChannel.id}`)
.body({user_limit: 10})
.execute();
expect(updated.user_limit).toBe(10);
});
it('member without permission cannot update voice channel', async () => {
const owner = await createTestAccount(harness);
const member = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, member.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!);
await acceptInvite(harness, member.token, invite.code);
await createBuilder(harness, member.token)
.patch(`/channels/${voiceChannel.id}`)
.body({name: 'updated-voice'})
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_PERMISSIONS')
.execute();
});
});
describe('Voice channel deletion', () => {
it('owner can delete voice channel', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
await createBuilder(harness, owner.token)
.delete(`/channels/${voiceChannel.id}`)
.expect(HTTP_STATUS.NO_CONTENT)
.execute();
await createBuilder(harness, owner.token)
.get(`/channels/${voiceChannel.id}`)
.expect(HTTP_STATUS.NOT_FOUND)
.execute();
});
it('member without permission cannot delete voice channel', async () => {
const owner = await createTestAccount(harness);
const member = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, member.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!);
await acceptInvite(harness, member.token, invite.code);
await createBuilder(harness, member.token)
.delete(`/channels/${voiceChannel.id}`)
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_PERMISSIONS')
.execute();
});
});
});

View File

@@ -0,0 +1,126 @@
/*
* 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 {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {createChannel, createGuild, getChannel} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
describe('Voice Channel RTC Region', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
it('voice channel has null rtc_region by default', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
expect(voiceChannel.rtc_region).toBeNull();
});
it('owner can set rtc_region on voice channel', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
const updated = await createBuilder<ChannelResponse>(harness, owner.token)
.patch(`/channels/${voiceChannel.id}`)
.body({rtc_region: 'us-west'})
.execute();
expect(updated.rtc_region).toBe('us-west');
});
it('owner can clear rtc_region to null', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
await createBuilder<ChannelResponse>(harness, owner.token)
.patch(`/channels/${voiceChannel.id}`)
.body({rtc_region: 'us-west'})
.execute();
const updated = await createBuilder<ChannelResponse>(harness, owner.token)
.patch(`/channels/${voiceChannel.id}`)
.body({rtc_region: null})
.execute();
expect(updated.rtc_region).toBeNull();
});
it('rtc_region persists after fetch', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
await createBuilder<ChannelResponse>(harness, owner.token)
.patch(`/channels/${voiceChannel.id}`)
.body({rtc_region: 'eu-west'})
.execute();
const fetched = await getChannel(harness, owner.token, voiceChannel.id);
expect(fetched.rtc_region).toBe('eu-west');
});
it('voice channel bitrate is set during creation', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
expect(voiceChannel.bitrate).toBeDefined();
expect(typeof voiceChannel.bitrate).toBe('number');
});
it('voice channel user_limit defaults to 0', async () => {
const owner = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
expect(voiceChannel.user_limit).toBe(0);
});
});

View File

@@ -0,0 +1,107 @@
/*
* 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 {VoiceRegionAvailability} from '@fluxer/api/src/voice/VoiceModel';
import {resolveVoiceRegionPreference, selectVoiceRegionId} from '@fluxer/api/src/voice/VoiceRegionSelection';
import {describe, expect, it} from 'vitest';
function createRegionAvailability({
id,
latitude,
longitude,
isDefault,
}: {
id: string;
latitude: number;
longitude: number;
isDefault: boolean;
}): VoiceRegionAvailability {
return {
id,
name: `Region ${id.toUpperCase()}`,
emoji: id.toUpperCase(),
latitude,
longitude,
isDefault,
vipOnly: false,
requiredGuildFeatures: [],
isAccessible: true,
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
serverCount: 1,
activeServerCount: 1,
};
}
describe('VoiceRegionSelection', () => {
it('selects the closest region when coordinates are provided', () => {
const regions = [
createRegionAvailability({id: 'a', latitude: 0, longitude: 0, isDefault: true}),
createRegionAvailability({id: 'b', latitude: 50, longitude: 50, isDefault: false}),
];
const preference = resolveVoiceRegionPreference({
preferredRegionId: null,
accessibleRegions: regions,
availableRegions: regions,
defaultRegionId: null,
});
const selected = selectVoiceRegionId({
preferredRegionId: preference.regionId,
mode: preference.mode,
accessibleRegions: regions,
availableRegions: regions,
latitude: '49',
longitude: '49',
});
expect(selected).toBe('b');
});
it('keeps explicit regions even when coordinates would choose another', () => {
const regions = [
createRegionAvailability({id: 'a', latitude: 0, longitude: 0, isDefault: false}),
createRegionAvailability({id: 'b', latitude: 50, longitude: 50, isDefault: false}),
];
const preference = resolveVoiceRegionPreference({
preferredRegionId: 'a',
accessibleRegions: regions,
availableRegions: regions,
defaultRegionId: null,
});
const selected = selectVoiceRegionId({
preferredRegionId: preference.regionId,
mode: preference.mode,
accessibleRegions: regions,
availableRegions: regions,
latitude: '49',
longitude: '49',
});
expect(preference.mode).toBe('explicit');
expect(selected).toBe('a');
});
});

View File

@@ -0,0 +1,139 @@
/*
* 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 {createTestAccount as authCreateTestAccount, type TestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {
acceptInvite,
createChannel,
createChannelInvite,
createDmChannel,
createFriendship,
createGuild,
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
export async function createTestAccount(harness: ApiTestHarness): Promise<TestAccount> {
return authCreateTestAccount(harness);
}
export async function createTestAccountUnclaimed(harness: ApiTestHarness): Promise<TestAccount> {
const account = await authCreateTestAccount(harness);
await createBuilder(harness, '').post(`/test/users/${account.userId}/unclaim`).body(null).execute();
return account;
}
export async function createGuildWithVoiceChannel(
harness: ApiTestHarness,
token: string,
guildName: string,
): Promise<{guild: GuildResponse; voiceChannel: ChannelResponse}> {
const guild = await createGuild(harness, token, guildName);
const voiceChannel = await createChannel(harness, token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
return {guild, voiceChannel};
}
export interface VoiceTestSetup {
owner: TestAccount;
member: TestAccount;
guild: GuildResponse;
voiceChannel: ChannelResponse;
textChannel: ChannelResponse;
}
export async function setupVoiceTestGuild(harness: ApiTestHarness): Promise<VoiceTestSetup> {
const owner = await createTestAccount(harness);
const member = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, member.token);
const guild = await createGuild(harness, owner.token, 'Voice Test Guild');
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'voice-test', ChannelTypes.GUILD_VOICE);
const textChannel = await createChannel(harness, owner.token, guild.id, 'text-test', ChannelTypes.GUILD_TEXT);
const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!);
await acceptInvite(harness, member.token, invite.code);
return {owner, member, guild, voiceChannel, textChannel};
}
export interface DmCallTestSetup {
user1: TestAccount;
user2: TestAccount;
dmChannel: {id: string; type: number};
}
export async function setupDmCallTest(harness: ApiTestHarness): Promise<DmCallTestSetup> {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
const guild = await createGuild(harness, user1.token, 'Mutual Guild');
const invite = await createChannelInvite(harness, user1.token, guild.system_channel_id!);
await acceptInvite(harness, user2.token, invite.code);
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
return {user1, user2, dmChannel};
}
export async function setupDmCallTestWithFriendship(harness: ApiTestHarness): Promise<DmCallTestSetup> {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
await createFriendship(harness, user1, user2);
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
return {user1, user2, dmChannel};
}
export async function getCallEligibility(
harness: ApiTestHarness,
token: string,
channelId: string,
): Promise<{ringable: boolean; silent?: boolean}> {
return createBuilder<{ringable: boolean; silent?: boolean}>(harness, token)
.get(`/channels/${channelId}/call`)
.execute();
}
export async function ringCall(
harness: ApiTestHarness,
token: string,
channelId: string,
recipients?: Array<string>,
): Promise<void> {
const body = recipients ? {recipients} : {};
await createBuilder(harness, token).post(`/channels/${channelId}/call/ring`).body(body).expect(204).execute();
}
export async function endCall(harness: ApiTestHarness, token: string, channelId: string): Promise<void> {
await createBuilder(harness, token).post(`/channels/${channelId}/call/end`).body(null).expect(204).execute();
}

View File

@@ -0,0 +1,313 @@
/*
* 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 {IVoiceRepository} from '@fluxer/api/src/voice/IVoiceRepository';
import type {VoiceRegionWithServers} from '@fluxer/api/src/voice/VoiceModel';
import {VoiceTopology} from '@fluxer/api/src/voice/VoiceTopology';
import {beforeEach, describe, expect, it, vi} from 'vitest';
function createMockVoiceRepository(regions: Array<VoiceRegionWithServers>): IVoiceRepository {
return {
listRegionsWithServers: vi.fn().mockResolvedValue(regions),
listRegions: vi.fn().mockResolvedValue(regions),
getRegion: vi.fn().mockResolvedValue(null),
getRegionWithServers: vi.fn().mockResolvedValue(null),
upsertRegion: vi.fn().mockResolvedValue(undefined),
deleteRegion: vi.fn().mockResolvedValue(undefined),
createRegion: vi.fn().mockResolvedValue(null),
listServersForRegion: vi.fn().mockResolvedValue([]),
listServers: vi.fn().mockResolvedValue([]),
getServer: vi.fn().mockResolvedValue(null),
createServer: vi.fn().mockResolvedValue(null),
upsertServer: vi.fn().mockResolvedValue(undefined),
deleteServer: vi.fn().mockResolvedValue(undefined),
};
}
describe('VoiceTopology', () => {
let topology: VoiceTopology;
const mockRegions: Array<VoiceRegionWithServers> = [
{
id: 'us-default',
name: 'US Default',
emoji: 'flag-us',
latitude: 39.8283,
longitude: -98.5795,
isDefault: true,
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
createdAt: new Date(),
updatedAt: new Date(),
servers: [
{
regionId: 'us-default',
serverId: 'us-server-1',
endpoint: 'wss://us1.voice.example.com',
apiKey: 'key1',
apiSecret: 'secret1',
isActive: true,
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
createdAt: new Date(),
updatedAt: new Date(),
},
{
regionId: 'us-default',
serverId: 'us-server-2',
endpoint: 'wss://us2.voice.example.com',
apiKey: 'key2',
apiSecret: 'secret2',
isActive: true,
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
createdAt: new Date(),
updatedAt: new Date(),
},
],
},
{
id: 'eu-default',
name: 'EU Default',
emoji: 'flag-eu',
latitude: 50.0755,
longitude: 14.4378,
isDefault: false,
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
createdAt: new Date(),
updatedAt: new Date(),
servers: [
{
regionId: 'eu-default',
serverId: 'eu-server-1',
endpoint: 'wss://eu1.voice.example.com',
apiKey: 'key3',
apiSecret: 'secret3',
isActive: true,
restrictions: {
vipOnly: false,
requiredGuildFeatures: new Set(),
allowedGuildIds: new Set(),
allowedUserIds: new Set(),
},
createdAt: new Date(),
updatedAt: new Date(),
},
],
},
];
beforeEach(() => {
const repository = createMockVoiceRepository(mockRegions);
topology = new VoiceTopology(repository, null);
});
describe('before initialization', () => {
it('returns null for default region before initialization', () => {
expect(topology.getDefaultRegion()).toBeNull();
});
it('returns null for default region id before initialization', () => {
expect(topology.getDefaultRegionId()).toBeNull();
});
it('returns empty array for all regions before initialization', () => {
expect(topology.getAllRegions()).toHaveLength(0);
});
});
describe('after initialization', () => {
beforeEach(async () => {
await topology.initialize();
});
it('returns default region after initialization', () => {
const defaultRegion = topology.getDefaultRegion();
expect(defaultRegion).not.toBeNull();
expect(defaultRegion!.id).toBe('us-default');
expect(defaultRegion!.isDefault).toBe(true);
});
it('returns default region id after initialization', () => {
expect(topology.getDefaultRegionId()).toBe('us-default');
});
it('returns all regions after initialization', () => {
const regions = topology.getAllRegions();
expect(regions).toHaveLength(2);
expect(regions[0].id).toBe('us-default');
expect(regions[1].id).toBe('eu-default');
});
it('returns specific region by id', () => {
const region = topology.getRegion('eu-default');
expect(region).not.toBeNull();
expect(region!.id).toBe('eu-default');
expect(region!.name).toBe('EU Default');
});
it('returns null for non-existent region', () => {
const region = topology.getRegion('non-existent');
expect(region).toBeNull();
});
it('returns servers for a region', () => {
const servers = topology.getServersForRegion('us-default');
expect(servers).toHaveLength(2);
expect(servers[0].serverId).toBe('us-server-1');
expect(servers[1].serverId).toBe('us-server-2');
});
it('returns empty array for region with no servers', () => {
const servers = topology.getServersForRegion('non-existent');
expect(servers).toHaveLength(0);
});
it('returns specific server by region and server id', () => {
const server = topology.getServer('us-default', 'us-server-2');
expect(server).not.toBeNull();
expect(server!.serverId).toBe('us-server-2');
expect(server!.endpoint).toBe('wss://us2.voice.example.com');
});
it('returns null for non-existent server', () => {
const server = topology.getServer('us-default', 'non-existent');
expect(server).toBeNull();
});
it('returns null for server in non-existent region', () => {
const server = topology.getServer('non-existent', 'us-server-1');
expect(server).toBeNull();
});
it('returns region metadata list', () => {
const metadata = topology.getRegionMetadataList();
expect(metadata).toHaveLength(2);
expect(metadata[0]).toEqual({
id: 'us-default',
name: 'US Default',
emoji: 'flag-us',
latitude: 39.8283,
longitude: -98.5795,
isDefault: true,
vipOnly: false,
requiredGuildFeatures: [],
});
});
});
describe('getNextServer', () => {
beforeEach(async () => {
await topology.initialize();
});
it('rotates through servers in order', () => {
const first = topology.getNextServer('us-default');
const second = topology.getNextServer('us-default');
const third = topology.getNextServer('us-default');
expect(first!.serverId).toBe('us-server-1');
expect(second!.serverId).toBe('us-server-2');
expect(third!.serverId).toBe('us-server-1');
});
it('returns null for region with no servers', () => {
const server = topology.getNextServer('non-existent');
expect(server).toBeNull();
});
});
describe('subscriber management', () => {
it('registers and unregisters subscribers', () => {
const subscriber = vi.fn();
topology.registerSubscriber(subscriber);
topology.unregisterSubscriber(subscriber);
});
});
describe('shutdown', () => {
it('shuts down without error', () => {
expect(() => topology.shutdown()).not.toThrow();
});
});
describe('empty regions', () => {
it('handles empty region list', async () => {
const repository = createMockVoiceRepository([]);
topology = new VoiceTopology(repository, null);
await topology.initialize();
expect(topology.getAllRegions()).toHaveLength(0);
expect(topology.getDefaultRegion()).toBeNull();
expect(topology.getDefaultRegionId()).toBeNull();
});
});
describe('no default region', () => {
it('uses first region as fallback when no default is set', async () => {
const regionsWithoutDefault: Array<VoiceRegionWithServers> = [
{
...mockRegions[0],
isDefault: false,
},
mockRegions[1],
];
const repository = createMockVoiceRepository(regionsWithoutDefault);
topology = new VoiceTopology(repository, null);
await topology.initialize();
expect(topology.getDefaultRegionId()).toBe('us-default');
});
});
});

View File

@@ -0,0 +1,119 @@
/*
* 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 {createTestAccount, unclaimAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {
acceptInvite,
createChannelInvite,
createDmChannel,
createFriendship,
createGuild,
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {updateUserSettings} from '@fluxer/api/src/user/tests/UserTestUtils';
import {IncomingCallFlags} from '@fluxer/constants/src/UserConstants';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
describe('Voice Unclaimed Account Restrictions', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
describe('DM call restrictions', () => {
it('unclaimed account cannot initiate DM call', async () => {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
const guild = await createGuild(harness, user1.token, 'Mutual Guild');
const invite = await createChannelInvite(harness, user1.token, guild.system_channel_id!);
await acceptInvite(harness, user2.token, invite.code);
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await unclaimAccount(harness, user1.userId);
const callData = await createBuilder<{ringable: boolean}>(harness, user1.token)
.get(`/channels/${dmChannel.id}/call`)
.execute();
expect(callData.ringable).toBe(false);
});
it('claimed account can initiate DM call with unclaimed recipient', async () => {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
const guild = await createGuild(harness, user1.token, 'Mutual Guild');
const invite = await createChannelInvite(harness, user1.token, guild.system_channel_id!);
await acceptInvite(harness, user2.token, invite.code);
await updateUserSettings(harness, user2.token, {
incoming_call_flags: IncomingCallFlags.GUILD_MEMBERS,
});
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await unclaimAccount(harness, user2.userId);
const callData = await createBuilder<{ringable: boolean}>(harness, user1.token)
.get(`/channels/${dmChannel.id}/call`)
.execute();
expect(callData.ringable).toBe(true);
});
it('unclaimed account cannot call friend', async () => {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
await createFriendship(harness, user1, user2);
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
await unclaimAccount(harness, user1.userId);
const callData = await createBuilder<{ringable: boolean}>(harness, user1.token)
.get(`/channels/${dmChannel.id}/call`)
.execute();
expect(callData.ringable).toBe(false);
});
});
});