Files
lindyson-ai/apps/api/tests/localStorageRoute.test.ts
Juno ChowandClaude Fable 5.1 f665082dad chore: 建立产品仓库,完成开源前的敏感信息清理
前端(Vite + React)、API/worker(Fastify + BullMQ)、api-contracts 契约包、
迁移脚本、测试与自托管 docker-compose 示例。

开源准备:运维脚本与机器信息已拆到私有仓库;公司名、备案号、隐私邮箱改读
publicCompliance.local.json(仓库只保留占位模板);站点地址与客服二维码改为
VITE_ 环境变量;fixtures 中的真实用户标识已匿名化;三个 package.json 标注
Apache-2.0。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-16 02:10:46 +08:00

121 lines
4.7 KiB
TypeScript

import { mkdtemp, readFile, rm } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import fastify from 'fastify';
import { afterEach, describe, expect, it, vi } from 'vitest';
const temporaryDirectories: string[] = [];
afterEach(async () => {
vi.unstubAllEnvs();
vi.resetModules();
await Promise.all(temporaryDirectories.splice(0).map(directory => rm(directory, { recursive: true, force: true })));
});
describe('localStorageRoute', () => {
it('真实 image/png PUT 通过签名验证后流式落盘并可用签名 GET 读取', async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), 'lindyson-storage-'));
temporaryDirectories.push(directory);
vi.stubEnv('STORAGE_DRIVER', 'local');
vi.stubEnv('LOCAL_STORAGE_DIR', directory);
vi.stubEnv('PUBLIC_BASE_URL', 'http://localhost:4300');
vi.stubEnv('JWT_SECRET', 'test-local-storage-signing-secret');
const [{ registerLocalStorageRoutes }, { createObjectDownloadUrl, createObjectReadUrl, createUploadSignature }] = await Promise.all([
import('../src/storage/localStorageRoute.js'),
import('../src/storage/storageService.js'),
]);
const app = fastify({ logger: false });
await app.register(registerLocalStorageRoutes);
const signature = await createUploadSignature({
objectKey: 'users/00000000-0000-4000-8000-000000000001/upload-staging/session-1/image.png',
contentType: 'image/png',
maxSizeBytes: 1024,
});
const uploadUrl = new URL(signature.uploadUrl);
const body = Buffer.from('\x89PNG\r\n\x1a\nreal-image-body', 'binary');
try {
const upload = await app.inject({
method: 'PUT',
url: `${uploadUrl.pathname}${uploadUrl.search}`,
headers: { 'content-type': 'image/png' },
payload: body,
});
expect(upload.statusCode).toBe(204);
await expect(readFile(path.join(directory, signature.objectKey))).resolves.toEqual(body);
const repeated = await app.inject({
method: 'PUT',
url: `${uploadUrl.pathname}${uploadUrl.search}`,
headers: { 'content-type': 'image/png' },
payload: Buffer.from('replacement'),
});
expect(repeated.statusCode).toBe(409);
await expect(readFile(path.join(directory, signature.objectKey))).resolves.toEqual(body);
const readUrl = new URL(await createObjectReadUrl(signature.objectKey));
const download = await app.inject({
method: 'GET',
url: `${readUrl.pathname}${readUrl.search}`,
});
expect(download.statusCode).toBe(200);
expect(download.headers['content-type']).toContain('image/png');
expect(download.headers['cache-control']).toBe('private, no-store');
expect(download.rawPayload).toEqual(body);
const attachmentUrl = new URL(await createObjectDownloadUrl(signature.objectKey, '测试图片.png'));
const attachment = await app.inject({
method: 'GET',
url: `${attachmentUrl.pathname}${attachmentUrl.search}`,
});
expect(attachment.statusCode).toBe(200);
expect(attachment.headers['content-disposition']).toContain("attachment; filename*=UTF-8''");
} finally {
await app.close();
}
});
it('拒绝篡改 MIME 或超过签名上限的 PUT', async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), 'lindyson-storage-'));
temporaryDirectories.push(directory);
vi.stubEnv('STORAGE_DRIVER', 'local');
vi.stubEnv('LOCAL_STORAGE_DIR', directory);
vi.stubEnv('PUBLIC_BASE_URL', 'http://localhost:4300');
vi.stubEnv('JWT_SECRET', 'test-local-storage-signing-secret');
const [{ registerLocalStorageRoutes }, { createUploadSignature }] = await Promise.all([
import('../src/storage/localStorageRoute.js'),
import('../src/storage/storageService.js'),
]);
const app = fastify({ logger: false });
await app.register(registerLocalStorageRoutes);
const signature = await createUploadSignature({
objectKey: 'users/00000000-0000-4000-8000-000000000001/upload-staging/session-2/small.png',
contentType: 'image/png',
maxSizeBytes: 4,
});
const uploadUrl = new URL(signature.uploadUrl);
try {
const wrongMime = await app.inject({
method: 'PUT',
url: `${uploadUrl.pathname}${uploadUrl.search}`,
headers: { 'content-type': 'image/jpeg' },
payload: Buffer.from('1234'),
});
expect(wrongMime.statusCode).toBe(403);
const tooLarge = await app.inject({
method: 'PUT',
url: `${uploadUrl.pathname}${uploadUrl.search}`,
headers: { 'content-type': 'image/png' },
payload: Buffer.from('12345'),
});
expect(tooLarge.statusCode).toBe(413);
} finally {
await app.close();
}
});
});