mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* 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>
1185 lines
40 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|