mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* fix(voice): 4 defect(s) (OC-0008, OC-0009, OC-0042, OC-0080) Guard LiveKit session state against supersession: bump the camera/screen generation in leaveVoice and teardownForReconnect so an in-flight enable discards its track, bail out of restoreLocalVoiceState when a newer room claimed _room mid-await, and recheck isStateConnected in the auto-reconnect tail. * fix(ws): 1 defect(s) (OC-0019) * fix(db): 1 defect(s) (OC-0023) * fix(ws): 1 defect(s) (OC-0029) * fix(ws): 1 defect(s) (OC-0032) * fix(voice): 1 defect(s) (OC-0034) * fix(admin): 1 defect(s) (OC-0035) * fix(service): 2 defect(s) (OC-0036, OC-0128) * fix(voice): 2 defect(s) (OC-0038, OC-0065) OC-0038: the LiveKit participant_left webhook cleared the leaver's own client voice state before broadcasting voice_leave, so the broadcast audience (READ_MESSAGES holders union still-in-the-room participants) could no longer see them. Voice membership is gated on CONNECT_VOICE alone, so a participant without READ_MESSAGES never learned the server had torn down their call. Extracted finishVoiceLeave's audience logic into broadcastVoiceEventWithLeaver and used it on the webhook path. OC-0065: handleWebhookParticipantJoined OR'd a GetVoiceState read error into the same branch as "no matching row", so a transient DB failure ejected a legitimate participant from the SFU mid-call. Now the read error is logged and the check skipped, matching sweepStaleVoiceStates. * fix(client): 1 defect(s) (OC-0041) * fix(client): 1 defect(s) (OC-0043) * fix(client): 1 defect(s) (OC-0046) * fix(client): 1 defect(s) (OC-0047) * fix(client): 1 defect(s) (OC-0049) * fix(client): 1 defect(s) (OC-0108) * fix(client): 2 defect(s) (OC-0111, OC-0143) OC-0111: retry a presence_update dropped by the 1-per-10s limiter once the window reopens, so auto-idle's return-to-online does not leave the server and every other client stuck on idle. OC-0143: pass apiConfig.host to the DM profile sidebar so per-user notes are scoped per server, matching channel mutes, the NSFW gate and volume. * test(ws): align aborted-switch test with OC-0034 no-resurrect behavior The fix agent rewrote this pre-existing test (it locked the buggy restore path) but the prove agent left it out of c67d25ed; committed state alone failed go test ./ws/ without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { fetchMock, getTokenMock, ensureHttpProxyMock } = vi.hoisted(() => ({
|
|
fetchMock: vi.fn<any>(),
|
|
getTokenMock: vi.fn<any>(),
|
|
ensureHttpProxyMock: vi.fn<any>(),
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/plugin-http", () => ({
|
|
fetch: fetchMock,
|
|
}));
|
|
|
|
vi.mock("@lib/httpProxy", () => ({
|
|
ensureHttpProxy: ensureHttpProxyMock,
|
|
}));
|
|
|
|
vi.mock("@stores/auth.store", () => ({
|
|
getToken: getTokenMock,
|
|
}));
|
|
|
|
vi.mock("@lib/logger", () => ({
|
|
createLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }),
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/plugin-dialog", () => ({ save: vi.fn() }));
|
|
vi.mock("@tauri-apps/plugin-fs", () => ({ writeFile: vi.fn() }));
|
|
vi.mock("@lib/icons", () => ({ createIcon: () => document.createElement("span") }));
|
|
vi.mock("@lib/media-visibility", () => ({ observeMedia: vi.fn() }));
|
|
vi.mock("../../src/components/message-list/media", () => ({ openImageLightbox: vi.fn() }));
|
|
|
|
// No-op IndexedDB so fetchImageAsDataUrl falls through to the network path.
|
|
vi.stubGlobal("indexedDB", {
|
|
open: () => {
|
|
const req: Record<string, unknown> = { onsuccess: null, onerror: null, onupgradeneeded: null };
|
|
Promise.resolve().then(() => {
|
|
const fn = req.onerror as ((ev: Event) => void) | null;
|
|
fn?.(new Event("error"));
|
|
});
|
|
return req;
|
|
},
|
|
});
|
|
|
|
import {
|
|
clearAttachmentCaches,
|
|
fetchImageAsDataUrl,
|
|
isTrustedServerUrl,
|
|
setServerHost,
|
|
} from "../../src/components/message-list/attachments";
|
|
|
|
function imageResponse() {
|
|
return {
|
|
ok: true,
|
|
headers: { get: () => "image/png" },
|
|
arrayBuffer: vi.fn().mockResolvedValue(Uint8Array.from([1, 2, 3]).buffer),
|
|
};
|
|
}
|
|
|
|
describe("attachment fetch authentication", () => {
|
|
beforeEach(() => {
|
|
fetchMock.mockReset();
|
|
getTokenMock.mockReset();
|
|
ensureHttpProxyMock.mockReset();
|
|
clearAttachmentCaches();
|
|
setServerHost("chat.example.com");
|
|
});
|
|
|
|
it("sends the bearer token through the TOFU proxy for server-hosted files", async () => {
|
|
getTokenMock.mockReturnValue("session-token");
|
|
ensureHttpProxyMock.mockResolvedValue("http://127.0.0.1:49812");
|
|
fetchMock.mockResolvedValue(imageResponse());
|
|
|
|
const result = await fetchImageAsDataUrl("https://chat.example.com/api/v1/files/abc-123");
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(ensureHttpProxyMock).toHaveBeenCalledWith("chat.example.com");
|
|
expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:49812/api/v1/files/abc-123", {
|
|
headers: { Authorization: "Bearer session-token" },
|
|
});
|
|
});
|
|
|
|
it("omits the Authorization header when no session token exists", async () => {
|
|
getTokenMock.mockReturnValue(null);
|
|
ensureHttpProxyMock.mockResolvedValue("http://127.0.0.1:49812");
|
|
fetchMock.mockResolvedValue(imageResponse());
|
|
|
|
await fetchImageAsDataUrl("https://chat.example.com/api/v1/files/abc-456");
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:49812/api/v1/files/abc-456", {
|
|
headers: {},
|
|
});
|
|
});
|
|
|
|
it("never sends the token to external hosts", async () => {
|
|
getTokenMock.mockReturnValue("session-token");
|
|
fetchMock.mockResolvedValue(imageResponse());
|
|
|
|
await fetchImageAsDataUrl("https://cdn.external.example/image.png");
|
|
|
|
expect(ensureHttpProxyMock).not.toHaveBeenCalled();
|
|
expect(fetchMock).toHaveBeenCalledWith("https://cdn.external.example/image.png");
|
|
});
|
|
|
|
it("still routes through the TOFU proxy with a bearer token when the host is stored with an explicit :443", async () => {
|
|
setServerHost("chat.example.com:443");
|
|
getTokenMock.mockReturnValue("session-token");
|
|
ensureHttpProxyMock.mockResolvedValue("http://127.0.0.1:49812");
|
|
fetchMock.mockResolvedValue(imageResponse());
|
|
|
|
const result = await fetchImageAsDataUrl("https://chat.example.com/api/v1/files/abc-789");
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(ensureHttpProxyMock).toHaveBeenCalledWith("chat.example.com");
|
|
expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:49812/api/v1/files/abc-789", {
|
|
headers: { Authorization: "Bearer session-token" },
|
|
});
|
|
});
|
|
|
|
it("treats a :443-suffixed stored host as trusted for the port-less resolved URL", () => {
|
|
setServerHost("chat.example.com:443");
|
|
expect(isTrustedServerUrl("https://chat.example.com/api/v1/files/abc")).toBe(true);
|
|
});
|
|
});
|