Files
OwnCord/Client/tauri-client/tests/unit/voice.store.test.ts
T
J3vbandClaude Fable 5 d880b64d64 test: audit 2026-08-19 — fix stale tests, close coverage gaps (#1397)
* test(server): admin/handlers/channels — test-audit 2026-08-19 fixes

* test(server): api/constants — test-audit 2026-08-19 fixes

* test(server): api/middleware — test-audit 2026-08-19 fixes

* test(server): api/waf — test-audit 2026-08-19 fixes

* test(server): auth/totp/encrypt — test-audit 2026-08-19 fixes

* test(server): db/session/expiry/test — test-audit 2026-08-19 fixes

* test(server): migrations/030/attachments/unlink/on/message/delete — test-audit 2026-08-19 fixes

* test(server): updater/download — test-audit 2026-08-19 fixes

* test(server): ws/handlers_command — test-audit 2026-08-19 fixes

* test(server): ws/hub/broadcast — test-audit 2026-08-19 fixes

* test(server): ws/hub/events — test-audit 2026-08-19 fixes

* test(server): ws/livekit/webhook — test-audit 2026-08-19 fixes

* test(server): ws/voice/controls — test-audit 2026-08-19 fixes

* test(server): ws/voice/join — test-audit 2026-08-19 fixes

* test(server): ws/voice/moderation — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/commands.rs — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/secret_store.rs — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/update_commands.rs — test-audit 2026-08-19 fixes

* test(client): src/components/ChannelSidebar.ts — test-audit 2026-08-19 fixes

* test(client): src/lib/ws.ts — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/credentials.rs — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/tofu.rs — test-audit 2026-08-19 fixes

* test(client): src/lib/hostValidation.ts — test-audit 2026-08-19 fixes

* test(client): src/lib/rate-limiter.ts — test-audit 2026-08-19 fixes

* test(client): src/pages/connect-page/LoginForm.ts — test-audit 2026-08-19 fixes

* test(client): src/pages/main-page/SidebarArea.ts — test-audit 2026-08-19 fixes

* test(client): src/stores/voice.store.ts — test-audit 2026-08-19 fixes

* test(client): tests/browser/smoke.test.ts — test-audit 2026-08-19 fixes

* test(client): tests/unit/media.test.ts — test-audit 2026-08-19 fixes

* test(client): tests/unit/renderers.test.ts — test-audit 2026-08-19 fixes

* test(client): src/components/UserProfilePopup.ts — test-audit 2026-08-19 fixes

* test(client): src/lib/e2eeCrypto.ts — test-audit 2026-08-19 fixes

* test(client): tests/unit/log-persistence.test.ts — test-audit 2026-08-19 fixes

* test(client): keep tests/browser out of the jsdom suite and run it in CI

* test(server): ws/hub_broadcast_test.go — bytes.Equal payload compare (gocritic)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(client): src/lib/credentials.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/dispatcher.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/permissions.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/rate-limiter.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/hostValidation.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/stores/messages.store.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/e2eeCrypto.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/ws.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/identity.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/livekitE2EE.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/stores/auth.store.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/stores/voice.store.ts — test-audit 2026-08-19 round 2 (Stryker)

* docs: test audit 2026-08-19 — findings, fixes, measured baselines

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(graph): refresh the knowledge graph after the 2026-08-19 test audit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 19:29:31 +02:00

1185 lines
40 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
voiceStore,
resetVoiceStore,
setVoiceStates,
updateVoiceState,
updateVoiceUserProfile,
removeVoiceUser,
joinVoiceChannel,
leaveVoiceChannel,
setLocalMuted,
setLocalDeafened,
setPttGated,
setLocalCamera,
setLocalScreenshare,
setListenOnly,
setLocalSpeaking,
setSpeakers,
setVoiceConfig,
getChannelVoiceUsers,
setVoiceStatus,
setEncryptionDegraded,
setPeerVerification,
clearPeerVerification,
clearPeerVerifications,
getPeerVerification,
setLocalSessionFingerprint,
setPttPollingLive,
isPttPollingLive,
} from "../../src/stores/voice.store";
import type { ReadyVoiceState, VoiceStatePayload, VoiceLeavePayload } from "../../src/lib/types";
import { authStore } from "../../src/stores/auth.store";
function resetStore(): void {
voiceStore.setState(() => ({
currentChannelId: null,
voiceUsers: new Map(),
voiceConfigs: new Map(),
localMuted: false,
localDeafened: false,
localCamera: false,
localScreenshare: false,
joinedAt: null,
listenOnly: false,
voiceStatus: "idle",
peerVerifications: new Map(),
}));
}
const VOICE_STATE_1: ReadyVoiceState = {
channel_id: 10,
user_id: 1,
muted: false,
deafened: false,
};
const VOICE_STATE_2: ReadyVoiceState = {
channel_id: 10,
user_id: 2,
muted: true,
deafened: false,
};
const VOICE_STATE_3: ReadyVoiceState = {
channel_id: 20,
user_id: 3,
muted: false,
deafened: true,
};
const FULL_VOICE_PAYLOAD: VoiceStatePayload = {
channel_id: 10,
user_id: 5,
username: "dave",
muted: false,
deafened: false,
speaking: true,
camera: false,
screenshare: false,
};
describe("voice store", () => {
beforeEach(() => {
resetStore();
});
describe("initial state", () => {
it("has null currentChannelId", () => {
expect(voiceStore.getState().currentChannelId).toBeNull();
});
it("has empty voiceUsers map", () => {
expect(voiceStore.getState().voiceUsers.size).toBe(0);
});
it("has localMuted false", () => {
expect(voiceStore.getState().localMuted).toBe(false);
});
it("has localDeafened false", () => {
expect(voiceStore.getState().localDeafened).toBe(false);
});
it("has localCamera false", () => {
expect(voiceStore.getState().localCamera).toBe(false);
});
it("has localScreenshare false", () => {
expect(voiceStore.getState().localScreenshare).toBe(false);
});
});
describe("setVoiceStates", () => {
it("populates voice users grouped by channel", () => {
setVoiceStates([VOICE_STATE_1, VOICE_STATE_2, VOICE_STATE_3]);
const state = voiceStore.getState();
expect(state.voiceUsers.size).toBe(2); // 2 channels
expect(state.voiceUsers.get(10)?.size).toBe(2);
expect(state.voiceUsers.get(20)?.size).toBe(1);
});
it("maps muted/deafened from ready payload", () => {
setVoiceStates([VOICE_STATE_2]);
const user = voiceStore.getState().voiceUsers.get(10)?.get(2);
expect(user?.muted).toBe(true);
expect(user?.deafened).toBe(false);
});
it("sets default false for speaking, camera, screenshare", () => {
setVoiceStates([VOICE_STATE_1]);
const user = voiceStore.getState().voiceUsers.get(10)?.get(1);
expect(user?.speaking).toBe(false);
expect(user?.camera).toBe(false);
expect(user?.screenshare).toBe(false);
});
it("maps camera/screenshare from the ready payload when present", () => {
// The server marshals the voice_states rows wholesale — a full-ready
// resync mid-call must not blank a peer's active camera/screenshare.
setVoiceStates([{ ...VOICE_STATE_1, camera: true, screenshare: true }]);
const user = voiceStore.getState().voiceUsers.get(10)?.get(1);
expect(user?.camera).toBe(true);
expect(user?.screenshare).toBe(true);
});
it("defaults username to empty string when the user isn't in membersStore", () => {
// VOICE_STATE_1's user_id (1) has no matching membersStore entry in
// this test file, so the `member?.username ?? ""` fallback applies.
setVoiceStates([VOICE_STATE_1]);
const user = voiceStore.getState().voiceUsers.get(10)?.get(1);
expect(user?.username).toBe("");
});
it("defaults serverMuted/serverDeafened to false when the ready row omits them", () => {
setVoiceStates([VOICE_STATE_1]); // no server_muted/server_deafened on this fixture
const user = voiceStore.getState().voiceUsers.get(10)?.get(1);
expect(user?.serverMuted).toBe(false);
expect(user?.serverDeafened).toBe(false);
});
it("carries serverMuted/serverDeafened through from the ready row when true", () => {
setVoiceStates([{ ...VOICE_STATE_1, server_muted: true, server_deafened: true }]);
const user = voiceStore.getState().voiceUsers.get(10)?.get(1);
expect(user?.serverMuted).toBe(true);
expect(user?.serverDeafened).toBe(true);
});
it("does not auto-join when there is no signed-in user, even if a row's user_id is 0", () => {
// authStore has no user in this test, so currentUserId defaults to 0
// (authStore.getState().user?.id ?? 0). The self-state lookup loop
// must be gated on "there IS a signed-in user", not run unconditionally
// and coincidentally match a user_id: 0 row.
joinVoiceChannel(42);
setVoiceStates([{ ...VOICE_STATE_1, user_id: 0 }]);
expect(voiceStore.getState().currentChannelId).toBe(42);
});
it("replaces existing voice states entirely", () => {
setVoiceStates([VOICE_STATE_1, VOICE_STATE_2]);
setVoiceStates([VOICE_STATE_3]);
const state = voiceStore.getState();
expect(state.voiceUsers.size).toBe(1);
expect(state.voiceUsers.has(10)).toBe(false);
expect(state.voiceUsers.has(20)).toBe(true);
});
describe("local moderator-flag derivation (v049)", () => {
afterEach(() => {
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: "",
isAuthenticated: false,
}));
});
it("derives localServerMuted/localServerDeafened from the signed-in user's row", () => {
authStore.setState((prev) => ({
...prev,
user: { id: 1, username: "me", avatar: null, role: "member" },
}));
// A full-ready reconnect (mustFullResync / replay miss) that
// preserves a live voice session reports the moderator flags on the
// signed-in user's own row.
setVoiceStates([
{ ...VOICE_STATE_1, server_muted: true, server_deafened: true },
VOICE_STATE_2,
]);
const state = voiceStore.getState();
expect(state.localServerMuted).toBe(true);
expect(state.localServerDeafened).toBe(true);
});
it("resets the local moderator flags to false when the signed-in user's row omits them", () => {
authStore.setState((prev) => ({
...prev,
user: { id: 1, username: "me", avatar: null, role: "member" },
}));
// Simulate a stale localServerMuted from before the resync.
voiceStore.setState((prev) => ({ ...prev, localServerMuted: true }));
setVoiceStates([VOICE_STATE_1]); // no server_muted/server_deafened on this row
const state = voiceStore.getState();
expect(state.localServerMuted).toBe(false);
expect(state.localServerDeafened).toBe(false);
});
it("leaves the local moderator flags false when the signed-in user is absent from the payload", () => {
authStore.setState((prev) => ({
...prev,
user: { id: 999, username: "me", avatar: null, role: "member" },
}));
setVoiceStates([{ ...VOICE_STATE_1, server_muted: true }]);
const state = voiceStore.getState();
expect(state.localServerMuted).toBe(false);
expect(state.localServerDeafened).toBe(false);
});
});
});
describe("updateVoiceState", () => {
it("adds a new user to a channel", () => {
updateVoiceState(FULL_VOICE_PAYLOAD);
const user = voiceStore.getState().voiceUsers.get(10)?.get(5);
expect(user).toEqual({
userId: 5,
username: "dave",
muted: false,
deafened: false,
speaking: true,
camera: false,
screenshare: false,
// Absent from the payload means not moderator-imposed.
serverMuted: false,
serverDeafened: false,
});
});
it("updates an existing user in the same channel", () => {
updateVoiceState(FULL_VOICE_PAYLOAD);
updateVoiceState({ ...FULL_VOICE_PAYLOAD, muted: true, speaking: false });
const user = voiceStore.getState().voiceUsers.get(10)?.get(5);
expect(user?.muted).toBe(true);
expect(user?.speaking).toBe(false);
});
it("does not affect other channels", () => {
setVoiceStates([VOICE_STATE_3]);
updateVoiceState(FULL_VOICE_PAYLOAD);
expect(voiceStore.getState().voiceUsers.get(20)?.size).toBe(1);
});
it("produces a new state object", () => {
const before = voiceStore.getState();
updateVoiceState(FULL_VOICE_PAYLOAD);
expect(voiceStore.getState()).not.toBe(before);
});
it("mirrors the moderator flags into the local ones for the signed-in user", () => {
authStore.setState((prev) => ({
...prev,
user: { id: 5, username: "dave", avatar: null, role: "member" },
}));
updateVoiceState({
...FULL_VOICE_PAYLOAD,
muted: true,
deafened: true,
server_muted: true,
server_deafened: true,
});
const state = voiceStore.getState();
expect(state.localServerMuted).toBe(true);
expect(state.localServerDeafened).toBe(true);
expect(state.voiceUsers.get(10)?.get(5)?.serverMuted).toBe(true);
});
it("leaves the local flags alone for another user's state", () => {
authStore.setState((prev) => ({
...prev,
user: { id: 999, username: "me", avatar: null, role: "member" },
}));
updateVoiceState({ ...FULL_VOICE_PAYLOAD, muted: true, server_muted: true });
expect(voiceStore.getState().localServerMuted).not.toBe(true);
});
});
describe("updateVoiceUserProfile", () => {
it("patches the username in every channel the user occupies", () => {
setVoiceStates([VOICE_STATE_1]); // user 1 in channel 10
updateVoiceState({ ...FULL_VOICE_PAYLOAD, user_id: 1, channel_id: 20 }); // user 1 also in channel 20
updateVoiceUserProfile(1, { username: "renamed" });
expect(voiceStore.getState().voiceUsers.get(10)?.get(1)?.username).toBe("renamed");
expect(voiceStore.getState().voiceUsers.get(20)?.get(1)?.username).toBe("renamed");
});
it("is a no-op (same state reference) when the user is in no voice channel", () => {
setVoiceStates([VOICE_STATE_1]);
const before = voiceStore.getState();
updateVoiceUserProfile(999, { username: "nobody" });
expect(voiceStore.getState()).toBe(before);
});
});
describe("removeVoiceUser", () => {
it("removes a user from a channel", () => {
setVoiceStates([VOICE_STATE_1, VOICE_STATE_2]);
const payload: VoiceLeavePayload = { channel_id: 10, user_id: 1 };
removeVoiceUser(payload);
expect(voiceStore.getState().voiceUsers.get(10)?.has(1)).toBe(false);
expect(voiceStore.getState().voiceUsers.get(10)?.size).toBe(1);
});
it("removes channel entry when last user leaves", () => {
setVoiceStates([VOICE_STATE_3]);
removeVoiceUser({ channel_id: 20, user_id: 3 });
expect(voiceStore.getState().voiceUsers.has(20)).toBe(false);
});
it("is a no-op for non-existent user", () => {
setVoiceStates([VOICE_STATE_1]);
const before = voiceStore.getState();
removeVoiceUser({ channel_id: 10, user_id: 999 });
expect(voiceStore.getState()).toBe(before);
});
it("is a no-op for non-existent channel", () => {
const before = voiceStore.getState();
removeVoiceUser({ channel_id: 999, user_id: 1 });
expect(voiceStore.getState()).toBe(before);
});
});
describe("joinVoiceChannel / leaveVoiceChannel", () => {
it("joinVoiceChannel sets currentChannelId", () => {
joinVoiceChannel(42);
expect(voiceStore.getState().currentChannelId).toBe(42);
});
it("leaveVoiceChannel clears the moderator-imposed flags with the session", () => {
authStore.setState((prev) => ({
...prev,
user: { id: 5, username: "dave", avatar: null, role: "member" },
}));
joinVoiceChannel(10);
updateVoiceState({ ...FULL_VOICE_PAYLOAD, muted: true, server_muted: true });
expect(voiceStore.getState().localServerMuted).toBe(true);
leaveVoiceChannel();
expect(voiceStore.getState().localServerMuted).toBe(false);
expect(voiceStore.getState().localServerDeafened).toBe(false);
// This describe block has no shared afterEach — without this reset the
// signed-in user leaks into every later test here (they'd silently
// stop exercising the "no signed-in user" / currentUserId === 0 path).
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: null,
isAuthenticated: false,
}));
});
it("joinVoiceChannel overwrites previous channel", () => {
joinVoiceChannel(42);
joinVoiceChannel(99);
expect(voiceStore.getState().currentChannelId).toBe(99);
});
it("leaveVoiceChannel clears currentChannelId", () => {
joinVoiceChannel(42);
leaveVoiceChannel();
expect(voiceStore.getState().currentChannelId).toBeNull();
});
it("leaveVoiceChannel is safe when not in a channel", () => {
leaveVoiceChannel();
expect(voiceStore.getState().currentChannelId).toBeNull();
});
it("leaveVoiceChannel clears encryptionDegraded with the session", () => {
joinVoiceChannel(10);
voiceStore.setState((prev) => ({ ...prev, encryptionDegraded: true }));
leaveVoiceChannel();
expect(voiceStore.getState().encryptionDegraded).toBe(false);
});
it("leaveVoiceChannel with no signed-in user does not touch voiceUsers, even if a row's user_id is 0", () => {
// authStore has no user, so currentUserId defaults to 0. The
// `currentUserId === 0` guard must take the early-return branch
// (which never touches voiceUsers) rather than falling through and
// deleting a "user_id: 0" row it has no business owning.
setVoiceStates([{ ...VOICE_STATE_1, user_id: 0 }]);
joinVoiceChannel(10);
leaveVoiceChannel();
expect(voiceStore.getState().voiceUsers.get(10)?.has(0)).toBe(true);
});
it("leaveVoiceChannel preserves the voiceUsers map reference when the current user isn't in it", () => {
authStore.setState((prev) => ({
...prev,
user: { id: 5, username: "dave", avatar: null, role: "member" },
}));
// Join channel 10 locally, but voiceUsers was never populated for
// user 5 (setVoiceStates/updateVoiceState were never called for it) —
// the second guard (existingChannel missing / doesn't have the user)
// must early-return without allocating a new voiceUsers Map.
joinVoiceChannel(10);
const usersBefore = voiceStore.getState().voiceUsers;
leaveVoiceChannel();
expect(voiceStore.getState().voiceUsers).toBe(usersBefore);
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: null,
isAuthenticated: false,
}));
});
});
describe("setLocalMuted / setLocalDeafened", () => {
it("setLocalMuted sets muted to true", () => {
setLocalMuted(true);
expect(voiceStore.getState().localMuted).toBe(true);
});
it("setLocalMuted sets muted to false", () => {
setLocalMuted(true);
setLocalMuted(false);
expect(voiceStore.getState().localMuted).toBe(false);
});
it("setLocalDeafened sets deafened to true", () => {
setLocalDeafened(true);
expect(voiceStore.getState().localDeafened).toBe(true);
});
it("setLocalDeafened sets deafened to false", () => {
setLocalDeafened(true);
setLocalDeafened(false);
expect(voiceStore.getState().localDeafened).toBe(false);
});
});
describe("setPttGated", () => {
it("sets pttGated to true", () => {
setPttGated(true);
expect(voiceStore.getState().pttGated).toBe(true);
});
it("sets pttGated to false", () => {
setPttGated(true);
setPttGated(false);
expect(voiceStore.getState().pttGated).toBe(false);
});
it("does not touch localMuted — PTT must never write the explicit-mute flag (v006)", () => {
setLocalMuted(true);
setPttGated(false); // PTT key pressed
expect(voiceStore.getState().localMuted).toBe(true);
expect(voiceStore.getState().pttGated).toBe(false);
});
it("is a no-op (same state reference) when the value hasn't changed", () => {
setPttGated(true);
const before = voiceStore.getState();
setPttGated(true);
expect(voiceStore.getState()).toBe(before);
});
});
describe("setEncryptionDegraded", () => {
it("sets encryptionDegraded to true", () => {
setEncryptionDegraded(true);
expect(voiceStore.getState().encryptionDegraded).toBe(true);
});
it("sets encryptionDegraded back to false", () => {
setEncryptionDegraded(true);
setEncryptionDegraded(false);
expect(voiceStore.getState().encryptionDegraded).toBe(false);
});
it("is a no-op (same state reference) when the value hasn't changed", () => {
setEncryptionDegraded(true);
const before = voiceStore.getState();
setEncryptionDegraded(true);
expect(voiceStore.getState()).toBe(before);
});
});
describe("setLocalCamera / setLocalScreenshare", () => {
it("setLocalCamera sets camera to true", () => {
setLocalCamera(true);
expect(voiceStore.getState().localCamera).toBe(true);
});
it("setLocalCamera sets camera to false", () => {
setLocalCamera(true);
setLocalCamera(false);
expect(voiceStore.getState().localCamera).toBe(false);
});
it("setLocalScreenshare sets screenshare to true", () => {
setLocalScreenshare(true);
expect(voiceStore.getState().localScreenshare).toBe(true);
});
it("setLocalScreenshare sets screenshare to false", () => {
setLocalScreenshare(true);
setLocalScreenshare(false);
expect(voiceStore.getState().localScreenshare).toBe(false);
});
});
describe("setLocalSpeaking", () => {
it("updates speaking state for current user in active channel", () => {
// Set up: current user (id=1) in channel 10
authStore.setState(() => ({
token: "t",
user: { id: 1, username: "me", avatar: "", role: "member" },
serverName: "s",
motd: "",
isAuthenticated: true,
}));
setVoiceStates([VOICE_STATE_1]);
joinVoiceChannel(10);
setLocalSpeaking(true);
const user = voiceStore.getState().voiceUsers.get(10)?.get(1);
expect(user?.speaking).toBe(true);
setLocalSpeaking(false);
const userAfter = voiceStore.getState().voiceUsers.get(10)?.get(1);
expect(userAfter?.speaking).toBe(false);
// Cleanup
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: null,
isAuthenticated: false,
}));
});
it("is a no-op when not in a voice channel", () => {
const before = voiceStore.getState();
setLocalSpeaking(true);
expect(voiceStore.getState()).toBe(before);
});
it("is a no-op with no signed-in user, even if a row's user_id is 0", () => {
// authStore has no user, so currentUserId defaults to 0. Without the
// `currentUserId === 0` early return, the function would happily
// treat a "user_id: 0" roster row as "us" and flip its speaking flag.
setVoiceStates([{ ...VOICE_STATE_1, user_id: 0 }]);
joinVoiceChannel(10);
setLocalSpeaking(true);
expect(voiceStore.getState().voiceUsers.get(10)?.get(0)?.speaking).toBe(false);
});
it("is a no-op (same state reference) when speaking already matches the requested value", () => {
authStore.setState(() => ({
token: "t",
user: { id: 1, username: "me", avatar: "", role: "member" },
serverName: "s",
motd: "",
isAuthenticated: true,
}));
setVoiceStates([VOICE_STATE_1]);
joinVoiceChannel(10);
setLocalSpeaking(true);
const before = voiceStore.getState();
setLocalSpeaking(true); // already true — must not allocate a new state
expect(voiceStore.getState()).toBe(before);
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: null,
isAuthenticated: false,
}));
});
});
describe("getChannelVoiceUsers", () => {
it("returns all voice users for a channel", () => {
setVoiceStates([VOICE_STATE_1, VOICE_STATE_2]);
const users = getChannelVoiceUsers(10);
expect(users).toHaveLength(2);
expect(users.map((u) => u.userId).sort()).toEqual([1, 2]);
});
it("returns empty array for unknown channel", () => {
expect(getChannelVoiceUsers(999)).toHaveLength(0);
});
it("returns empty array when no voice states exist", () => {
expect(getChannelVoiceUsers(10)).toHaveLength(0);
});
});
describe("setSpeakers", () => {
beforeEach(() => {
// Set up: user 1 (local) and user 2 (remote) in channel 10
authStore.setState(() => ({
token: "t",
user: { id: 1, username: "me", avatar: "", role: "member" },
serverName: "s",
motd: "",
isAuthenticated: true,
}));
setVoiceStates([VOICE_STATE_1, VOICE_STATE_2]);
joinVoiceChannel(10);
});
afterEach(() => {
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: null,
isAuthenticated: false,
}));
});
it("updates local user's speaking state from LiveKit", () => {
// LiveKit says we're speaking
setSpeakers({ channel_id: 10, speakers: [1, 2], threshold_mode: "forwarding" });
expect(voiceStore.getState().voiceUsers.get(10)?.get(1)?.speaking).toBe(true);
// LiveKit says we're NOT speaking — should update
setSpeakers({ channel_id: 10, speakers: [2], threshold_mode: "forwarding" });
expect(voiceStore.getState().voiceUsers.get(10)?.get(1)?.speaking).toBe(false);
});
it("updates remote users' speaking state from server", () => {
// Server says user 2 is speaking
setSpeakers({ channel_id: 10, speakers: [2], threshold_mode: "forwarding" });
expect(voiceStore.getState().voiceUsers.get(10)?.get(2)?.speaking).toBe(true);
// Server says nobody is speaking — remote user updated, local unchanged
setSpeakers({ channel_id: 10, speakers: [], threshold_mode: "forwarding" });
expect(voiceStore.getState().voiceUsers.get(10)?.get(2)?.speaking).toBe(false);
});
it("preserves the VoiceUser object reference for a user whose speaking state doesn't change", () => {
// Both users default to speaking: false (from setVoiceStates). An
// empty speakers list changes nothing, so the unchanged branch must
// reuse the existing VoiceUser object rather than cloning it.
const before = voiceStore.getState().voiceUsers.get(10)?.get(1);
setSpeakers({ channel_id: 10, speakers: [], threshold_mode: "forwarding" });
expect(voiceStore.getState().voiceUsers.get(10)?.get(1)).toBe(before);
});
});
describe("setListenOnly", () => {
it("sets listenOnly to true", () => {
setListenOnly(true);
expect(voiceStore.getState().listenOnly).toBe(true);
});
it("sets listenOnly back to false", () => {
setListenOnly(true);
setListenOnly(false);
expect(voiceStore.getState().listenOnly).toBe(false);
});
});
describe("setVoiceConfig", () => {
it("stores voice config for a channel", () => {
setVoiceConfig({
channel_id: 10,
quality: "high",
bitrate: 128000,
threshold_mode: "forwarding",
mixing_threshold: 5,
top_speakers: 3,
max_users: 50,
});
const config = voiceStore.getState().voiceConfigs.get(10);
expect(config).toEqual({
quality: "high",
bitrate: 128000,
threshold_mode: "forwarding",
mixing_threshold: 5,
top_speakers: 3,
max_users: 50,
});
});
it("overwrites existing config for the same channel", () => {
setVoiceConfig({
channel_id: 10,
quality: "low",
bitrate: 64000,
threshold_mode: "mixing",
mixing_threshold: 3,
top_speakers: 2,
max_users: 25,
});
setVoiceConfig({
channel_id: 10,
quality: "high",
bitrate: 128000,
threshold_mode: "forwarding",
mixing_threshold: 5,
top_speakers: 3,
max_users: 50,
});
const config = voiceStore.getState().voiceConfigs.get(10);
expect(config?.quality).toBe("high");
expect(config?.bitrate).toBe(128000);
});
it("does not affect other channels' configs", () => {
setVoiceConfig({
channel_id: 10,
quality: "low",
bitrate: 64000,
threshold_mode: "mixing",
mixing_threshold: 3,
top_speakers: 2,
max_users: 25,
});
setVoiceConfig({
channel_id: 20,
quality: "high",
bitrate: 128000,
threshold_mode: "forwarding",
mixing_threshold: 5,
top_speakers: 3,
max_users: 50,
});
expect(voiceStore.getState().voiceConfigs.get(10)?.quality).toBe("low");
expect(voiceStore.getState().voiceConfigs.get(20)?.quality).toBe("high");
});
});
describe("joinVoiceChannel — same channel no-op", () => {
it("does not reset joinedAt when re-joining the same channel", () => {
// Date.now() spied with distinct values per call — two real calls in
// the same millisecond would otherwise make this assertion pass by
// coincidence even if the same-channel guard were gone entirely.
const nowSpy = vi.spyOn(Date, "now").mockReturnValueOnce(1000).mockReturnValueOnce(2000);
try {
joinVoiceChannel(42);
const firstJoinedAt = voiceStore.getState().joinedAt;
expect(firstJoinedAt).toBe(1000);
// Re-join same channel — guard must return `prev` before Date.now()
// is called again, so joinedAt stays 1000, not the mocked 2000.
joinVoiceChannel(42);
expect(voiceStore.getState().joinedAt).toBe(firstJoinedAt);
} finally {
nowSpy.mockRestore();
}
});
it("resets joinedAt when joining a different channel", () => {
joinVoiceChannel(42);
joinVoiceChannel(99);
expect(voiceStore.getState().joinedAt).not.toBeNull();
expect(voiceStore.getState().currentChannelId).toBe(99);
});
it("clears encryptionDegraded on a fresh join — a new session doesn't inherit the previous one's degraded flag", () => {
voiceStore.setState((prev) => ({ ...prev, encryptionDegraded: true }));
joinVoiceChannel(42);
expect(voiceStore.getState().encryptionDegraded).toBe(false);
});
});
describe("voiceStatus", () => {
it("seeds 'joining' optimistically on a fresh join", () => {
expect(voiceStore.getState().voiceStatus).toBe("idle");
joinVoiceChannel(42);
expect(voiceStore.getState().voiceStatus).toBe("joining");
});
it("resets to 'idle' on leaveVoiceChannel", () => {
joinVoiceChannel(42);
setVoiceStatus("connected");
leaveVoiceChannel();
expect(voiceStore.getState().voiceStatus).toBe("idle");
});
it("setVoiceStatus writes the given status", () => {
setVoiceStatus("securing");
expect(voiceStore.getState().voiceStatus).toBe("securing");
setVoiceStatus("reconnecting");
expect(voiceStore.getState().voiceStatus).toBe("reconnecting");
});
it("setVoiceStatus is a no-op (same state reference) when the status hasn't changed", () => {
setVoiceStatus("securing");
const before = voiceStore.getState();
setVoiceStatus("securing");
expect(voiceStore.getState()).toBe(before);
});
});
describe("leaveVoiceChannel — clears user from voiceUsers", () => {
it("removes current user from the channel's voiceUsers map", () => {
authStore.setState(() => ({
token: "t",
user: { id: 1, username: "me", avatar: "", role: "member" },
serverName: "s",
motd: "",
isAuthenticated: true,
}));
setVoiceStates([VOICE_STATE_1, VOICE_STATE_2]);
joinVoiceChannel(10);
leaveVoiceChannel();
expect(voiceStore.getState().currentChannelId).toBeNull();
expect(voiceStore.getState().joinedAt).toBeNull();
// User 1 should be removed from channel 10
const ch10 = voiceStore.getState().voiceUsers.get(10);
expect(ch10?.has(1)).toBe(false);
// User 2 should still be present
expect(ch10?.has(2)).toBe(true);
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: null,
isAuthenticated: false,
}));
});
it("removes the channel entry when current user is the last user", () => {
authStore.setState(() => ({
token: "t",
user: { id: 3, username: "me", avatar: "", role: "member" },
serverName: "s",
motd: "",
isAuthenticated: true,
}));
setVoiceStates([VOICE_STATE_3]); // User 3 alone in channel 20
joinVoiceChannel(20);
leaveVoiceChannel();
expect(voiceStore.getState().voiceUsers.has(20)).toBe(false);
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: null,
isAuthenticated: false,
}));
});
});
describe("setVoiceStates — auto-join for current user", () => {
it("auto-joins current user's channel from ready payload", () => {
authStore.setState(() => ({
token: "t",
user: { id: 1, username: "me", avatar: "", role: "member" },
serverName: "s",
motd: "",
isAuthenticated: true,
}));
setVoiceStates([VOICE_STATE_1, VOICE_STATE_2]);
// Current user (id=1) is in channel 10
expect(voiceStore.getState().currentChannelId).toBe(10);
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: null,
isAuthenticated: false,
}));
});
it("preserves currentChannelId when current user is not in the payload", () => {
authStore.setState(() => ({
token: "t",
user: { id: 999, username: "me", avatar: "", role: "member" },
serverName: "s",
motd: "",
isAuthenticated: true,
}));
joinVoiceChannel(42);
setVoiceStates([VOICE_STATE_1, VOICE_STATE_2]); // Neither is user 999
// Should preserve the existing channel since user isn't in the payload
expect(voiceStore.getState().currentChannelId).toBe(42);
authStore.setState(() => ({
token: null,
user: null,
serverName: null,
motd: null,
isAuthenticated: false,
}));
});
});
describe("resetVoiceStore", () => {
it("resets all fields to initial state", () => {
joinVoiceChannel(42);
setLocalMuted(true);
setLocalDeafened(true);
setLocalCamera(true);
setLocalScreenshare(true);
setListenOnly(true);
resetVoiceStore();
const state = voiceStore.getState();
expect(state.currentChannelId).toBeNull();
expect(state.localMuted).toBe(false);
expect(state.localDeafened).toBe(false);
expect(state.localCamera).toBe(false);
expect(state.localScreenshare).toBe(false);
expect(state.joinedAt).toBeNull();
expect(state.listenOnly).toBe(false);
expect(state.voiceUsers.size).toBe(0);
expect(state.voiceConfigs.size).toBe(0);
});
it("resets localServerMuted/localServerDeafened/pttGated/encryptionDegraded to false", () => {
voiceStore.setState((prev) => ({
...prev,
localServerMuted: true,
localServerDeafened: true,
pttGated: true,
encryptionDegraded: true,
}));
resetVoiceStore();
const state = voiceStore.getState();
expect(state.localServerMuted).toBe(false);
expect(state.localServerDeafened).toBe(false);
expect(state.pttGated).toBe(false);
expect(state.encryptionDegraded).toBe(false);
});
});
describe("setPttPollingLive / isPttPollingLive", () => {
afterEach(() => {
// Module-level flag, not covered by resetStore() — restore the
// default so later tests in this file see a clean slate.
setPttPollingLive(false);
});
it("defaults to false", () => {
expect(isPttPollingLive()).toBe(false);
});
it("setPttPollingLive(true) flips isPttPollingLive() to true", () => {
setPttPollingLive(true);
expect(isPttPollingLive()).toBe(true);
});
it("resetVoiceStore() does not clear it — process-wide platform capability, not per-session voice state", () => {
setPttPollingLive(true);
resetVoiceStore();
expect(isPttPollingLive()).toBe(true);
});
});
describe("clearAuth voice cleanup", () => {
it("calls leaveVoice to clean up session state", async () => {
// We test indirectly: clearAuth should call leaveVoice(false) which
// is idempotent, and then resetVoiceStore which clears the store.
const { clearAuth } = await import("../../src/stores/auth.store");
joinVoiceChannel(42);
setLocalMuted(true);
expect(voiceStore.getState().currentChannelId).toBe(42);
clearAuth();
// After clearAuth, voice store should be fully reset
expect(voiceStore.getState().currentChannelId).toBeNull();
expect(voiceStore.getState().localMuted).toBe(false);
expect(voiceStore.getState().voiceUsers.size).toBe(0);
});
});
describe("setPeerVerification / clearPeerVerification / clearPeerVerifications / getPeerVerification (F3 TOFU)", () => {
it("getPeerVerification returns null for an unknown peer", () => {
expect(getPeerVerification(1)).toBeNull();
});
it("setPeerVerification records a peer's verification, readable via getPeerVerification", () => {
setPeerVerification({
userId: 1,
status: "verified",
safetyNumber: "abcd-1234",
sessionFingerprint: "fp-1",
});
expect(getPeerVerification(1)).toEqual({
userId: 1,
status: "verified",
safetyNumber: "abcd-1234",
sessionFingerprint: "fp-1",
});
});
it("setPeerVerification overwrites a previous verification for the same peer", () => {
setPeerVerification({
userId: 1,
status: "unverified",
safetyNumber: null,
sessionFingerprint: "fp-1",
});
setPeerVerification({
userId: 1,
status: "mismatch",
safetyNumber: null,
sessionFingerprint: "fp-2",
});
expect(getPeerVerification(1)?.status).toBe("mismatch");
expect(getPeerVerification(1)?.sessionFingerprint).toBe("fp-2");
});
it("clearPeerVerification removes a single peer's verification", () => {
setPeerVerification({
userId: 1,
status: "verified",
safetyNumber: "x",
sessionFingerprint: "y",
});
setPeerVerification({
userId: 2,
status: "verified",
safetyNumber: "x",
sessionFingerprint: "y",
});
clearPeerVerification(1);
expect(getPeerVerification(1)).toBeNull();
expect(getPeerVerification(2)).not.toBeNull();
});
it("clearPeerVerification is a no-op (same state reference) for a peer with no verification", () => {
setPeerVerification({
userId: 2,
status: "verified",
safetyNumber: "x",
sessionFingerprint: "y",
});
const before = voiceStore.getState();
clearPeerVerification(999);
expect(voiceStore.getState()).toBe(before);
});
it("clearPeerVerifications drops every peer's verification", () => {
setPeerVerification({
userId: 1,
status: "verified",
safetyNumber: "x",
sessionFingerprint: "y",
});
setPeerVerification({
userId: 2,
status: "unverified",
safetyNumber: null,
sessionFingerprint: "z",
});
clearPeerVerifications();
expect(voiceStore.getState().peerVerifications?.size).toBe(0);
});
it("clearPeerVerifications is a no-op (same state reference) when already empty", () => {
const before = voiceStore.getState();
clearPeerVerifications();
expect(voiceStore.getState()).toBe(before);
});
});
describe("setLocalSessionFingerprint", () => {
it("publishes the local session fingerprint", () => {
setLocalSessionFingerprint("fp-local");
expect(voiceStore.getState().localSessionFingerprint).toBe("fp-local");
});
it("clears the fingerprint with null", () => {
setLocalSessionFingerprint("fp-local");
setLocalSessionFingerprint(null);
expect(voiceStore.getState().localSessionFingerprint).toBeNull();
});
it("is a no-op (same state reference) when the value hasn't changed", () => {
setLocalSessionFingerprint("fp-local");
const before = voiceStore.getState();
setLocalSessionFingerprint("fp-local");
expect(voiceStore.getState()).toBe(before);
});
});
describe("subscribe", () => {
it("notifies on state changes", () => {
const listener = vi.fn();
const unsub = voiceStore.subscribe(listener);
joinVoiceChannel(42);
voiceStore.flush();
expect(listener).toHaveBeenCalledTimes(1);
unsub();
});
it("does not notify after unsubscribe", () => {
const listener = vi.fn();
const unsub = voiceStore.subscribe(listener);
unsub();
joinVoiceChannel(42);
expect(listener).not.toHaveBeenCalled();
});
});
// MUST stay the last describe block in this file: vi.resetModules() below
// repoints subsequent `await import(...)` calls at fresh module instances
// for the rest of the file's execution, which would desync any later
// test's dynamically-imported module from this file's statically-imported
// `voiceStore`/`authStore` (as it did for the "clearAuth voice cleanup"
// test above, which relies on that static `voiceStore` observing the
// effect of a dynamically-imported `clearAuth()`).
describe("module INITIAL_STATE (fresh import, untouched by any reset)", () => {
it("defaults every flag to false — the describe blocks above only ever observe state after the outer beforeEach's resetStore() has already overwritten it, and that local helper doesn't even set localServerMuted/localServerDeafened/pttGated/encryptionDegraded (they'd read `undefined` there, not the real default)", async () => {
vi.resetModules();
const fresh = await import("../../src/stores/voice.store");
const state = fresh.voiceStore.getState();
expect(state.localMuted).toBe(false);
expect(state.localDeafened).toBe(false);
expect(state.localServerMuted).toBe(false);
expect(state.localServerDeafened).toBe(false);
expect(state.pttGated).toBe(false);
expect(state.localCamera).toBe(false);
expect(state.localScreenshare).toBe(false);
expect(state.listenOnly).toBe(false);
expect(state.encryptionDegraded).toBe(false);
expect(fresh.isPttPollingLive()).toBe(false);
});
});
});