Files
OwnCord/Client/tauri-client/tests/unit/attachments-auth.test.ts
T
J3vbandClaude Fable 5 7be9ccd2f9 fix: batch of 22 correctness fixes across server and client (#1371)
* 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>
2026-08-14 16:15:32 +02:00

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);
});
});