mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
* fix: make OCR portable and reliable * fix: harden OCR installation portability * fix: pin OCR partials across downloads * fix: make OCR execution reliably asynchronous * fix: harden OCR portability and docs routes * fix: preserve decoder and docs safeguards
197 lines
6.6 KiB
TypeScript
197 lines
6.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const uploadConfig = vi.hoisted(() => ({
|
|
MAX_UPLOAD_SIZE_MB: 10,
|
|
MAX_BATCH_SIZE: 5,
|
|
}));
|
|
|
|
vi.mock("../../../apps/api/src/config.js", () => ({ env: uploadConfig }));
|
|
|
|
const _mockStaticRegister = vi.fn().mockResolvedValue(undefined);
|
|
vi.mock("@fastify/static", () => ({ default: "fastify-static-plugin" }));
|
|
|
|
const mockMultipartPlugin = vi.fn();
|
|
vi.mock("@fastify/multipart", () => ({ default: mockMultipartPlugin }));
|
|
|
|
const multipartPartsMock = vi.hoisted(() => vi.fn());
|
|
vi.mock("../../../apps/api/src/lib/multipart-parts.js", () => ({
|
|
multipartParts: multipartPartsMock,
|
|
}));
|
|
|
|
vi.mock("node:fs", async (importOriginal) => {
|
|
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
return {
|
|
...actual,
|
|
existsSync: vi.fn(() => true),
|
|
};
|
|
});
|
|
|
|
describe("registerStatic", () => {
|
|
let registerStatic: typeof import("../../../apps/api/src/plugins/static.js").registerStatic;
|
|
let existsSyncMock: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
const staticModule = await import("../../../apps/api/src/plugins/static.js");
|
|
registerStatic = staticModule.registerStatic;
|
|
const fsMod = await import("node:fs");
|
|
existsSyncMock = fsMod.existsSync as ReturnType<typeof vi.fn>;
|
|
});
|
|
|
|
it("registers static plugin when dist path exists", async () => {
|
|
existsSyncMock.mockReturnValue(true);
|
|
const app = {
|
|
register: vi.fn().mockResolvedValue(undefined),
|
|
setNotFoundHandler: vi.fn(),
|
|
hasReplyDecorator: vi.fn().mockReturnValue(false),
|
|
log: { warn: vi.fn() },
|
|
};
|
|
|
|
await registerStatic(app as never);
|
|
expect(app.register).toHaveBeenCalledWith("fastify-static-plugin", {
|
|
root: expect.stringContaining("web/dist"),
|
|
prefix: "/",
|
|
wildcard: false,
|
|
decorateReply: true,
|
|
});
|
|
expect(app.setNotFoundHandler).toHaveBeenCalled();
|
|
});
|
|
|
|
it("sets SPA not-found handler that returns 404 for API routes", async () => {
|
|
existsSyncMock.mockReturnValue(true);
|
|
let notFoundHandler: (request: unknown, reply: unknown) => void;
|
|
const app = {
|
|
register: vi.fn().mockResolvedValue(undefined),
|
|
setNotFoundHandler: vi.fn((handler: typeof notFoundHandler) => {
|
|
notFoundHandler = handler;
|
|
}),
|
|
hasReplyDecorator: vi.fn().mockReturnValue(false),
|
|
log: { warn: vi.fn() },
|
|
};
|
|
|
|
await registerStatic(app as never);
|
|
|
|
const reply = { code: vi.fn().mockReturnThis(), send: vi.fn(), sendFile: vi.fn() };
|
|
notFoundHandler?.({ url: "/api/v1/tools" }, reply);
|
|
expect(reply.code).toHaveBeenCalledWith(404);
|
|
expect(reply.send).toHaveBeenCalledWith({ error: "Not found", code: "NOT_FOUND" });
|
|
});
|
|
|
|
it("sets SPA not-found handler that serves index.html for non-API routes", async () => {
|
|
existsSyncMock.mockReturnValue(true);
|
|
let notFoundHandler: (request: unknown, reply: unknown) => void;
|
|
const app = {
|
|
register: vi.fn().mockResolvedValue(undefined),
|
|
setNotFoundHandler: vi.fn((handler: typeof notFoundHandler) => {
|
|
notFoundHandler = handler;
|
|
}),
|
|
hasReplyDecorator: vi.fn().mockReturnValue(false),
|
|
log: { warn: vi.fn() },
|
|
};
|
|
|
|
await registerStatic(app as never);
|
|
|
|
const reply = { code: vi.fn().mockReturnThis(), send: vi.fn(), sendFile: vi.fn() };
|
|
notFoundHandler?.({ url: "/resize" }, reply);
|
|
expect(reply.sendFile).toHaveBeenCalledWith("index.html");
|
|
});
|
|
|
|
it("logs warning and skips registration when dist path does not exist", async () => {
|
|
existsSyncMock.mockReturnValue(false);
|
|
const app = {
|
|
register: vi.fn().mockResolvedValue(undefined),
|
|
setNotFoundHandler: vi.fn(),
|
|
log: { warn: vi.fn() },
|
|
};
|
|
|
|
await registerStatic(app as never);
|
|
expect(app.log.warn).toHaveBeenCalledWith(expect.stringContaining("SPA dist not found"));
|
|
expect(app.register).not.toHaveBeenCalled();
|
|
expect(app.setNotFoundHandler).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("registerUpload", () => {
|
|
let registerUpload: typeof import("../../../apps/api/src/plugins/upload.js").registerUpload;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
const uploadModule = await import("../../../apps/api/src/plugins/upload.js");
|
|
registerUpload = uploadModule.registerUpload;
|
|
});
|
|
|
|
it("registers multipart with correct file size limit", async () => {
|
|
uploadConfig.MAX_UPLOAD_SIZE_MB = 10;
|
|
uploadConfig.MAX_BATCH_SIZE = 5;
|
|
const app = { register: vi.fn().mockResolvedValue(undefined), addHook: vi.fn() };
|
|
|
|
await registerUpload(app as never);
|
|
expect(app.register).toHaveBeenCalledWith(mockMultipartPlugin, {
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024,
|
|
files: 5,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("passes undefined for fileSize when MAX_UPLOAD_SIZE_MB is 0", async () => {
|
|
uploadConfig.MAX_UPLOAD_SIZE_MB = 0;
|
|
uploadConfig.MAX_BATCH_SIZE = 5;
|
|
const app = { register: vi.fn().mockResolvedValue(undefined), addHook: vi.fn() };
|
|
|
|
await registerUpload(app as never);
|
|
expect(app.register).toHaveBeenCalledWith(mockMultipartPlugin, {
|
|
limits: {
|
|
fileSize: undefined,
|
|
files: 5,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("passes undefined for files when MAX_BATCH_SIZE is 0", async () => {
|
|
uploadConfig.MAX_UPLOAD_SIZE_MB = 10;
|
|
uploadConfig.MAX_BATCH_SIZE = 0;
|
|
const app = { register: vi.fn().mockResolvedValue(undefined), addHook: vi.fn() };
|
|
|
|
await registerUpload(app as never);
|
|
expect(app.register).toHaveBeenCalledWith(mockMultipartPlugin, {
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024,
|
|
files: undefined,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("passes undefined for both limits when both are 0", async () => {
|
|
uploadConfig.MAX_UPLOAD_SIZE_MB = 0;
|
|
uploadConfig.MAX_BATCH_SIZE = 0;
|
|
const app = { register: vi.fn().mockResolvedValue(undefined), addHook: vi.fn() };
|
|
|
|
await registerUpload(app as never);
|
|
expect(app.register).toHaveBeenCalledWith(mockMultipartPlugin, {
|
|
limits: {
|
|
fileSize: undefined,
|
|
files: undefined,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("forwards route-specific limits through the request.parts replacement", async () => {
|
|
let hook: ((request: unknown) => Promise<void>) | undefined;
|
|
const app = {
|
|
register: vi.fn().mockResolvedValue(undefined),
|
|
addHook: vi.fn((_name, handler) => {
|
|
hook = handler;
|
|
}),
|
|
};
|
|
await registerUpload(app as never);
|
|
const iterator = {};
|
|
multipartPartsMock.mockReturnValueOnce(iterator);
|
|
const request = { isMultipart: () => true, parts: vi.fn() };
|
|
|
|
await hook?.(request);
|
|
expect(request.parts({ limits: { fileSize: 1234, files: 2 } })).toBe(iterator);
|
|
expect(multipartPartsMock).toHaveBeenCalledWith(request, { fileSize: 1234, files: 2 });
|
|
});
|
|
});
|