80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { processFile } from "./file-attachment";
|
|
|
|
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
|
|
// Minimal FileReader stub — jsdom's version is fine, but we stub for deterministic output.
|
|
class FakeFileReader {
|
|
public result: string | null = null;
|
|
public onload: ((event: { target: { result: string } }) => void) | null = null;
|
|
public onerror: (() => void) | null = null;
|
|
readAsDataURL(file: File) {
|
|
// Small synthetic data URL — content doesn't matter for the id assertion.
|
|
const payload = "AAAA"; // 3 bytes base64
|
|
this.result = `data:${file.type || "application/octet-stream"};base64,${payload}`;
|
|
queueMicrotask(() => this.onload?.({ target: { result: this.result as string } }));
|
|
}
|
|
}
|
|
|
|
// Image stub — fires onload synchronously so the image branch resolves.
|
|
class FakeImage {
|
|
public onload: (() => void) | null = null;
|
|
public onerror: (() => void) | null = null;
|
|
set src(_: string) {
|
|
queueMicrotask(() => this.onload?.());
|
|
}
|
|
}
|
|
|
|
describe("processFile in insecure context (HTTP, no crypto.randomUUID)", () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("assigns a fallback UUID to non-image attachments when crypto.randomUUID is unavailable", async () => {
|
|
vi.stubGlobal("crypto", {}); // simulate non-secure context
|
|
vi.stubGlobal("FileReader", FakeFileReader);
|
|
|
|
const file = new File(["hello"], "notes.txt", { type: "text/plain" });
|
|
Object.defineProperty(file, "size", { value: 5 });
|
|
|
|
const attachment = await processFile(file);
|
|
expect(attachment).not.toBeNull();
|
|
expect(attachment!.id).toMatch(UUID_V4_REGEX);
|
|
expect(attachment!.isImage).toBe(false);
|
|
expect(attachment!.fileName).toBe("notes.txt");
|
|
});
|
|
|
|
it("assigns a fallback UUID to image attachments when crypto.randomUUID is unavailable", async () => {
|
|
vi.stubGlobal("crypto", {});
|
|
vi.stubGlobal("FileReader", FakeFileReader);
|
|
vi.stubGlobal("Image", FakeImage);
|
|
|
|
const file = new File(["img"], "shot.png", { type: "image/png" });
|
|
Object.defineProperty(file, "size", { value: 3 });
|
|
|
|
const attachment = await processFile(file);
|
|
expect(attachment).not.toBeNull();
|
|
expect(attachment!.id).toMatch(UUID_V4_REGEX);
|
|
expect(attachment!.isImage).toBe(true);
|
|
expect(attachment!.preview).toMatch(/^data:image\/png;base64,/);
|
|
});
|
|
|
|
it("treats non-previewable image MIME types as regular file attachments", async () => {
|
|
const ImageSpy = vi.fn();
|
|
vi.stubGlobal("crypto", {});
|
|
vi.stubGlobal("FileReader", FakeFileReader);
|
|
vi.stubGlobal("Image", ImageSpy);
|
|
|
|
const file = new File(["svg"], "icon.svg", { type: "image/svg+xml" });
|
|
Object.defineProperty(file, "size", { value: 3 });
|
|
|
|
const attachment = await processFile(file);
|
|
expect(attachment).not.toBeNull();
|
|
expect(attachment!.id).toMatch(UUID_V4_REGEX);
|
|
expect(attachment!.isImage).toBe(false);
|
|
expect(attachment!.mimeType).toBe("image/svg+xml");
|
|
expect(attachment!.preview).toBeUndefined();
|
|
expect(ImageSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|