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>
520 lines
18 KiB
TypeScript
520 lines
18 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import {
|
|
authStore,
|
|
setAuth,
|
|
clearAuth,
|
|
getToken,
|
|
getCurrentUser,
|
|
updateUser,
|
|
} from "../../src/stores/auth.store";
|
|
import {
|
|
voiceStore,
|
|
resetVoiceStore,
|
|
joinVoiceChannel,
|
|
setVoiceStatus,
|
|
} from "../../src/stores/voice.store";
|
|
import { leaveVoice } from "@lib/livekitSession";
|
|
import { setMessages, isChannelLoaded, getChannelMessages } from "../../src/stores/messages.store";
|
|
import { channelsStore, setChannels } from "../../src/stores/channels.store";
|
|
import type { ReadyChannel } from "../../src/lib/types";
|
|
import { acknowledgeNsfw, isNsfwAcknowledged } from "../../src/lib/nsfw-gate";
|
|
import { addLogListener, type LogEntry } from "@lib/logger";
|
|
import type { UserWithRole, MessageResponse, MessageUser } from "../../src/lib/types";
|
|
|
|
// Mock the lazily-imported voice SDK module so we can assert clearAuth() only
|
|
// pulls it in (loading the ~1.3 MB LiveKit chunk) when a voice session exists.
|
|
vi.mock("@lib/livekitSession", () => ({
|
|
leaveVoice: vi.fn(),
|
|
}));
|
|
|
|
const flushMicrotasks = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
const TEST_USER: UserWithRole = {
|
|
id: 42,
|
|
username: "testuser",
|
|
avatar: "avatar.png",
|
|
role: "member",
|
|
};
|
|
|
|
const TEST_TOKEN = "session-token-abc123";
|
|
const TEST_SERVER_NAME = "My OwnCord Server";
|
|
const TEST_MOTD = "Welcome to OwnCord!";
|
|
|
|
function resetStore(): void {
|
|
clearAuth();
|
|
}
|
|
|
|
describe("auth store", () => {
|
|
beforeEach(() => {
|
|
resetStore();
|
|
});
|
|
|
|
// 1. Initial state is unauthenticated
|
|
describe("initial state", () => {
|
|
it("has null token", () => {
|
|
expect(authStore.getState().token).toBeNull();
|
|
});
|
|
|
|
it("has null user", () => {
|
|
expect(authStore.getState().user).toBeNull();
|
|
});
|
|
|
|
it("has null serverName", () => {
|
|
expect(authStore.getState().serverName).toBeNull();
|
|
});
|
|
|
|
it("has null motd", () => {
|
|
expect(authStore.getState().motd).toBeNull();
|
|
});
|
|
|
|
it("is not authenticated", () => {
|
|
expect(authStore.getState().isAuthenticated).toBe(false);
|
|
});
|
|
});
|
|
|
|
// 2. setAuth populates all fields correctly
|
|
describe("setAuth", () => {
|
|
it("sets token", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(authStore.getState().token).toBe(TEST_TOKEN);
|
|
});
|
|
|
|
it("sets user", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(authStore.getState().user).toEqual(TEST_USER);
|
|
});
|
|
|
|
it("sets serverName", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(authStore.getState().serverName).toBe(TEST_SERVER_NAME);
|
|
});
|
|
|
|
it("sets motd", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(authStore.getState().motd).toBe(TEST_MOTD);
|
|
});
|
|
|
|
it("sets isAuthenticated to true", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(authStore.getState().isAuthenticated).toBe(true);
|
|
});
|
|
|
|
it("returns a new state object on each call", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
const first = authStore.getState();
|
|
setAuth("other-token", TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
const second = authStore.getState();
|
|
expect(first).not.toBe(second);
|
|
});
|
|
});
|
|
|
|
// 3. clearAuth resets to initial state
|
|
describe("clearAuth", () => {
|
|
it("resets all fields after being authenticated", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
clearAuth();
|
|
|
|
const state = authStore.getState();
|
|
expect(state.token).toBeNull();
|
|
expect(state.user).toBeNull();
|
|
expect(state.serverName).toBeNull();
|
|
expect(state.motd).toBeNull();
|
|
expect(state.isAuthenticated).toBe(false);
|
|
});
|
|
|
|
it("produces a new state object", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
const before = authStore.getState();
|
|
clearAuth();
|
|
const after = authStore.getState();
|
|
expect(before).not.toBe(after);
|
|
});
|
|
|
|
// v076: acknowledgements are per-viewer consent, not per-device. Host
|
|
// scoping cannot cover a second account on the SAME server, so the age
|
|
// gate must be re-armed on logout or the next user silently inherits it.
|
|
it("clears NSFW acknowledgements so the next account re-sees the age gate", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
acknowledgeNsfw(12);
|
|
expect(isNsfwAcknowledged(12)).toBe(true);
|
|
|
|
clearAuth();
|
|
|
|
expect(isNsfwAcknowledged(12)).toBe(false);
|
|
});
|
|
|
|
it("records 'user' as the default logout reason", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
clearAuth();
|
|
expect(authStore.getState().logoutReason).toBe("user");
|
|
});
|
|
|
|
it("records an explicit logout reason and resets it on the next setAuth", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
clearAuth("server_shutdown");
|
|
expect(authStore.getState().logoutReason).toBe("server_shutdown");
|
|
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(authStore.getState().logoutReason).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// 4. getToken returns current token
|
|
describe("getToken", () => {
|
|
it("returns null when unauthenticated", () => {
|
|
expect(getToken()).toBeNull();
|
|
});
|
|
|
|
it("returns token after setAuth", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(getToken()).toBe(TEST_TOKEN);
|
|
});
|
|
|
|
it("returns null after clearAuth", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
clearAuth();
|
|
expect(getToken()).toBeNull();
|
|
});
|
|
});
|
|
|
|
// 5. updateUser patches user fields
|
|
describe("updateUser", () => {
|
|
it("updates username on authenticated user", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
updateUser({ username: "newname" });
|
|
expect(authStore.getState().user?.username).toBe("newname");
|
|
});
|
|
|
|
it("preserves other user fields when patching", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
updateUser({ username: "newname" });
|
|
const user = authStore.getState().user;
|
|
expect(user?.id).toBe(42);
|
|
expect(user?.avatar).toBe("avatar.png");
|
|
expect(user?.role).toBe("member");
|
|
});
|
|
|
|
it("is a no-op when user is null", () => {
|
|
updateUser({ username: "newname" });
|
|
expect(authStore.getState().user).toBeNull();
|
|
});
|
|
|
|
it("produces a new state object", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
const before = authStore.getState();
|
|
updateUser({ username: "changed" });
|
|
expect(authStore.getState()).not.toBe(before);
|
|
});
|
|
|
|
it("produces a new user object (immutable)", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
const userBefore = authStore.getState().user;
|
|
updateUser({ avatar: "new-avatar.png" });
|
|
const userAfter = authStore.getState().user;
|
|
expect(userBefore).not.toBe(userAfter);
|
|
expect(userAfter?.avatar).toBe("new-avatar.png");
|
|
});
|
|
|
|
it("sets totp_enabled to true", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
updateUser({ totp_enabled: true });
|
|
expect(authStore.getState().user?.totp_enabled).toBe(true);
|
|
});
|
|
|
|
it("sets totp_enabled to false", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
updateUser({ totp_enabled: true });
|
|
updateUser({ totp_enabled: false });
|
|
expect(authStore.getState().user?.totp_enabled).toBe(false);
|
|
});
|
|
|
|
it("initial user has no totp_enabled (undefined)", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(authStore.getState().user?.totp_enabled).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// 6. getCurrentUser returns current user
|
|
describe("getCurrentUser", () => {
|
|
it("returns null when unauthenticated", () => {
|
|
expect(getCurrentUser()).toBeNull();
|
|
});
|
|
|
|
it("returns user after setAuth", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(getCurrentUser()).toEqual(TEST_USER);
|
|
});
|
|
|
|
it("returns null after clearAuth", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
clearAuth();
|
|
expect(getCurrentUser()).toBeNull();
|
|
});
|
|
});
|
|
|
|
// clearAuth voice-session cleanup (regression: don't force-load the LiveKit
|
|
// chunk on every logout/401 for a text-only user).
|
|
describe("clearAuth voice cleanup", () => {
|
|
beforeEach(() => {
|
|
resetVoiceStore();
|
|
vi.mocked(leaveVoice).mockClear();
|
|
});
|
|
|
|
it("does NOT load livekitSession when there is no active voice session", async () => {
|
|
// Voice store is idle (currentChannelId null, voiceStatus "idle").
|
|
clearAuth();
|
|
await flushMicrotasks();
|
|
expect(leaveVoice).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("leaves voice when a voice session is active", async () => {
|
|
joinVoiceChannel(7); // currentChannelId=7, voiceStatus="joining"
|
|
setVoiceStatus("connected");
|
|
clearAuth();
|
|
await flushMicrotasks();
|
|
expect(leaveVoice).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
// Boundary: a channel id can outlive the status settling back to idle
|
|
// (e.g. a leave that updated voiceStatus but hasn't cleared
|
|
// currentChannelId yet). clearAuth's guard is an AND of both conditions,
|
|
// not just "was a channel ever joined" — this pins that a set channel id
|
|
// alone must NOT trigger another leaveVoice call once already idle.
|
|
it("does NOT load livekitSession when the channel id is set but status is already idle", async () => {
|
|
joinVoiceChannel(7);
|
|
setVoiceStatus("idle");
|
|
clearAuth();
|
|
await flushMicrotasks();
|
|
expect(leaveVoice).not.toHaveBeenCalled();
|
|
});
|
|
|
|
// Boundary: the inverse — a non-idle status alone (no channel id) must
|
|
// also NOT trigger leaveVoice. Together with the case above, this pins
|
|
// that clearAuth requires BOTH currentChannelId set AND status !== idle,
|
|
// not either one alone.
|
|
it("does NOT load livekitSession when status is non-idle but no channel id is set", async () => {
|
|
setVoiceStatus("reconnecting");
|
|
clearAuth();
|
|
await flushMicrotasks();
|
|
expect(leaveVoice).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("logs a warning tagged with this module's component name when leaveVoice rejects", async () => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
const entries: LogEntry[] = [];
|
|
const unsub = addLogListener((e) => entries.push(e));
|
|
vi.mocked(leaveVoice).mockRejectedValueOnce(new Error("boom"));
|
|
|
|
joinVoiceChannel(7);
|
|
setVoiceStatus("connected");
|
|
clearAuth();
|
|
await flushMicrotasks();
|
|
unsub();
|
|
|
|
const warnEntry = entries.find((e) => e.level === "warn");
|
|
expect(warnEntry?.component).toBe("auth.store");
|
|
expect(warnEntry?.message).toBe("Failed to leave voice session during clearAuth");
|
|
});
|
|
});
|
|
|
|
// clearAuth's logoutWasInVoice snapshot — main.ts's isAuthenticated
|
|
// subscriber gates its voice_leave send on this instead of re-reading
|
|
// voiceStore, which clearAuth has already reset by the time any subscriber
|
|
// observes the transition (store notifications are microtask-deferred).
|
|
describe("clearAuth logoutWasInVoice snapshot", () => {
|
|
beforeEach(() => {
|
|
resetVoiceStore();
|
|
});
|
|
|
|
it("is false when not in a voice channel at logout", () => {
|
|
clearAuth();
|
|
expect(authStore.getState().logoutWasInVoice).toBe(false);
|
|
});
|
|
|
|
it("snapshots true when in a voice channel, surviving clearAuth's own voiceStore reset", () => {
|
|
joinVoiceChannel(7);
|
|
clearAuth();
|
|
|
|
expect(authStore.getState().logoutWasInVoice).toBe(true);
|
|
// The snapshot must reflect voice state as it was BEFORE this same
|
|
// call reset it — not the (already-idle) state read afterward.
|
|
expect(voiceStore.getState().currentChannelId).toBeNull();
|
|
});
|
|
});
|
|
|
|
// 6. Subscribe receives updates on setAuth/clearAuth
|
|
describe("subscribe", () => {
|
|
it("notifies on setAuth", () => {
|
|
const listener = vi.fn();
|
|
const unsub = authStore.subscribe(listener);
|
|
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
authStore.flush();
|
|
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(listener).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
token: TEST_TOKEN,
|
|
user: TEST_USER,
|
|
serverName: TEST_SERVER_NAME,
|
|
motd: TEST_MOTD,
|
|
isAuthenticated: true,
|
|
}),
|
|
);
|
|
|
|
unsub();
|
|
});
|
|
|
|
it("notifies on clearAuth", () => {
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
|
|
const listener = vi.fn();
|
|
const unsub = authStore.subscribe(listener);
|
|
|
|
clearAuth();
|
|
authStore.flush();
|
|
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(listener).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
token: null,
|
|
user: null,
|
|
serverName: null,
|
|
motd: null,
|
|
isAuthenticated: false,
|
|
}),
|
|
);
|
|
|
|
unsub();
|
|
});
|
|
|
|
it("does not notify after unsubscribe", () => {
|
|
const listener = vi.fn();
|
|
const unsub = authStore.subscribe(listener);
|
|
unsub();
|
|
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("notifies multiple subscribers independently", () => {
|
|
const listenerA = vi.fn();
|
|
const listenerB = vi.fn();
|
|
const unsubA = authStore.subscribe(listenerA);
|
|
const unsubB = authStore.subscribe(listenerB);
|
|
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
authStore.flush();
|
|
|
|
expect(listenerA).toHaveBeenCalledTimes(1);
|
|
expect(listenerB).toHaveBeenCalledTimes(1);
|
|
|
|
unsubA();
|
|
unsubB();
|
|
});
|
|
});
|
|
|
|
// Regression: clearAuth() must also drop messagesStore, or a channel id
|
|
// that also exists on the next-signed-into server (channel ids are only
|
|
// unique per-server) renders the previous session's cached messages and
|
|
// never refetches, because MessageController.loadMessages short-circuits
|
|
// on isChannelLoaded.
|
|
describe("clearAuth messages cleanup", () => {
|
|
const AUTHOR: MessageUser = { id: 1, username: "alice", avatar: "alice.png" };
|
|
|
|
function makeMessageResponse(overrides?: Partial<MessageResponse>): MessageResponse {
|
|
return {
|
|
id: 1,
|
|
channel_id: 1,
|
|
user: AUTHOR,
|
|
content: "pre-logout message",
|
|
reply_to: null,
|
|
attachments: [],
|
|
reactions: [],
|
|
pinned: false,
|
|
edited_at: null,
|
|
deleted: false,
|
|
timestamp: "2026-03-15T10:00:00Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
it("clears cached messages and the loaded flag on logout", () => {
|
|
setMessages(1, [makeMessageResponse()], false);
|
|
expect(isChannelLoaded(1)).toBe(true);
|
|
expect(getChannelMessages(1)).toHaveLength(1);
|
|
|
|
clearAuth();
|
|
|
|
expect(isChannelLoaded(1)).toBe(false);
|
|
expect(getChannelMessages(1)).toHaveLength(0);
|
|
});
|
|
|
|
it("does not leak the previous session's message content into the next", () => {
|
|
setMessages(1, [makeMessageResponse({ content: "server A secret" })], false);
|
|
clearAuth();
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
|
|
// Same numeric channel id, different server: must come back empty and
|
|
// unloaded so the caller refetches instead of rendering stale content.
|
|
expect(isChannelLoaded(1)).toBe(false);
|
|
expect(getChannelMessages(1)).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// Regression: clearAuth() must also drop channelsStore, or setChannels'
|
|
// DM-row carry (a DM channel row is deliberately preserved across a normal
|
|
// `ready` rebuild, since ready never restates DM rows) re-inserts the
|
|
// PREVIOUS server's DM channel ids into the NEXT server's channel map on
|
|
// the next login — a stale phantom channel signed into an unrelated server.
|
|
describe("clearAuth channels cleanup", () => {
|
|
const readyChannels: ReadyChannel[] = [
|
|
{ id: 1, name: "general", type: "text", category: "Text", position: 0, unread_count: 3 },
|
|
];
|
|
|
|
it("clears channels, activeChannelId, and roles on logout", () => {
|
|
setChannels(readyChannels);
|
|
expect(channelsStore.getState().channels.size).toBe(1);
|
|
|
|
clearAuth();
|
|
|
|
expect(channelsStore.getState().channels.size).toBe(0);
|
|
expect(channelsStore.getState().activeChannelId).toBeNull();
|
|
});
|
|
|
|
it("does not carry the previous server's DM channel row into the next session", () => {
|
|
setChannels(readyChannels);
|
|
// Synthesize a DM row the way addDmToChannelsStore does — setChannels'
|
|
// carry loop (channels.store.ts) re-inserts any "dm"-typed row across
|
|
// every future setChannels call unless the store is reset first.
|
|
channelsStore.setState((prev) => {
|
|
const next = new Map(prev.channels);
|
|
next.set(999, {
|
|
id: 999,
|
|
name: "alice",
|
|
type: "dm",
|
|
category: null,
|
|
topic: "",
|
|
position: 0,
|
|
unreadCount: 0,
|
|
mentionCount: 0,
|
|
lastMessageId: null,
|
|
canSend: true,
|
|
slowMode: 0,
|
|
nsfw: false,
|
|
voiceMaxUsers: 0,
|
|
voiceMaxVideo: 0,
|
|
});
|
|
return { ...prev, channels: next };
|
|
});
|
|
expect(channelsStore.getState().channels.has(999)).toBe(true);
|
|
|
|
clearAuth();
|
|
setAuth(TEST_TOKEN, TEST_USER, TEST_SERVER_NAME, TEST_MOTD);
|
|
setChannels([]); // the next server's `ready` — no DMs of its own yet
|
|
|
|
expect(channelsStore.getState().channels.has(999)).toBe(false);
|
|
});
|
|
});
|
|
});
|