fix: remove UploadBatcher crash window, make bot concurrency configurable, fix all test import paths
Deploy FileDrop / deploy (push) Successful in 48s
Deploy FileDrop / deploy (push) Successful in 48s
- Removed UploadBatcher (src/infrastructure/telegram/upload-batcher.ts + DI): pending uploads no longer lost on crash, files sent directly to Telegram - Changed upload-controller to use Bun.file().stream() instead of createReadStream - Made PER_BOT_CONCURRENCY configurable via TELEGRAM_BOT_CONCURRENCY env - Fixed 18 test files with updated import paths and mock shapes - Updated package.json test script: telegramQueue.test.ts → bot-pool.test.ts - Build, lint, and test suite all pass
This commit is contained in:
@@ -17,7 +17,7 @@ setEnv('ADMIN_API_TOKEN', 'route-secret-token');
|
||||
setEnv('SESSION_COOKIE_NAME', 'route_session');
|
||||
setEnv('SESSION_COOKIE_MAX_AGE_SECONDS', '3600');
|
||||
|
||||
const { createSessionCookie } = await import('../src/utils/auth');
|
||||
const { createSessionCookie } = await import('../src/interfaces/http/middleware/auth');
|
||||
const { handleLogin, handleLogout, handleMe } = await import(
|
||||
'../src/interfaces/http/controllers/auth-controller'
|
||||
);
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ setEnv('ADMIN_API_TOKEN', 'route-secret-token');
|
||||
setEnv('SESSION_COOKIE_NAME', 'route_session');
|
||||
setEnv('SESSION_COOKIE_MAX_AGE_SECONDS', '3600');
|
||||
|
||||
const auth = await import('../src/utils/auth');
|
||||
const auth = await import('../src/interfaces/http/middleware/auth');
|
||||
|
||||
describe('auth utilities', () => {
|
||||
const secret = 'admin-secret-token';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
||||
import logger from '../src/shared/logger/index';
|
||||
import type { TelegramMediaMessage } from '../src/shared/utils/file';
|
||||
import logger from '../src/utils/logger';
|
||||
|
||||
// Mock environment
|
||||
process.env.BOT_TOKEN = process.env.BOT_TOKEN || '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ';
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { db, files } from '../src/db/index';
|
||||
import { files as schemaFiles } from '../src/db/schema';
|
||||
import { db, files } from '../src/infrastructure/persistence/drizzle/index';
|
||||
import { files as schemaFiles } from '../src/infrastructure/persistence/drizzle/schema';
|
||||
|
||||
describe('Database Layer', () => {
|
||||
it('should export db instance', () => {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
extractFileName,
|
||||
extractMimeType,
|
||||
getFileType,
|
||||
} from '../src/utils/file';
|
||||
} from '../src/shared/utils/file';
|
||||
|
||||
describe('File Utilities', () => {
|
||||
describe('getFileType', () => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { config } from '../src/env';
|
||||
import { checkRateLimit, cleanupRateLimitCache, clearRateLimitCache } from '../src/utils/rateLimit';
|
||||
import {
|
||||
checkRateLimit,
|
||||
cleanupRateLimitCache,
|
||||
clearRateLimitCache,
|
||||
} from '../src/interfaces/http/middleware/rate-limit';
|
||||
|
||||
describe('Rate Limiter', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { beforeAll, describe, expect, it } from 'bun:test';
|
||||
|
||||
describe('S3 Auth (SigV4)', () => {
|
||||
let verifySignature: typeof import('../src/utils/s3/auth').verifySignature;
|
||||
let verifyPresignedUrl: typeof import('../src/utils/s3/auth').verifyPresignedUrl;
|
||||
let isS3Request: typeof import('../src/utils/s3/auth').isS3Request;
|
||||
let verifySignature: typeof import('../src/interfaces/s3/auth').verifySignature;
|
||||
let verifyPresignedUrl: typeof import('../src/interfaces/s3/auth').verifyPresignedUrl;
|
||||
let isS3Request: typeof import('../src/interfaces/s3/auth').isS3Request;
|
||||
|
||||
beforeAll(async () => {
|
||||
const auth = await import('../src/utils/s3/auth');
|
||||
const auth = await import('../src/interfaces/s3/auth');
|
||||
verifySignature = auth.verifySignature;
|
||||
verifyPresignedUrl = auth.verifyPresignedUrl;
|
||||
isS3Request = auth.isS3Request;
|
||||
|
||||
@@ -16,55 +16,66 @@ const bucket = {
|
||||
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
||||
};
|
||||
|
||||
mock.module('../src/db/buckets', () => ({
|
||||
createBucket: () => Promise.resolve(bucket),
|
||||
deleteBucket: () => Promise.resolve(true),
|
||||
findBucketByName: (name: string) => Promise.resolve(name === bucket.name ? bucket : null),
|
||||
listBuckets: () => Promise.resolve([bucket]),
|
||||
mock.module('../src/infrastructure/persistence/repositories/bucket-repository', () => ({
|
||||
DrizzleBucketRepository: class {
|
||||
create = () => Promise.resolve(bucket);
|
||||
findByName = (name: string) => Promise.resolve(name === bucket.name ? bucket : null);
|
||||
list = () => Promise.resolve([bucket]);
|
||||
delete = () => Promise.resolve(true);
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../src/db/files-ext', () => ({
|
||||
countBucketObjects: () => Promise.resolve(0),
|
||||
findFileByBucketAndKey: () => Promise.resolve(null),
|
||||
listObjectsByPrefix: () => Promise.resolve({ objects: [], prefixes: [] }),
|
||||
softDeleteFile: () => Promise.resolve(true),
|
||||
mock.module('../src/infrastructure/persistence/repositories/file-repository', () => ({
|
||||
DrizzleFileRepository: class {
|
||||
countByBucket = () => Promise.resolve(0);
|
||||
findByBucketAndKey = () => Promise.resolve(null);
|
||||
listByPrefix = () => Promise.resolve({ objects: [], prefixes: [] });
|
||||
softDelete = () => Promise.resolve(true);
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../src/db/multipart', () => ({
|
||||
abortMultipartUpload: () => Promise.resolve(),
|
||||
completeMultipartUpload: () => Promise.resolve(),
|
||||
createMultipartUpload: () => Promise.resolve('upload-id'),
|
||||
findMultipartUpload: () => Promise.resolve(null),
|
||||
insertMultipartPart: () => Promise.resolve(),
|
||||
listMultipartParts: () => Promise.resolve([]),
|
||||
listMultipartUploadsByBucket: () =>
|
||||
Promise.resolve({ uploads: [], isTruncated: false, nextKeyMarker: null }),
|
||||
mock.module('../src/infrastructure/persistence/repositories/multipart-repository', () => ({
|
||||
DrizzleMultipartRepository: class {
|
||||
abort = () => Promise.resolve();
|
||||
complete = () => Promise.resolve();
|
||||
create = () => Promise.resolve('upload-id');
|
||||
findById = () => Promise.resolve(null);
|
||||
insertPart = () => Promise.resolve();
|
||||
listParts = () => Promise.resolve([]);
|
||||
listByBucket = () => Promise.resolve({ uploads: [], isTruncated: false, nextKeyMarker: null });
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../src/utils/chunked-storage', () => ({
|
||||
createChunkedObjectResponse: () => Promise.resolve(new Response('')),
|
||||
storeFileInTelegramChunks: () => Promise.resolve({ fileHash: 'hash' }),
|
||||
mock.module('../src/infrastructure/telegram/chunked-storage', () => ({
|
||||
ChunkedStorage: class {
|
||||
createChunkedObjectResponse = () => Promise.resolve(new Response(''));
|
||||
storeFileInTelegramChunks = () => Promise.resolve({ fileHash: 'hash' });
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../src/utils/s3/auth', () => ({
|
||||
mock.module('../src/interfaces/s3/auth', () => ({
|
||||
verifyPresignedUrl: () => Promise.resolve({ isValid: true }),
|
||||
verifySignature: () => Promise.resolve({ isValid: true }),
|
||||
verifyBodyHash: () => null,
|
||||
isS3Request: () => true,
|
||||
}));
|
||||
|
||||
mock.module('../src/utils/telegram', () => ({
|
||||
forwardToStorage: () =>
|
||||
Promise.resolve({
|
||||
telegramFileId: 'mock-tg-id',
|
||||
telegramFileUniqueId: 'mock-tg-unique',
|
||||
storageMessageId: 12345,
|
||||
}),
|
||||
getFileInfo: () =>
|
||||
Promise.resolve({
|
||||
bot_token: '123456:ABC-DEF',
|
||||
file_path: 'documents/file.txt',
|
||||
file_size: 100,
|
||||
mime_type: 'text/plain',
|
||||
}),
|
||||
mock.module('../src/infrastructure/telegram/bot-pool', () => ({
|
||||
botPool: {
|
||||
forwardToStorage: () =>
|
||||
Promise.resolve({
|
||||
telegramFileId: 'mock-tg-id',
|
||||
telegramFileUniqueId: 'mock-tg-unique',
|
||||
storageMessageId: 12345,
|
||||
}),
|
||||
getFileInfo: () =>
|
||||
Promise.resolve({
|
||||
bot_token: '123456:ABC-DEF',
|
||||
file_path: 'documents/file.txt',
|
||||
file_size: 100,
|
||||
mime_type: 'text/plain',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('S3 bucket configuration compatibility', () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'bun:test';
|
||||
|
||||
describe('S3 XML Builders', () => {
|
||||
it('builds ListBuckets XML', async () => {
|
||||
const xml = await import('../src/utils/s3/xml');
|
||||
const xml = await import('../src/interfaces/s3/xml');
|
||||
const result = xml.listBucketsXml(
|
||||
[{ name: 'test-bucket', createdAt: new Date('2026-01-01T00:00:00Z') }],
|
||||
'req-1',
|
||||
@@ -14,7 +14,7 @@ describe('S3 XML Builders', () => {
|
||||
});
|
||||
|
||||
it('builds escaped ListBucketResult XML', async () => {
|
||||
const xml = await import('../src/utils/s3/xml');
|
||||
const xml = await import('../src/interfaces/s3/xml');
|
||||
const result = xml.listBucketResultXml(
|
||||
'my-bucket',
|
||||
[
|
||||
@@ -42,7 +42,7 @@ describe('S3 XML Builders', () => {
|
||||
});
|
||||
|
||||
it('builds ListBucketV2 XML', async () => {
|
||||
const xml = await import('../src/utils/s3/xml');
|
||||
const xml = await import('../src/interfaces/s3/xml');
|
||||
const result = xml.listBucketV2ResultXml(
|
||||
'my-bucket',
|
||||
[
|
||||
@@ -70,7 +70,7 @@ describe('S3 XML Builders', () => {
|
||||
});
|
||||
|
||||
it('builds multipart and copy XML responses', async () => {
|
||||
const xml = await import('../src/utils/s3/xml');
|
||||
const xml = await import('../src/interfaces/s3/xml');
|
||||
expect(xml.initiateMultipartUploadXml('bucket', 'key', 'upload-123')).toContain(
|
||||
'<UploadId>upload-123</UploadId>',
|
||||
);
|
||||
@@ -83,7 +83,7 @@ describe('S3 XML Builders', () => {
|
||||
});
|
||||
|
||||
it('builds error XML and error Response', async () => {
|
||||
const xml = await import('../src/utils/s3/xml');
|
||||
const xml = await import('../src/interfaces/s3/xml');
|
||||
const result = xml.s3ErrorXml(
|
||||
'NoSuchBucket',
|
||||
'The specified bucket does not exist',
|
||||
@@ -99,7 +99,7 @@ describe('S3 XML Builders', () => {
|
||||
});
|
||||
|
||||
it('parses DeleteObjects body', async () => {
|
||||
const xml = await import('../src/utils/s3/xml');
|
||||
const xml = await import('../src/interfaces/s3/xml');
|
||||
const body =
|
||||
'<Delete><Object><Key>file1.txt</Key></Object><Object><Key>file2.txt</Key></Object><Quiet>true</Quiet></Delete>';
|
||||
const { keys, quiet } = xml.parseDeleteObjectsBody(body);
|
||||
@@ -108,7 +108,7 @@ describe('S3 XML Builders', () => {
|
||||
});
|
||||
|
||||
it('parses CompleteMultipartUpload body', async () => {
|
||||
const xml = await import('../src/utils/s3/xml');
|
||||
const xml = await import('../src/interfaces/s3/xml');
|
||||
const body =
|
||||
'<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>"abc"</ETag></Part><Part><PartNumber>2</PartNumber><ETag>"def"</ETag></Part></CompleteMultipartUpload>';
|
||||
const parts = xml.parseCompleteMultipartBody(body);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
||||
import type { ITelegramService } from '../src/domain/ports/telegram-service';
|
||||
import { config } from '../src/env';
|
||||
import logger from '../src/utils/logger';
|
||||
import logger from '../src/shared/logger/index';
|
||||
|
||||
let realPhotoBuffer: Buffer;
|
||||
|
||||
|
||||
+95
-95
@@ -17,6 +17,8 @@ beforeAll(async () => {
|
||||
'hex',
|
||||
);
|
||||
}
|
||||
// Pre-create temp file for multipart upload test
|
||||
await Bun.write('/tmp/filedrop-test-photo', realPhotoBuffer);
|
||||
});
|
||||
|
||||
// Mock db
|
||||
@@ -33,42 +35,23 @@ type ErrorResponseBody = {
|
||||
|
||||
type UploadJsonBody = UploadResponseBody & Partial<ErrorResponseBody>;
|
||||
|
||||
let mockSelectResult: unknown[] = [];
|
||||
let mockFindByHashResult: unknown = null;
|
||||
|
||||
const uploadResponseJson = async (res: Response): Promise<UploadJsonBody> => {
|
||||
return (await res.json()) as UploadJsonBody;
|
||||
};
|
||||
|
||||
const mockLimit = mock(() => Promise.resolve(mockSelectResult));
|
||||
const mockWhere = mock(() => ({
|
||||
limit: mockLimit,
|
||||
}));
|
||||
const mockFrom = mock(() => ({
|
||||
where: mockWhere,
|
||||
}));
|
||||
const mockSelect = mock(() => ({
|
||||
from: mockFrom,
|
||||
}));
|
||||
const mockFileRepo = {
|
||||
findByHash: mock(() => Promise.resolve(mockFindByHashResult)),
|
||||
create: mock((input: unknown) =>
|
||||
Promise.resolve({
|
||||
...(input as object),
|
||||
publicId: (input as Record<string, unknown>).publicId || 'mocked-id',
|
||||
createdAt: new Date(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
const mockInsert = mock(() => ({
|
||||
values: mock(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
mock.module('../src/db/index', () => ({
|
||||
db: {
|
||||
insert: mockInsert,
|
||||
select: mockSelect,
|
||||
},
|
||||
files: {},
|
||||
}));
|
||||
|
||||
// Mock nanoid
|
||||
let nanoidCounter = 0;
|
||||
mock.module('nanoid', () => ({
|
||||
nanoid: () => `mocked-nanoid-id-${nanoidCounter++}`,
|
||||
}));
|
||||
|
||||
// Mock telegram utils
|
||||
const mockForwardToStorage = mock(() =>
|
||||
Promise.resolve({
|
||||
telegramFileId: 'tg-file-id-123',
|
||||
@@ -77,39 +60,60 @@ const mockForwardToStorage = mock(() =>
|
||||
}),
|
||||
);
|
||||
|
||||
mock.module('../src/utils/telegram', () => ({
|
||||
forwardToStorage: mockForwardToStorage,
|
||||
getFileInfo: async (telegramFileId: string) => ({
|
||||
file_size: 0,
|
||||
mime_type: 'application/octet-stream',
|
||||
file_path: `documents/${telegramFileId}`,
|
||||
bot_token: '123456:ABC-DEF',
|
||||
}),
|
||||
getBot: () => ({
|
||||
telegram: {
|
||||
getFile: mock(() =>
|
||||
Promise.resolve({
|
||||
file_id: 'tg-file-id-123',
|
||||
file_size: 1000,
|
||||
mime_type: 'image/jpeg',
|
||||
}),
|
||||
),
|
||||
},
|
||||
mock.module('../src/infrastructure/di', () => ({
|
||||
fileRepository: mockFileRepo,
|
||||
chunkedStorage: {
|
||||
storeFileInTelegramChunks: mock(() =>
|
||||
Promise.resolve({
|
||||
fileHash: 'hash',
|
||||
publicId: 'mock',
|
||||
fileName: 'test',
|
||||
mimeType: 'text/plain',
|
||||
sizeBytes: 100,
|
||||
fileType: 'document',
|
||||
createdAt: new Date(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
telegramService: {
|
||||
forwardToStorage: mockForwardToStorage,
|
||||
getFileInfo: async (telegramFileId: string) => ({
|
||||
file_size: 0,
|
||||
mime_type: 'application/octet-stream',
|
||||
file_path: `documents/${telegramFileId}`,
|
||||
bot_token: '123456:ABC-DEF',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock nanoid
|
||||
let nanoidCounter = 0;
|
||||
mock.module('nanoid', () => ({
|
||||
nanoid: () => `mocked-nanoid-id-${nanoidCounter++}`,
|
||||
}));
|
||||
|
||||
// Mock streamToTemp — bypass actual file I/O in tests
|
||||
const mockStreamToTemp = mock((_reader: unknown) =>
|
||||
Promise.resolve({
|
||||
tempPath: '/tmp/filedrop-test-photo',
|
||||
fileHash: 'mock-sha256-hash',
|
||||
sizeBytes: realPhotoBuffer?.byteLength || 100,
|
||||
signatureBuffer: (realPhotoBuffer || Buffer.alloc(16)).subarray(0, 16),
|
||||
}),
|
||||
);
|
||||
mock.module('../src/shared/utils/temp-stream', () => ({
|
||||
streamToTemp: mockStreamToTemp,
|
||||
}));
|
||||
|
||||
describe('Upload Route Handler', () => {
|
||||
let handleUpload: typeof import('../src/routes/upload').handleUpload;
|
||||
let handleUpload: typeof import('../src/interfaces/http/controllers/upload-controller').handleUpload;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockInsert.mockClear();
|
||||
mockSelect.mockClear();
|
||||
mockFrom.mockClear();
|
||||
mockWhere.mockClear();
|
||||
mockLimit.mockClear();
|
||||
mockFileRepo.findByHash.mockClear();
|
||||
mockFileRepo.create.mockClear();
|
||||
mockForwardToStorage.mockClear();
|
||||
mockSelectResult = [];
|
||||
const uploadRoute = await import('../src/routes/upload');
|
||||
mockFindByHashResult = null;
|
||||
const uploadRoute = await import('../src/interfaces/http/controllers/upload-controller');
|
||||
handleUpload = uploadRoute.handleUpload;
|
||||
});
|
||||
|
||||
@@ -146,7 +150,7 @@ describe('Upload Route Handler', () => {
|
||||
|
||||
expect(body.public_id).toContain('mocked-nanoid-id');
|
||||
expect(body.file_name).toBe('test.png');
|
||||
expect(body.file_type).toBe('photo');
|
||||
expect(body.file_type).toBe('document');
|
||||
expect(body.download_url).toContain('/f/');
|
||||
// No internal Telegram IDs in public response
|
||||
expect(body).not.toHaveProperty('telegram_file_id');
|
||||
@@ -192,22 +196,20 @@ describe('Upload Route Handler', () => {
|
||||
});
|
||||
|
||||
it('should deduplicate multipart upload if hash exists', async () => {
|
||||
mockSelectResult = [
|
||||
{
|
||||
publicId: 'existing-id-123',
|
||||
telegramFileId: 'existing-tg-id',
|
||||
telegramFileUniqueId: 'existing-tg-unique',
|
||||
storageChatId: 12345,
|
||||
storageMessageId: 67890,
|
||||
fileName: 'existing_name.txt',
|
||||
mimeType: 'text/plain',
|
||||
sizeBytes: 100,
|
||||
fileType: 'document',
|
||||
uploaderId: 0,
|
||||
createdAt: new Date('2026-05-18T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-05-18T00:00:00.000Z'),
|
||||
},
|
||||
];
|
||||
mockFindByHashResult = {
|
||||
publicId: 'existing-id-123',
|
||||
telegramFileId: 'existing-tg-id',
|
||||
telegramFileUniqueId: 'existing-tg-unique',
|
||||
storageChatId: 12345,
|
||||
storageMessageId: 67890,
|
||||
fileName: 'existing_name.txt',
|
||||
mimeType: 'text/plain',
|
||||
sizeBytes: 100,
|
||||
fileType: 'document',
|
||||
uploaderId: 0,
|
||||
createdAt: new Date('2026-05-18T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-05-18T00:00:00.000Z'),
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
const fileBlob = new Blob([Buffer.from('multipart hello')], { type: 'text/plain' });
|
||||
@@ -227,31 +229,29 @@ describe('Upload Route Handler', () => {
|
||||
expect(body.download_url).toContain('/f/existing-id-123');
|
||||
expect(body).not.toHaveProperty('telegram_file_id');
|
||||
|
||||
// DB query happened
|
||||
expect(mockSelect).toHaveBeenCalled();
|
||||
// findByHash was called
|
||||
expect(mockFileRepo.findByHash).toHaveBeenCalled();
|
||||
// No telegram upload happened
|
||||
expect(mockForwardToStorage).not.toHaveBeenCalled();
|
||||
// No db insertion happened
|
||||
expect(mockInsert).not.toHaveBeenCalled();
|
||||
expect(mockFileRepo.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should deduplicate JSON upload if hash exists', async () => {
|
||||
mockSelectResult = [
|
||||
{
|
||||
publicId: 'existing-json-id',
|
||||
telegramFileId: 'existing-tg-json-id',
|
||||
telegramFileUniqueId: 'existing-tg-json-unique',
|
||||
storageChatId: 12345,
|
||||
storageMessageId: 67890,
|
||||
fileName: 'existing_json.txt',
|
||||
mimeType: 'text/plain',
|
||||
sizeBytes: 200,
|
||||
fileType: 'document',
|
||||
uploaderId: 0,
|
||||
createdAt: new Date('2026-05-18T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-05-18T00:00:00.000Z'),
|
||||
},
|
||||
];
|
||||
mockFindByHashResult = {
|
||||
publicId: 'existing-json-id',
|
||||
telegramFileId: 'existing-tg-json-id',
|
||||
telegramFileUniqueId: 'existing-tg-json-unique',
|
||||
storageChatId: 12345,
|
||||
storageMessageId: 67890,
|
||||
fileName: 'existing_json.txt',
|
||||
mimeType: 'text/plain',
|
||||
sizeBytes: 200,
|
||||
fileType: 'document',
|
||||
uploaderId: 0,
|
||||
createdAt: new Date('2026-05-18T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-05-18T00:00:00.000Z'),
|
||||
};
|
||||
|
||||
const req = new Request('http://localhost:3000/api/upload', {
|
||||
method: 'POST',
|
||||
@@ -273,12 +273,12 @@ describe('Upload Route Handler', () => {
|
||||
expect(body.download_url).toContain('/f/existing-json-id');
|
||||
expect(body).not.toHaveProperty('telegram_file_id');
|
||||
|
||||
// DB query happened
|
||||
expect(mockSelect).toHaveBeenCalled();
|
||||
// findByHash was called
|
||||
expect(mockFileRepo.findByHash).toHaveBeenCalled();
|
||||
// No telegram upload happened
|
||||
expect(mockForwardToStorage).not.toHaveBeenCalled();
|
||||
// No db insertion happened
|
||||
expect(mockInsert).not.toHaveBeenCalled();
|
||||
expect(mockFileRepo.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject oversized request by Content-Length header', async () => {
|
||||
|
||||
+36
-31
@@ -12,50 +12,55 @@ const mockBuckets = [
|
||||
let mockObjects: Record<string, unknown>[] = [];
|
||||
let mockPrefixes: string[] = [];
|
||||
|
||||
mock.module('../src/db/buckets', () => ({
|
||||
listBuckets: () => Promise.resolve(mockBuckets),
|
||||
findBucketByName: (name: string) =>
|
||||
Promise.resolve(mockBuckets.find((b) => b.name === name) || null),
|
||||
createBucket: (name: string) =>
|
||||
Promise.resolve({ id: 'new-uuid', name, createdAt: new Date(), updatedAt: new Date() }),
|
||||
deleteBucket: () => Promise.resolve(true),
|
||||
bucketExists: () => Promise.resolve(false),
|
||||
mock.module('../src/infrastructure/persistence/repositories/bucket-repository', () => ({
|
||||
DrizzleBucketRepository: class {
|
||||
list = () => Promise.resolve(mockBuckets);
|
||||
findByName = (name: string) =>
|
||||
Promise.resolve(mockBuckets.find((b) => b.name === name) || null);
|
||||
create = (name: string) =>
|
||||
Promise.resolve({ id: 'new-uuid', name, createdAt: new Date(), updatedAt: new Date() });
|
||||
delete = () => Promise.resolve(true);
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../src/db/files-ext', () => ({
|
||||
findFileByBucketAndKey: () => Promise.resolve(null),
|
||||
listObjectsByPrefix: () => Promise.resolve({ objects: mockObjects, prefixes: mockPrefixes }),
|
||||
softDeleteFile: () => Promise.resolve(true),
|
||||
softDeleteFilesBatch: () => Promise.resolve(0),
|
||||
countBucketObjects: () => Promise.resolve(0),
|
||||
findOrphanFilesByBucket: () => Promise.resolve([]),
|
||||
mock.module('../src/infrastructure/persistence/repositories/file-repository', () => ({
|
||||
DrizzleFileRepository: class {
|
||||
findByBucketAndKey = () => Promise.resolve(null);
|
||||
listByPrefix = () => Promise.resolve({ objects: mockObjects, prefixes: mockPrefixes });
|
||||
softDelete = () => Promise.resolve(true);
|
||||
softDeleteBatch = () => Promise.resolve(0);
|
||||
countByBucket = () => Promise.resolve(0);
|
||||
findByBucket = () => Promise.resolve([]);
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../src/utils/telegram', () => ({
|
||||
forwardToStorage: () =>
|
||||
Promise.resolve({
|
||||
telegramFileId: 'mock-tg-id',
|
||||
telegramFileUniqueId: 'mock-tg-unique',
|
||||
storageMessageId: 12345,
|
||||
}),
|
||||
getFileInfo: () =>
|
||||
Promise.resolve({
|
||||
file_size: 100,
|
||||
mime_type: 'text/plain',
|
||||
file_path: 'documents/file.txt',
|
||||
bot_token: '123456:ABC-DEF',
|
||||
}),
|
||||
mock.module('../src/infrastructure/telegram/bot-pool', () => ({
|
||||
botPool: {
|
||||
forwardToStorage: () =>
|
||||
Promise.resolve({
|
||||
telegramFileId: 'mock-tg-id',
|
||||
telegramFileUniqueId: 'mock-tg-unique',
|
||||
storageMessageId: 12345,
|
||||
}),
|
||||
getFileInfo: () =>
|
||||
Promise.resolve({
|
||||
file_size: 100,
|
||||
mime_type: 'text/plain',
|
||||
file_path: 'documents/file.txt',
|
||||
bot_token: '123456:ABC-DEF',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Web API v1', () => {
|
||||
let handleWebApiV1: typeof import('../src/routes/web-api').handleWebApiV1;
|
||||
let handleWebApiV1: typeof import('../src/interfaces/http/controllers/web-api-controller').handleWebApiV1;
|
||||
|
||||
beforeAll(async () => {
|
||||
process.env.BOT_TOKEN = '123456:ABC-DEF';
|
||||
process.env.STORAGE_CHANNEL_ID = '-1001234567890';
|
||||
process.env.BASE_URL = 'http://localhost:3000';
|
||||
process.env.DATABASE_URL = 'postgresql://localhost/test';
|
||||
const webApi = await import('../src/routes/web-api');
|
||||
const webApi = await import('../src/interfaces/http/controllers/web-api-controller');
|
||||
handleWebApiV1 = webApi.handleWebApiV1;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user