feat: enhance file upload handling with improved file detection, size limits, and response formatting

This commit is contained in:
MythEclipse
2026-05-21 22:31:55 +07:00
parent 06844f1fa1
commit 52340ee77d
20 changed files with 696 additions and 539 deletions
+14 -3
View File
@@ -1,7 +1,17 @@
// @ts-nocheck
import { afterAll, beforeEach, describe, expect, it, mock } from 'bun:test';
const mockServe = mock((options) => {
type ServeOptions = {
port?: number;
routes?: Record<string, unknown>;
};
type MockServer = {
port?: number;
routes?: Record<string, unknown>;
stop: ReturnType<typeof mock>;
};
const mockServe = mock((options: ServeOptions): MockServer => {
return {
port: options.port,
routes: options.routes,
@@ -10,7 +20,7 @@ const mockServe = mock((options) => {
});
const originalServe = Bun.serve;
Bun.serve = mockServe;
Bun.serve = mockServe as unknown as typeof Bun.serve;
const mockStartBot = mock(() =>
Promise.resolve({
@@ -58,6 +68,7 @@ describe('Bootstrap Server', () => {
const serveCallArgs = mockServe.mock.calls[0][0];
expect(serveCallArgs).toHaveProperty('port');
expect(serveCallArgs).toHaveProperty('routes');
expect(serveCallArgs.routes).toBeDefined();
expect(serveCallArgs.routes).toHaveProperty('/api/upload');
expect(serveCallArgs.routes).toHaveProperty('/f/:public_id');
expect(serveCallArgs.routes).toHaveProperty('/file/:public_id/info');
+59 -44
View File
@@ -1,5 +1,5 @@
// @ts-nocheck
import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import type { TelegramMediaMessage } from '../src/utils/file';
import logger from '../src/utils/logger';
// Mock environment
@@ -7,48 +7,66 @@ process.env.BOT_TOKEN = process.env.BOT_TOKEN || '123456789:ABCdefGhIJKlmNoPQRsT
process.env.STORAGE_CHANNEL_ID = process.env.STORAGE_CHANNEL_ID || '-1001234567890';
process.env.BASE_URL = process.env.BASE_URL || 'https://tele.asepharyana.tech';
type BotTestContext = {
message: TelegramMediaMessage;
from: { id: number };
reply: ReturnType<typeof mock>;
};
type BotFileHandler = (ctx: BotTestContext) => Promise<unknown>;
type StartHandler = (ctx: { reply: ReturnType<typeof mock> }) => Promise<unknown>;
const getStartHandler = (): StartHandler => {
return mockCommand.mock.calls.find((call) => call[0] === 'start')?.[1] as StartHandler;
};
const getFileHandler = (): BotFileHandler => {
return mockOn.mock.calls[0][1] as BotFileHandler;
};
// Mock Telegraf
const mockLaunch = mock(() => Promise.resolve());
const mockCommand = mock();
const mockOn = mock();
const mockUse = mock();
mock.module('telegraf', () => {
return {
Telegraf: class {
constructor(token) {
this.token = token;
this.launch = mockLaunch;
this.command = mockCommand;
this.on = mockOn;
this.use = mockUse;
}
},
};
});
class MockTelegraf {
token: string;
launch = mockLaunch;
command = mockCommand;
on = mockOn;
use = mockUse;
constructor(token: string) {
this.token = token;
}
}
mock.module('telegraf', () => ({
Telegraf: MockTelegraf,
}));
// Mock database
const mockInsert = mock(() => ({
values: mock(() => Promise.resolve()),
}));
const mockLimit = mock(() => Promise.resolve([]));
const mockWhere = mock(() => ({
limit: mockLimit,
}));
const mockFrom = mock(() => ({
where: mockWhere,
}));
const mockSelect = mock(() => ({
from: mockFrom,
}));
type ExistingFile = {
publicId: string;
telegramFileId: string;
telegramFileUniqueId: string;
};
const mockFindFileByUniqueId = mock((): Promise<ExistingFile | null> => Promise.resolve(null));
mock.module('../src/db/index', () => ({
db: {
insert: mockInsert,
select: mockSelect,
},
files: {
telegramFileUniqueId: 'telegram_file_unique_id',
},
files: {},
}));
mock.module('../src/db/files', () => ({
findFileByUniqueId: mockFindFileByUniqueId,
}));
// Mock forwardToStorage
@@ -73,8 +91,8 @@ describe('Telegram Bot Handler', () => {
mockOn.mockClear();
mockUse.mockClear();
mockInsert.mockClear();
mockLimit.mockClear();
mockLimit.mockResolvedValue([]);
mockFindFileByUniqueId.mockClear();
mockFindFileByUniqueId.mockResolvedValue(null);
mockForwardToStorage.mockClear();
infoSpy.mockClear();
errorSpy.mockClear();
@@ -98,7 +116,7 @@ describe('Telegram Bot Handler', () => {
const { startBot } = await import('../src/bot');
await startBot();
const startHandler = mockCommand.mock.calls.find((call) => call[0] === 'start')[1];
const startHandler = getStartHandler();
const replyMock = mock(() => Promise.resolve());
const ctx = {
reply: replyMock,
@@ -112,7 +130,7 @@ describe('Telegram Bot Handler', () => {
const { startBot } = await import('../src/bot');
await startBot();
const fileHandler = mockOn.mock.calls[0][1];
const fileHandler = getFileHandler();
const replyMock = mock(() => Promise.resolve());
const ctx = {
message: {
@@ -144,7 +162,7 @@ describe('Telegram Bot Handler', () => {
const { startBot } = await import('../src/bot');
await startBot();
const fileHandler = mockOn.mock.calls[0][1];
const fileHandler = getFileHandler();
const replyMock = mock(() => Promise.resolve());
const ctx = {
message: {
@@ -173,17 +191,14 @@ describe('Telegram Bot Handler', () => {
const { startBot } = await import('../src/bot');
await startBot();
const fileHandler = mockOn.mock.calls[0][1];
const fileHandler = getFileHandler();
const replyMock = mock(() => Promise.resolve());
// Mock DB to return an existing match
mockLimit.mockResolvedValueOnce([
{
publicId: 'already_exists_abc',
telegramFileId: 'stored_file_id',
telegramFileUniqueId: 'doc_uniq_123',
},
]);
mockFindFileByUniqueId.mockResolvedValueOnce({
publicId: 'already_exists_abc',
telegramFileId: 'stored_file_id',
telegramFileUniqueId: 'doc_uniq_123',
});
const ctx = {
message: {
@@ -215,7 +230,7 @@ describe('Telegram Bot Handler', () => {
const { startBot } = await import('../src/bot');
await startBot();
const fileHandler = mockOn.mock.calls[0][1];
const fileHandler = getFileHandler();
const replyMock = mock(() => Promise.resolve());
const ctx = {
message: {
@@ -245,7 +260,7 @@ describe('Telegram Bot Handler', () => {
const { startBot } = await import('../src/bot');
await startBot();
const fileHandler = mockOn.mock.calls[0][1];
const fileHandler = getFileHandler();
const replyMock = mock(() => Promise.resolve());
const ctx = {
message: {
-1
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import { describe, expect, it } from 'bun:test';
import { db, files } from '../src/db/index';
import { files as schemaFiles } from '../src/db/schema';
+2 -3
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import { describe, expect, it } from 'bun:test';
import { config } from '../src/env';
@@ -16,12 +15,12 @@ describe('Environment Variables Validation', () => {
});
it('config.botToken should return BOT_TOKEN from process.env', () => {
expect(config.botToken).toBe(process.env.BOT_TOKEN);
expect(config.botToken).toBe(process.env.BOT_TOKEN || '');
});
it('config.storageChatId should be parsed as integer from STORAGE_CHANNEL_ID', () => {
expect(typeof config.storageChatId).toBe('number');
expect(config.storageChatId).toBe(parseInt(process.env.STORAGE_CHANNEL_ID, 10));
expect(config.storageChatId).toBe(parseInt(process.env.STORAGE_CHANNEL_ID || '0', 10));
});
it('config.port should default to 3000 when not specified', () => {
-1
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import { describe, expect, it } from 'bun:test';
import {
checkFileSize,
+71 -38
View File
@@ -1,23 +1,62 @@
// @ts-nocheck
import { afterAll, beforeEach, describe, expect, it, mock } from 'bun:test';
// Mock database layer
const mockSelect = mock(() => ({
from: mock(() => ({
where: mock(() => ({
limit: mock(() => Promise.resolve([])),
})),
})),
}));
type RequestWithParams = Request & {
params?: {
public_id?: string;
};
};
mock.module('../src/db/index', () => ({
db: {
select: mockSelect,
},
files: {
publicId: {
equals: (val) => ({ type: 'equals', value: val }),
},
type ErrorBody = {
error: string;
};
type FileInfoBody = {
public_id: string;
file_name: string;
mime_type: string;
size_bytes: number;
file_type: string;
uploader_id: number;
created_at: string;
};
type JsonBody = ErrorBody | FileInfoBody | Record<string, unknown>;
type MockFileRecord = Record<string, unknown>;
type MockSelectChain = {
from: () => {
where: () => {
limit: () => Promise<MockFileRecord[]>;
};
};
};
const requestWithPublicId = (url: string, publicId: string): RequestWithParams => {
const req = new Request(url) as RequestWithParams;
req.params = { public_id: publicId };
return req;
};
const responseJson = async <T extends JsonBody>(res: Response): Promise<T> => {
return (await res.json()) as T;
};
// Mock database layer
const emptySelectChain = (): MockSelectChain => ({
from: () => ({
where: () => ({
limit: () => Promise.resolve([]),
}),
}),
});
const mockSelect = mock(() => emptySelectChain());
mock.module('../src/db/files', () => ({
findFileByPublicId: async () => {
const chain = mockSelect();
return (await chain.from().where().limit())[0] || null;
},
}));
@@ -38,7 +77,8 @@ mock.module('../src/utils/rateLimit', () => ({
}));
describe('File Route Handlers', () => {
let handleFileRedirect: any, handleFileInfo: any;
let handleFileRedirect: typeof import('../src/routes/files').handleFileRedirect;
let handleFileInfo: typeof import('../src/routes/files').handleFileInfo;
beforeEach(async () => {
mockSelect.mockClear();
@@ -56,12 +96,11 @@ describe('File Route Handlers', () => {
describe('handleFileRedirect', () => {
it('should return 429 if rate limit is exceeded', async () => {
mockCheckRateLimit.mockImplementationOnce(() => false);
const req = new Request('http://localhost:3000/f/test-id');
req.params = { public_id: 'test-id' };
const req = requestWithPublicId('http://localhost:3000/f/test-id', 'test-id');
const res = await handleFileRedirect(req);
expect(res.status).toBe(429);
const body = await res.json();
const body = await responseJson<ErrorBody>(res);
expect(body.error).toBe('Rate limit exceeded');
});
@@ -74,11 +113,10 @@ describe('File Route Handlers', () => {
}),
}));
const req = new Request('http://localhost:3000/f/missing-id');
req.params = { public_id: 'missing-id' };
const req = requestWithPublicId('http://localhost:3000/f/missing-id', 'missing-id');
const res = await handleFileRedirect(req);
expect(res.status).toBe(404);
const body = await res.json();
const body = await responseJson<ErrorBody>(res);
expect(body.error).toBe('File not found');
});
@@ -99,8 +137,7 @@ describe('File Route Handlers', () => {
}),
}));
const req = new Request('http://localhost:3000/f/test-id');
req.params = { public_id: 'test-id' };
const req = requestWithPublicId('http://localhost:3000/f/test-id', 'test-id');
const res = await handleFileRedirect(req);
expect(res.status).toBe(302);
expect(res.headers.get('Location')).toBe(
@@ -113,11 +150,10 @@ describe('File Route Handlers', () => {
throw new Error('DB Connection Error');
});
const req = new Request('http://localhost:3000/f/test-id');
req.params = { public_id: 'test-id' };
const req = requestWithPublicId('http://localhost:3000/f/test-id', 'test-id');
const res = await handleFileRedirect(req);
expect(res.status).toBe(500);
const body = await res.json();
const body = await responseJson<ErrorBody>(res);
expect(body.error).toBe('Server error');
});
});
@@ -132,11 +168,10 @@ describe('File Route Handlers', () => {
}),
}));
const req = new Request('http://localhost:3000/file/missing-id/info');
req.params = { public_id: 'missing-id' };
const req = requestWithPublicId('http://localhost:3000/file/missing-id/info', 'missing-id');
const res = await handleFileInfo(req);
expect(res.status).toBe(404);
const body = await res.json();
const body = await responseJson<ErrorBody>(res);
expect(body.error).toBe('File not found');
});
@@ -159,11 +194,10 @@ describe('File Route Handlers', () => {
}),
}));
const req = new Request('http://localhost:3000/file/test-id/info');
req.params = { public_id: 'test-id' };
const req = requestWithPublicId('http://localhost:3000/file/test-id/info', 'test-id');
const res = await handleFileInfo(req);
expect(res.status).toBe(200);
const body = await res.json();
const body = await responseJson<FileInfoBody>(res);
expect(body).toEqual({
public_id: 'test-id',
file_name: 'image.png',
@@ -180,11 +214,10 @@ describe('File Route Handlers', () => {
throw new Error('DB Connection Error');
});
const req = new Request('http://localhost:3000/file/test-id/info');
req.params = { public_id: 'test-id' };
const req = requestWithPublicId('http://localhost:3000/file/test-id/info', 'test-id');
const res = await handleFileInfo(req);
expect(res.status).toBe(500);
const body = await res.json();
const body = await responseJson<ErrorBody>(res);
expect(body.error).toBe('Server error');
});
});
+1 -2
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import { beforeEach, describe, expect, it, mock } from 'bun:test';
// Mock database layer
@@ -11,7 +10,7 @@ mock.module('../src/db/index', () => ({
}));
describe('Health Route Handler', () => {
let handleHealth: any;
let handleHealth: typeof import('../src/routes/health').handleHealth;
beforeEach(async () => {
mockExecute.mockClear();
-1
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import { beforeEach, describe, expect, it, spyOn } from 'bun:test';
import logger from '../src/utils/logger';
import { checkRateLimit, cleanupRateLimitCache } from '../src/utils/rateLimit';
+5 -1
View File
@@ -8,7 +8,11 @@ describe('Swagger Documentation Endpoints', () => {
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toContain('application/json');
const body = (await res.json()) as any;
const body = (await res.json()) as {
openapi: string;
info: { title: string };
paths: Record<string, { post?: { requestBody: { content: Record<string, unknown> } } }>;
};
expect(body.openapi).toBe('3.0.0');
expect(body.info.title).toBe('TeleUploader API');
expect(body.paths).toHaveProperty('/health');
+3 -2
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import { afterEach, beforeAll, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import { config } from '../src/env';
import logger from '../src/utils/logger';
@@ -57,7 +56,9 @@ const infoSpy = spyOn(logger, 'info');
const errorSpy = spyOn(logger, 'error');
describe('Telegram API Utilities', () => {
let forwardToStorage: any, getFileInfo: any, getBot: any;
let forwardToStorage: typeof import('../src/utils/telegram').forwardToStorage;
let getFileInfo: typeof import('../src/utils/telegram').getFileInfo;
let getBot: typeof import('../src/utils/telegram').getBot;
beforeEach(async () => {
infoSpy.mockClear();
+27 -9
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import { afterAll, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
let realPhotoBuffer: Buffer;
@@ -21,7 +20,26 @@ beforeAll(async () => {
});
// Mock db
let mockSelectResult: any[] = [];
type UploadResponseBody = {
public_id: string;
telegram_file_id: string;
telegram_file_unique_id: string;
file_name: string;
file_type: string;
download_url: string;
};
type ErrorResponseBody = {
error: string;
};
type UploadJsonBody = UploadResponseBody & Partial<ErrorResponseBody>;
let mockSelectResult: unknown[] = [];
const uploadResponseJson = async (res: Response): Promise<UploadJsonBody> => {
return (await res.json()) as UploadJsonBody;
};
const mockLimit = mock(() => Promise.resolve(mockSelectResult));
const mockWhere = mock(() => ({
@@ -79,7 +97,7 @@ mock.module('../src/utils/telegram', () => ({
}));
describe('Upload Route Handler', () => {
let handleUpload: any;
let handleUpload: typeof import('../src/routes/upload').handleUpload;
beforeEach(async () => {
mockInsert.mockClear();
@@ -105,7 +123,7 @@ describe('Upload Route Handler', () => {
const res = await handleUpload(req);
expect(res.status).toBe(400);
const body = await res.json();
const body = await uploadResponseJson(res);
expect(body.error).toContain('Unsupported content type');
});
@@ -123,7 +141,7 @@ describe('Upload Route Handler', () => {
const res = await handleUpload(req);
expect(res.status).toBe(200);
const body = await res.json();
const body = await uploadResponseJson(res);
expect(body.public_id).toContain('mocked-nanoid-id');
expect(body.telegram_file_id).toBe('tg-file-id-123');
@@ -145,7 +163,7 @@ describe('Upload Route Handler', () => {
const res = await handleUpload(req);
expect(res.status).toBe(400);
const body = await res.json();
const body = await uploadResponseJson(res);
expect(body.error).toContain('Invalid JSON');
});
@@ -161,7 +179,7 @@ describe('Upload Route Handler', () => {
const res = await handleUpload(req);
expect(res.status).toBe(200);
const body = await res.json();
const body = await uploadResponseJson(res);
expect(body.public_id).toContain('mocked-nanoid-id');
expect(body.file_name).toBe('test_multi.png');
});
@@ -195,7 +213,7 @@ describe('Upload Route Handler', () => {
const res = await handleUpload(req);
expect(res.status).toBe(200);
const body = await res.json();
const body = await uploadResponseJson(res);
expect(body.public_id).toBe('existing-id-123');
expect(body.telegram_file_id).toBe('existing-tg-id');
@@ -242,7 +260,7 @@ describe('Upload Route Handler', () => {
const res = await handleUpload(req);
expect(res.status).toBe(200);
const body = await res.json();
const body = await uploadResponseJson(res);
expect(body.public_id).toBe('existing-json-id');
expect(body.telegram_file_id).toBe('existing-tg-json-id');