상위 디렉터리 및 카테고리의 소유권을 검증하지 않던 문제 등 버그 수정 및 Vitest 기반 테스트 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
staticandClaude Opus 5.5 committed 2026-10-11 23:45:56 +09:00
1 parent 67989f4741
commit ed732be999
31 files changed
+2057 -42

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
import { describe, expect, it } from "vitest";
import { BoundedQueue, Scheduler } from "#lib/utils/concurrency/index.js";
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
describe("BoundedQueue", () => {
it("blocks producers while full", async () => {
const queue = new BoundedQueue<number>(2);
await queue.push(1);
await queue.push(2);
let isPushed = false;
const pushing = queue.push(3).then(() => (isPushed = true));
await tick();
expect(isPushed).toBe(false);
expect(await queue.pop()).toBe(1);
await pushing;
expect(isPushed).toBe(true);
});
it("drains remaining items and then returns null after closing", async () => {
const queue = new BoundedQueue<number>(2);
await queue.push(1);
queue.close();
expect(await queue.pop()).toBe(1);
expect(await queue.pop()).toBeNull();
await expect(queue.push(2)).rejects.toThrow("Queue closed");
});
it("wakes up waiting consumers when closed", async () => {
const queue = new BoundedQueue<number>(1);
const popping = queue.pop();
queue.close();
expect(await popping).toBeNull();
});
});
describe("Scheduler", () => {
it("defers tasks while the memory limit is exceeded", async () => {
const scheduler = new Scheduler<void>(100);
const events: string[] = [];
let finishFirst!: () => void;
const first = scheduler.schedule(100, async () => {
events.push("first:start");
await new Promise<void>((resolve) => (finishFirst = resolve));
events.push("first:end");
});
const second = scheduler.schedule(10, async () => {
events.push("second:start");
});
await tick();
expect(events).toEqual(["first:start"]);
finishFirst();
await Promise.all([first, second]);
expect(events).toEqual(["first:start", "first:end", "second:start"]);
});
it("runs small tasks concurrently", async () => {
const scheduler = new Scheduler<number>(100);
let running = 0;
let maxRunning = 0;
await Promise.all(
[1, 2, 3].map((value) =>
scheduler.schedule(10, async () => {
maxRunning = Math.max(maxRunning, ++running);
await tick();
running--;
return value;
}),
),
);
expect(maxRunning).toBe(3);
});
it("releases memory when a task fails", async () => {
const scheduler = new Scheduler<void>(100);
await expect(
scheduler.schedule(100, async () => {
throw new Error("Failed");
}),
).rejects.toThrow("Failed");
await expect(scheduler.schedule(100, async () => {})).resolves.toBeUndefined();
});
});
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import {
decodeFromBase64,
decryptChunk,
decryptString,
encodeToBase64,
encryptChunk,
encryptString,
generateDataKey,
} from "#lib/modules/crypto/index.js";
describe("base64", () => {
it("round-trips large buffers", () => {
const data = Uint8Array.from({ length: 1024 * 1024 }, (_, i) => (i * 31) % 256);
const encoded = encodeToBase64(data);
expect(new Uint8Array(decodeFromBase64(encoded))).toEqual(data);
expect(encodeToBase64(data.buffer)).toBe(encoded);
});
});
describe("AES-GCM", () => {
it("round-trips strings", async () => {
const { dataKey } = await generateDataKey();
const { ciphertext, iv } = await encryptString("안녕하세요.jpg", dataKey);
expect(await decryptString(ciphertext, iv, dataKey)).toBe("안녕하세요.jpg");
});
it("round-trips chunks", async () => {
const { dataKey } = await generateDataKey();
const chunk = crypto.getRandomValues(new Uint8Array(4096));
const encrypted = await encryptChunk(chunk.buffer, dataKey);
expect(encrypted.byteLength).toBe(4096 + 12 + 16);
expect(new Uint8Array(await decryptChunk(encrypted, dataKey))).toEqual(chunk);
});
it("rejects tampered chunks", async () => {
const { dataKey } = await generateDataKey();
const encrypted = new Uint8Array(await encryptChunk(new Uint8Array(32).buffer, dataKey));
encrypted[20]! ^= 1;
await expect(decryptChunk(encrypted.buffer, dataKey)).rejects.toThrow();
});
});
+68
View File
@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import { CHUNK_SIZE, ENCRYPTED_CHUNK_SIZE, ENCRYPTION_OVERHEAD } from "#lib/constants/index.js";
import { getContentRangeHeader, parseRangeHeader } from "#lib/modules/http.js";
import { getDecryptedSize, getEncryptedRange } from "../../src/service-worker/modules/crypto";
describe("parseRangeHeader", () => {
it("parses byte ranges", () => {
expect(parseRangeHeader(null)).toBeUndefined();
expect(parseRangeHeader("bytes=0-")).toEqual({ start: 0, end: undefined });
expect(parseRangeHeader("bytes=10-20")).toEqual({ start: 10, end: 20 });
expect(parseRangeHeader("bytes=10-20, 30-40")).toEqual({ start: 10, end: 20 });
});
it("formats Content-Range", () => {
expect(getContentRangeHeader({ start: 1, end: 2, total: 3 })).toEqual({
"Content-Range": "bytes 1-2/3",
});
});
});
describe("encrypted chunk layout", () => {
const encryptedSizeOf = (size: number) =>
size + Math.ceil(size / CHUNK_SIZE) * ENCRYPTION_OVERHEAD;
it.each([1, CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE + 1, 3 * CHUNK_SIZE + 12345])(
"computes the decrypted size of %i bytes",
(size) => {
expect(getDecryptedSize(encryptedSizeOf(size), false)).toBe(size);
},
);
it("computes the decrypted size of legacy files", () => {
expect(getDecryptedSize(100 + ENCRYPTION_OVERHEAD, true)).toBe(100);
});
it("maps a plaintext range to whole encrypted chunks", () => {
const totalSize = 3 * CHUNK_SIZE + 100;
const totalEncryptedSize = encryptedSizeOf(totalSize);
expect(getEncryptedRange(0, 10, totalEncryptedSize, false)).toEqual({
firstChunkIndex: 0,
lastChunkIndex: 0,
start: 0,
end: ENCRYPTED_CHUNK_SIZE - 1,
});
expect(getEncryptedRange(CHUNK_SIZE - 1, CHUNK_SIZE, totalEncryptedSize, false)).toEqual({
firstChunkIndex: 0,
lastChunkIndex: 1,
start: 0,
end: 2 * ENCRYPTED_CHUNK_SIZE - 1,
});
expect(getEncryptedRange(totalSize - 1, totalSize - 1, totalEncryptedSize, false)).toEqual({
firstChunkIndex: 3,
lastChunkIndex: 3,
start: 3 * ENCRYPTED_CHUNK_SIZE,
end: totalEncryptedSize - 1,
});
});
it("maps any range of legacy files to the whole file", () => {
expect(getEncryptedRange(5, 10, 1000, true)).toEqual({
firstChunkIndex: 0,
lastChunkIndex: 0,
start: 0,
end: 999,
});
});
});