Files
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

477 lines
15 KiB
TypeScript

import express, { type ErrorRequestHandler } from 'express';
import http, { type Server } from 'http';
import { afterEach, describe, expect, test, vi } from 'vitest';
const storageMocks = vi.hoisted(() => ({
isBucketPublic: vi.fn(),
getObjectMetadataVisible: vi.fn(),
getDownloadStrategy: vi.fn(),
getObjectMetadataRow: vi.fn(),
getObject: vi.fn(),
objectIsVisible: vi.fn(),
deleteObjects: vi.fn(),
}));
const authMocks = vi.hoisted(() => ({
verifyAdmin: vi.fn((_req, _res, next) => next()),
verifyUser: vi.fn((_req, _res, next) => next()),
}));
const requestMethod = (
method: 'DELETE' | 'GET' | 'POST',
port: number,
path: string,
body?: string
): Promise<{ statusCode: number; body: string }> =>
new Promise((resolve, reject) => {
const headers = body
? {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
}
: undefined;
const request = http.request(
{ hostname: '127.0.0.1', port, path, method, headers },
(response) => {
let responseBody = '';
response.setEncoding('utf8');
response.on('data', (chunk) => {
responseBody += chunk;
});
response.on('end', () =>
resolve({ statusCode: response.statusCode ?? 0, body: responseBody })
);
}
);
request.on('error', reject);
if (body) {
request.write(body);
}
request.end();
});
const post = (port: number, path: string) => requestMethod('POST', port, path);
const get = (port: number, path: string) => requestMethod('GET', port, path);
const deleteJson = (port: number, path: string, body: string) =>
requestMethod('DELETE', port, path, body);
const routeErrorHandler: ErrorRequestHandler = (error, _req, res, next) => {
void next;
const statusCode =
error instanceof Error && 'statusCode' in error && typeof error.statusCode === 'number'
? error.statusCode
: 500;
const message = error instanceof Error ? error.message : 'Internal server error';
res.status(statusCode).json({ message });
};
vi.mock('../../src/services/storage/storage.service.js', () => ({
StorageService: {
getInstance: () => storageMocks,
},
}));
vi.mock('../../src/services/storage/storage-config.service.js', () => ({
StorageConfigService: {
getInstance: () => ({}),
},
}));
vi.mock('../../src/services/logs/audit.service.js', () => ({
AuditService: {
getInstance: () => ({
log: vi.fn(),
}),
},
}));
vi.mock('../../src/infra/socket/socket.manager.js', () => ({
SocketManager: {
getInstance: () => ({
broadcastToRoom: vi.fn(),
}),
},
}));
vi.mock('../../src/api/middlewares/auth.js', () => ({
verifyAdmin: authMocks.verifyAdmin,
verifyUser: authMocks.verifyUser,
}));
describe('Storage routes', () => {
let server: Server | undefined;
afterEach(() => {
server?.close();
server = undefined;
vi.clearAllMocks();
});
test('batch delete route validates body and delegates to storage service', async () => {
vi.resetModules();
storageMocks.deleteObjects.mockResolvedValue({
results: [
{ key: 'a.txt', status: 'deleted' },
{ key: 'missing.txt', status: 'notFound' },
],
});
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const response = await deleteJson(
address.port,
'/api/storage/buckets/photos/objects',
JSON.stringify({ keys: ['a.txt', 'missing.txt'] })
);
expect(response.statusCode, response.body).toBe(200);
expect(JSON.parse(response.body)).toEqual({
results: [
{ key: 'a.txt', status: 'deleted' },
{ key: 'missing.txt', status: 'notFound' },
],
});
expect(storageMocks.deleteObjects).toHaveBeenCalledWith(
undefined,
'photos',
['a.txt', 'missing.txt'],
false
);
});
test('batch delete route returns 400 for an empty key list', async () => {
vi.resetModules();
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const response = await deleteJson(
address.port,
'/api/storage/buckets/photos/objects',
JSON.stringify({ keys: [] })
);
expect(response.statusCode, response.body).toBe(400);
expect(storageMocks.deleteObjects).not.toHaveBeenCalled();
});
test('download strategy route captures nested object keys', async () => {
vi.resetModules();
storageMocks.isBucketPublic.mockResolvedValue(true);
storageMocks.getObjectMetadataVisible.mockResolvedValue(true);
storageMocks.getDownloadStrategy.mockResolvedValue({
method: 'direct',
url: 'http://localhost:7130/api/storage/buckets/product-images/objects/products%2Fprod_123%2Fmain.jpg',
});
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const response = await post(
address.port,
'/api/storage/buckets/product-images/objects/products/prod_123/main.jpg/download-strategy'
);
expect(response.statusCode, response.body).toBe(200);
expect(storageMocks.getDownloadStrategy).toHaveBeenCalledWith(
'product-images',
'products/prod_123/main.jpg',
undefined,
{ asAttachment: false, prefetchedMetadata: true }
);
expect(authMocks.verifyUser).not.toHaveBeenCalled();
});
test('download strategy route returns 400 when object key is missing', async () => {
vi.resetModules();
storageMocks.isBucketPublic.mockResolvedValue(true);
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const response = await post(
address.port,
'/api/storage/buckets/product-images/objects//download-strategy'
);
expect(response.statusCode, response.body).toBe(400);
expect(storageMocks.getDownloadStrategy).not.toHaveBeenCalled();
});
test('download strategy route requires auth middleware for private buckets', async () => {
vi.resetModules();
storageMocks.isBucketPublic.mockResolvedValue(false);
storageMocks.getObjectMetadataVisible.mockResolvedValue(true);
storageMocks.getDownloadStrategy.mockResolvedValue({
method: 'direct',
url: 'http://localhost:7130/api/storage/buckets/product-images/objects/products%2Fprod_123%2Fmain.jpg',
});
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const response = await post(
address.port,
'/api/storage/buckets/product-images/objects/products/prod_123/main.jpg/download-strategy'
);
expect(response.statusCode, response.body).toBe(200);
expect(authMocks.verifyUser).toHaveBeenCalledOnce();
expect(storageMocks.getDownloadStrategy).toHaveBeenCalledWith(
'product-images',
'products/prod_123/main.jpg',
undefined,
{ asAttachment: false, prefetchedMetadata: true }
);
});
test('download strategy route forwards a caller-supplied expiresIn', async () => {
vi.resetModules();
storageMocks.isBucketPublic.mockResolvedValue(false);
storageMocks.getObjectMetadataVisible.mockResolvedValue(true);
storageMocks.getDownloadStrategy.mockResolvedValue({
method: 'presigned',
url: 'https://cdn.example.com/product-images/products%2Fprod_123%2Fmain.jpg?Signature=abc',
expiresAt: new Date('2026-01-01T00:00:00.000Z'),
});
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const response = await post(
address.port,
'/api/storage/buckets/product-images/objects/products/prod_123/main.jpg/download-strategy?expiresIn=120'
);
expect(response.statusCode, response.body).toBe(200);
expect(storageMocks.getDownloadStrategy).toHaveBeenCalledWith(
'product-images',
'products/prod_123/main.jpg',
120,
{ asAttachment: false, prefetchedMetadata: true }
);
});
test('canonical GET download-strategy route forwards expiresIn', async () => {
vi.resetModules();
storageMocks.isBucketPublic.mockResolvedValue(false);
storageMocks.getObjectMetadataVisible.mockResolvedValue(true);
storageMocks.getDownloadStrategy.mockResolvedValue({
method: 'presigned',
url: 'https://cdn.example.com/product-images/products%2Fprod_123%2Fmain.jpg?Signature=abc',
expiresAt: new Date('2026-01-01T00:00:00.000Z'),
});
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const response = await get(
address.port,
'/api/storage/buckets/product-images/download-strategy/objects/products/prod_123/main.jpg?expiresIn=120'
);
expect(response.statusCode, response.body).toBe(200);
expect(storageMocks.getDownloadStrategy).toHaveBeenCalledWith(
'product-images',
'products/prod_123/main.jpg',
120,
{ asAttachment: false, prefetchedMetadata: true }
);
});
test('download strategy route rejects a non-numeric expiresIn with 400', async () => {
vi.resetModules();
storageMocks.isBucketPublic.mockResolvedValue(false);
storageMocks.getObjectMetadataVisible.mockResolvedValue(true);
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const response = await get(
address.port,
'/api/storage/buckets/product-images/download-strategy/objects/products/prod_123/main.jpg?expiresIn=abc'
);
expect(response.statusCode, response.body).toBe(400);
expect(storageMocks.getDownloadStrategy).not.toHaveBeenCalled();
});
test('direct download route applies read-time defense for unsafe MIME types', async () => {
vi.resetModules();
storageMocks.getObjectMetadataVisible.mockResolvedValue({
mime_type: 'text/html',
bucket: 'test-bucket',
key: 'test.html',
});
storageMocks.getDownloadStrategy.mockResolvedValue({
method: 'direct',
url: 'http://localhost:7130/api/storage/buckets/product-images/objects/products/prod_123/main.html',
});
storageMocks.getObject.mockResolvedValue({
file: Buffer.from('<html><script>alert(1)</script></html>'),
metadata: { mimeType: 'text/html' },
});
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const request = http.request({
hostname: '127.0.0.1',
port: address.port,
path: '/api/storage/buckets/product-images/objects/products/prod_123/main.html',
method: 'GET',
});
const response = await new Promise<http.IncomingMessage>((resolve, reject) => {
request.on('response', resolve);
request.on('error', reject);
request.end();
});
expect(response.statusCode).toBe(200);
expect(response.headers['x-content-type-options']).toBe('nosniff');
expect(response.headers['content-disposition']).toBe('attachment');
});
test('download strategy route returns 404 for missing or invisible objects', async () => {
vi.resetModules();
storageMocks.isBucketPublic.mockResolvedValue(true);
storageMocks.getObjectMetadataVisible.mockResolvedValue(null);
const { storageRouter } = await import('../../src/api/routes/storage/index.routes.js');
const app = express();
app.use(express.json());
app.use('/api/storage', storageRouter);
app.use(routeErrorHandler);
await new Promise<void>((resolve) => {
server = app.listen(0, resolve);
});
const address = server?.address();
if (!address || typeof address === 'string') {
throw new Error('Test server did not bind to a TCP port');
}
const response = await post(
address.port,
'/api/storage/buckets/product-images/objects/products/prod_123/missing.jpg/download-strategy'
);
expect(response.statusCode, response.body).toBe(404);
expect(storageMocks.getDownloadStrategy).not.toHaveBeenCalled();
});
});