style: format and lint codebase using Biome v2

This commit is contained in:
MythEclipse
2026-05-18 07:29:01 +07:00
parent 3f4e697733
commit cf79a1f195
22 changed files with 638 additions and 528 deletions
+10 -7
View File
@@ -1,7 +1,11 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"organizeImports": { "assist": {
"enabled": true "actions": {
"source": {
"organizeImports": "on"
}
}
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
@@ -20,13 +24,12 @@
"formatWithErrors": false, "formatWithErrors": false,
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 2, "indentWidth": 2,
"lineWidth": 100, "lineWidth": 100
"quoteStyle": "single",
"semicolons": "always"
}, },
"javascript": { "javascript": {
"formatter": { "formatter": {
"quoteStyle": "single" "quoteStyle": "single",
"semicolons": "always"
} }
} }
} }
+84 -59
View File
@@ -1,8 +1,8 @@
import { Telegraf, type Context } from 'telegraf';
import logger from './utils/logger';
import { config } from './env';
import { db, files as fileSchema } from './db';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { type Context, Telegraf } from 'telegraf';
import { db, files as fileSchema } from './db';
import { config } from './env';
import logger from './utils/logger';
import { forwardToStorage } from './utils/telegram'; import { forwardToStorage } from './utils/telegram';
export const startBot = async (): Promise<Telegraf<Context>> => { export const startBot = async (): Promise<Telegraf<Context>> => {
@@ -12,76 +12,101 @@ export const startBot = async (): Promise<Telegraf<Context>> => {
bot.command('start', async (ctx) => { bot.command('start', async (ctx) => {
await ctx.reply( await ctx.reply(
`👋 Halo! Kirimkan file (document, photo, video, audio, voice, animation) ke bot ini. ` + `👋 Halo! Kirimkan file (document, photo, video, audio, voice, animation) ke bot ini. ` +
`File akan disimpan di private channel dan kamu dapat download link permanen.` `File akan disimpan di private channel dan kamu dapat download link permanen.`,
); );
}); });
// Cast bot.on elements individually or explicitly as any to bypass Telegraf v4 typescript deprecation warnings on array syntax // Cast bot.on elements individually or explicitly as any to bypass Telegraf v4 typescript deprecation warnings on array syntax
(bot as any).on(['document', 'photo', 'video', 'audio', 'voice', 'animation'], async (ctx: any) => { (bot as any).on(
try { ['document', 'photo', 'video', 'audio', 'voice', 'animation'],
const fileType: 'document' | 'photo' | 'video' | 'audio' | 'voice' | 'animation' = ctx.message.document ? 'document' : async (ctx: any) => {
ctx.message.photo ? 'photo' : try {
ctx.message.video ? 'video' : const fileType: 'document' | 'photo' | 'video' | 'audio' | 'voice' | 'animation' = ctx
ctx.message.audio ? 'audio' : .message.document
ctx.message.voice ? 'voice' : 'animation'; ? 'document'
: ctx.message.photo
? 'photo'
: ctx.message.video
? 'video'
: ctx.message.audio
? 'audio'
: ctx.message.voice
? 'voice'
: 'animation';
const fileObj = fileType === 'photo' ? ctx.message.photo.slice(-1)[0] : ctx.message[fileType]; const fileObj =
const { file_id, file_size, mime_type } = fileObj; fileType === 'photo' ? ctx.message.photo.slice(-1)[0] : ctx.message[fileType];
const fileName = ctx.message.document?.file_name || const { file_id, file_size, mime_type } = fileObj;
ctx.message.photo?.slice(-1)[0]?.file_name || const fileName =
ctx.message.video?.file_name || ctx.message.document?.file_name ||
ctx.message.audio?.file_name || ctx.message.photo?.slice(-1)[0]?.file_name ||
ctx.message.voice?.file_name || ctx.message.video?.file_name ||
'file'; ctx.message.audio?.file_name ||
ctx.message.voice?.file_name ||
'file';
const maxSize = fileType === 'photo' ? 10 * 1024 * 1024 : const maxSize =
fileType === 'audio' ? 200 * 1024 * 1024 : fileType === 'photo'
fileType === 'voice' ? 200 * 1024 * 1024 : 2 * 1024 * 1024 * 1024; ? 10 * 1024 * 1024
: fileType === 'audio'
? 200 * 1024 * 1024
: fileType === 'voice'
? 200 * 1024 * 1024
: 2 * 1024 * 1024 * 1024;
if (file_size > maxSize) { if (file_size > maxSize) {
return ctx.reply(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`); return ctx.reply(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`);
}
const result = await forwardToStorage(file_id, fileName);
const publicId = nanoid();
const uploaded = {
publicId: publicId,
telegramFileId: result.telegramFileId,
telegramFileUniqueId: result.telegramFileUniqueId,
storageChatId: config.storageChatId,
storageMessageId: result.storageMessageId,
fileName: fileName,
mimeType: mime_type || 'application/octet-stream',
sizeBytes: file_size,
fileType: fileType,
uploaderId: ctx.from.id,
createdAt: new Date(),
updatedAt: new Date(),
};
await db.insert(fileSchema).values(uploaded);
const url = `${config.baseUrl}/f/${publicId}`;
await ctx.reply(`File berhasil diupload! 📎\n\nDownload: ${url}`, {
reply_parameters: { message_id: ctx.message.message_id },
});
logger.info('File uploaded via bot', {
publicId,
fileType,
fileName,
uploader: ctx.from.id,
});
} catch (error: any) {
logger.error('Bot file handler error', { error: error.message, chat_id: ctx.chat?.id });
await ctx.reply('❌ Gagal mengupload file. Coba lagi nanti.');
} }
},
const result = await forwardToStorage(file_id, fileName); );
const publicId = nanoid();
const uploaded = {
publicId: publicId,
telegramFileId: result.telegramFileId,
telegramFileUniqueId: result.telegramFileUniqueId,
storageChatId: config.storageChatId,
storageMessageId: result.storageMessageId,
fileName: fileName,
mimeType: mime_type || 'application/octet-stream',
sizeBytes: file_size,
fileType: fileType,
uploaderId: ctx.from.id,
createdAt: new Date(),
updatedAt: new Date()
};
await db.insert(fileSchema).values(uploaded);
const url = `${config.baseUrl}/f/${publicId}`;
await ctx.reply(`File berhasil diupload! 📎\n\nDownload: ${url}`, {
reply_parameters: { message_id: ctx.message.message_id }
});
logger.info('File uploaded via bot', { publicId, fileType, fileName, uploader: ctx.from.id });
} catch (error: any) {
logger.error('Bot file handler error', { error: error.message, chat_id: ctx.chat?.id });
await ctx.reply('❌ Gagal mengupload file. Coba lagi nanti.');
}
});
bot.use((ctx, next) => { bot.use((ctx, next) => {
logger.info('Telegram event received', { type: (ctx.update as any).type, chat_id: ctx.chat?.id }); logger.info('Telegram event received', {
type: (ctx.update as any).type,
chat_id: ctx.chat?.id,
});
return next(); return next();
}); });
await bot.launch(); await bot.launch();
logger.info('Telegram bot started', { botToken: config.botToken?.substring(0, 10) + '...' }); logger.info('Telegram bot started', { botToken: `${config.botToken?.substring(0, 10)}...` });
return bot; return bot;
} catch (error: any) { } catch (error: any) {
+1 -1
View File
@@ -5,7 +5,7 @@ import { files } from './schema';
const client = postgres(process.env.DATABASE_URL!, { const client = postgres(process.env.DATABASE_URL!, {
max: 10, max: 10,
idle_timeout: 20, idle_timeout: 20,
connect_timeout: 10 connect_timeout: 10,
}); });
export const db = drizzle(client, { schema: { files } }); export const db = drizzle(client, { schema: { files } });
+3 -3
View File
@@ -1,5 +1,5 @@
import { pgTable, text, bigint, timestamp, uuid } from 'drizzle-orm/pg-core'; import type { InferInsertModel, InferSelectModel } from 'drizzle-orm';
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; import { bigint, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
export const files = pgTable('files', { export const files = pgTable('files', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@@ -14,7 +14,7 @@ export const files = pgTable('files', {
fileType: text('file_type').notNull(), fileType: text('file_type').notNull(),
uploaderId: bigint('uploader_id', { mode: 'number' }).notNull(), uploaderId: bigint('uploader_id', { mode: 'number' }).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull() updatedAt: timestamp('updated_at').defaultNow().notNull(),
}); });
export type File = InferSelectModel<typeof files>; export type File = InferSelectModel<typeof files>;
+5 -3
View File
@@ -17,7 +17,7 @@ const requiredEnv = {
STORAGE_CHANNEL_ID: process.env.STORAGE_CHANNEL_ID, STORAGE_CHANNEL_ID: process.env.STORAGE_CHANNEL_ID,
BASE_URL: process.env.BASE_URL, BASE_URL: process.env.BASE_URL,
DATABASE_URL: process.env.DATABASE_URL, DATABASE_URL: process.env.DATABASE_URL,
PORT: process.env.PORT PORT: process.env.PORT,
}; };
const missing = Object.entries(requiredEnv) const missing = Object.entries(requiredEnv)
@@ -38,7 +38,9 @@ export const config: AppConfig = {
nodeEnv: process.env.NODE_ENV || 'development', nodeEnv: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info', logLevel: process.env.LOG_LEVEL || 'info',
rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS!, 10) || 60000, rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS!, 10) || 60000,
rateLimitMaxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS!, 10) || 30 rateLimitMaxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS!, 10) || 30,
}; };
logger.info('Environment variables loaded', { config: { ...config, botToken: config.botToken?.substring(0, 10) + '...' } }); logger.info('Environment variables loaded', {
config: { ...config, botToken: `${config.botToken?.substring(0, 10)}...` },
});
+10 -10
View File
@@ -1,28 +1,28 @@
import { serve } from 'bun'; import { serve } from 'bun';
import logger from './utils/logger';
import { config } from './env';
import { startBot } from './bot'; import { startBot } from './bot';
import { handleUpload } from './routes/upload'; import { config } from './env';
import { handleFileRedirect, handleFileInfo } from './routes/files'; import { handleFileInfo, handleFileRedirect } from './routes/files';
import { handleHealth } from './routes/health'; import { handleHealth } from './routes/health';
import { handleUpload } from './routes/upload';
import logger from './utils/logger';
import { cleanupRateLimitCache } from './utils/rateLimit'; import { cleanupRateLimitCache } from './utils/rateLimit';
const server = serve({ const server = serve({
port: config.port, port: config.port,
routes: { routes: {
'/api/upload': { '/api/upload': {
POST: handleUpload POST: handleUpload,
}, },
'/f/:public_id': { '/f/:public_id': {
GET: handleFileRedirect GET: handleFileRedirect,
}, },
'/file/:public_id/info': { '/file/:public_id/info': {
GET: handleFileInfo GET: handleFileInfo,
}, },
'/health': { '/health': {
GET: handleHealth GET: handleHealth,
} },
} },
}); });
const bot = await startBot(); const bot = await startBot();
+30 -16
View File
@@ -1,7 +1,7 @@
import logger from '../utils/logger';
import { db, files as fileSchema } from '../db';
import { checkRateLimit } from '../utils/rateLimit';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db, files as fileSchema } from '../db';
import logger from '../utils/logger';
import { checkRateLimit } from '../utils/rateLimit';
type RequestWithParams = Request & { type RequestWithParams = Request & {
params?: { params?: {
@@ -18,7 +18,11 @@ export const handleFileRedirect = async (req: RequestWithParams): Promise<Respon
return Response.json({ error: 'Rate limit exceeded' }, { status: 429 }); return Response.json({ error: 'Rate limit exceeded' }, { status: 429 });
} }
const result = await db.select().from(fileSchema).where(eq(fileSchema.publicId, public_id)).limit(1); const result = await db
.select()
.from(fileSchema)
.where(eq(fileSchema.publicId, public_id))
.limit(1);
if (!result.length) { if (!result.length) {
logger.warn('File not found', { public_id }); logger.warn('File not found', { public_id });
@@ -34,8 +38,8 @@ export const handleFileRedirect = async (req: RequestWithParams): Promise<Respon
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { headers: {
'Location': redirectUrl Location: redirectUrl,
} },
}); });
} catch (error: any) { } catch (error: any) {
logger.error('File redirect error', { public_id, error: error.message }); logger.error('File redirect error', { public_id, error: error.message });
@@ -50,7 +54,11 @@ export const handleFileInfo = async (req: RequestWithParams): Promise<Response>
return Response.json({ error: 'Missing file id' }, { status: 400 }); return Response.json({ error: 'Missing file id' }, { status: 400 });
} }
const result = await db.select().from(fileSchema).where(eq(fileSchema.publicId, public_id)).limit(1); const result = await db
.select()
.from(fileSchema)
.where(eq(fileSchema.publicId, public_id))
.limit(1);
if (!result.length) { if (!result.length) {
logger.warn('File not found', { public_id }); logger.warn('File not found', { public_id });
@@ -58,15 +66,21 @@ export const handleFileInfo = async (req: RequestWithParams): Promise<Response>
} }
const file = result[0]; const file = result[0];
return Response.json({ return Response.json(
public_id: file.publicId, {
file_name: file.fileName, public_id: file.publicId,
mime_type: file.mimeType, file_name: file.fileName,
size_bytes: file.sizeBytes, mime_type: file.mimeType,
file_type: file.fileType, size_bytes: file.sizeBytes,
uploader_id: file.uploaderId, file_type: file.fileType,
created_at: typeof file.createdAt === 'string' ? file.createdAt : (file.createdAt as Date).toISOString() uploader_id: file.uploaderId,
}, { status: 200 }); created_at:
typeof file.createdAt === 'string'
? file.createdAt
: (file.createdAt as Date).toISOString(),
},
{ status: 200 },
);
} catch (error: any) { } catch (error: any) {
logger.error('File info error', { public_id, error: error.message }); logger.error('File info error', { public_id, error: error.message });
return Response.json({ error: 'Server error' }, { status: 500 }); return Response.json({ error: 'Server error' }, { status: 500 });
+2 -2
View File
@@ -1,6 +1,6 @@
import logger from '../utils/logger';
import { db } from '../db';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { db } from '../db';
import logger from '../utils/logger';
export const handleHealth = async (_req: Request): Promise<Response> => { export const handleHealth = async (_req: Request): Promise<Response> => {
try { try {
+24 -14
View File
@@ -1,9 +1,9 @@
import logger from '../utils/logger';
import { db, files as fileSchema } from '../db';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { forwardToStorage, getBot } from '../utils/telegram'; import { db, files as fileSchema } from '../db';
import { getFileType, checkFileSize, extractMimeType } from '../utils/file';
import { config } from '../env'; import { config } from '../env';
import { checkFileSize, extractMimeType, getFileType } from '../utils/file';
import logger from '../utils/logger';
import { forwardToStorage, getBot } from '../utils/telegram';
export const handleUpload = async (req: Request): Promise<Response> => { export const handleUpload = async (req: Request): Promise<Response> => {
try { try {
@@ -17,7 +17,7 @@ export const handleUpload = async (req: Request): Promise<Response> => {
return Response.json( return Response.json(
{ error: 'Unsupported content type. Use multipart/form-data or application/json' }, { error: 'Unsupported content type. Use multipart/form-data or application/json' },
{ status: 400 } { status: 400 },
); );
} catch (error: any) { } catch (error: any) {
logger.error('Upload error', { error: error.message }); logger.error('Upload error', { error: error.message });
@@ -29,7 +29,8 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
try { try {
const formData = await req.formData(); const formData = await req.formData();
const file = formData.get('file'); const file = formData.get('file');
const fileName = (formData.get('fileName') as string) || (file instanceof File ? file.name : null) || 'file'; const fileName =
(formData.get('fileName') as string) || (file instanceof File ? file.name : null) || 'file';
if (!file || !(file instanceof File)) { if (!file || !(file instanceof File)) {
return Response.json({ error: 'No file provided' }, { status: 400 }); return Response.json({ error: 'No file provided' }, { status: 400 });
@@ -44,7 +45,10 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 }); return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
} }
const isDocument = fileName.endsWith('.pdf') || fileName.endsWith('.txt') || !['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType); const isDocument =
fileName.endsWith('.pdf') ||
fileName.endsWith('.txt') ||
!['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType);
const result = await forwardToStorage(fileBuffer, fileName, isDocument); const result = await forwardToStorage(fileBuffer, fileName, isDocument);
const bot = getBot(); const bot = getBot();
const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any; const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any;
@@ -61,7 +65,7 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
fileType: fileType, fileType: fileType,
uploaderId: 0, uploaderId: 0,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
}; };
await db.insert(fileSchema).values(uploaded); await db.insert(fileSchema).values(uploaded);
@@ -78,7 +82,7 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
file_type: uploaded.fileType, file_type: uploaded.fileType,
uploader_id: uploaded.uploaderId, uploader_id: uploaded.uploaderId,
created_at: uploaded.createdAt.toISOString(), created_at: uploaded.createdAt.toISOString(),
download_url: `${config.baseUrl}/f/${uploaded.publicId}` download_url: `${config.baseUrl}/f/${uploaded.publicId}`,
}; };
return Response.json(responsePayload, { status: 200 }); return Response.json(responsePayload, { status: 200 });
@@ -95,19 +99,25 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
if (!file || typeof file !== 'string') { if (!file || typeof file !== 'string') {
return Response.json( return Response.json(
{ error: 'Invalid JSON. Must include "file" (base64) and optional "fileName"' }, { error: 'Invalid JSON. Must include "file" (base64) and optional "fileName"' },
{ status: 400 } { status: 400 },
); );
} }
const fileBytes = Buffer.from(file, 'base64'); const fileBytes = Buffer.from(file, 'base64');
const mimeType = 'application/octet-stream'; const mimeType = 'application/octet-stream';
const fileType = getFileType(mimeType, fileName) === 'application' ? 'document' : getFileType(mimeType, fileName); const fileType =
getFileType(mimeType, fileName) === 'application'
? 'document'
: getFileType(mimeType, fileName);
if (!checkFileSize(fileBytes.byteLength, fileType)) { if (!checkFileSize(fileBytes.byteLength, fileType)) {
return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 }); return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
} }
const isDocument = fileName.endsWith('.pdf') || fileName.endsWith('.txt') || !['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType); const isDocument =
fileName.endsWith('.pdf') ||
fileName.endsWith('.txt') ||
!['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType);
const result = await forwardToStorage(fileBytes, fileName, isDocument); const result = await forwardToStorage(fileBytes, fileName, isDocument);
const bot = getBot(); const bot = getBot();
const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any; const fileInfo = (await bot.telegram.getFile(result.telegramFileId)) as any;
@@ -124,7 +134,7 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
fileType: fileType, fileType: fileType,
uploaderId: 0, uploaderId: 0,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
}; };
await db.insert(fileSchema).values(uploaded); await db.insert(fileSchema).values(uploaded);
@@ -141,7 +151,7 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
file_type: uploaded.fileType, file_type: uploaded.fileType,
uploader_id: uploaded.uploaderId, uploader_id: uploaded.uploaderId,
created_at: uploaded.createdAt.toISOString(), created_at: uploaded.createdAt.toISOString(),
download_url: `${config.baseUrl}/f/${uploaded.publicId}` download_url: `${config.baseUrl}/f/${uploaded.publicId}`,
}; };
return Response.json(responsePayload, { status: 200 }); return Response.json(responsePayload, { status: 200 });
+17 -5
View File
@@ -4,7 +4,7 @@ const FILE_TYPES: Record<string, number> = {
video: 2 * 1024 * 1024 * 1024, // 2GB video: 2 * 1024 * 1024 * 1024, // 2GB
audio: 200 * 1024 * 1024, // 200MB audio: 200 * 1024 * 1024, // 200MB
voice: 200 * 1024 * 1024, // 200MB voice: 200 * 1024 * 1024, // 200MB
animation: 2 * 1024 * 1024 * 1024 // 2GB animation: 2 * 1024 * 1024 * 1024, // 2GB
}; };
export const getFileType = (mime: string | null, caption?: string): string => { export const getFileType = (mime: string | null, caption?: string): string => {
@@ -30,14 +30,26 @@ export const extractFileName = (msg: any, request: any): string => {
if (request?.headers?.['x-file-name']) { if (request?.headers?.['x-file-name']) {
return request.headers['x-file-name']; return request.headers['x-file-name'];
} }
return msg.document?.fileName || msg.photo?.slice(-1)[0]?.fileName || msg.audio?.fileName || return (
msg.voice?.fileName || msg.animation?.fileName || 'file'; msg.document?.fileName ||
msg.photo?.slice(-1)[0]?.fileName ||
msg.audio?.fileName ||
msg.voice?.fileName ||
msg.animation?.fileName ||
'file'
);
}; };
export const extractMimeType = (msg: any, request: any): string => { export const extractMimeType = (msg: any, request: any): string => {
if (request?.headers?.['x-mime-type']) { if (request?.headers?.['x-mime-type']) {
return request.headers['x-mime-type']; return request.headers['x-mime-type'];
} }
return msg.document?.mimeType || msg.photo?.slice(-1)[0]?.mimeType || msg.audio?.mimeType || return (
msg.voice?.mimeType || msg.animation?.mimeType || 'application/octet-stream'; msg.document?.mimeType ||
msg.photo?.slice(-1)[0]?.mimeType ||
msg.audio?.mimeType ||
msg.voice?.mimeType ||
msg.animation?.mimeType ||
'application/octet-stream'
);
}; };
+8 -9
View File
@@ -5,24 +5,23 @@ const logger = winston.createLogger({
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
winston.format.errors({ stack: true }), winston.format.errors({ stack: true }),
winston.format.json() winston.format.json(),
), ),
defaultMeta: { service: 'teleuploader' }, defaultMeta: { service: 'teleuploader' },
transports: [ transports: [
// Write all logs including error logs to file // Write all logs including error logs to file
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }) new winston.transports.File({ filename: 'logs/combined.log' }),
] ],
}); });
// If not production, also log to console // If not production, also log to console
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({ logger.add(
format: winston.format.combine( new winston.transports.Console({
winston.format.colorize(), format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
winston.format.simple() }),
) );
}));
} }
export default logger; export default logger;
+9 -7
View File
@@ -1,6 +1,6 @@
import { Telegraf } from 'telegraf'; import { Telegraf } from 'telegraf';
import logger from './logger';
import { config } from '../env'; import { config } from '../env';
import logger from './logger';
const bot = new Telegraf(config.botToken); const bot = new Telegraf(config.botToken);
const TELEGRAM_API_URL = `https://api.telegram.org/bot${config.botToken}/`; const TELEGRAM_API_URL = `https://api.telegram.org/bot${config.botToken}/`;
@@ -20,11 +20,13 @@ interface TelegramFileInfo {
export const forwardToStorage = async ( export const forwardToStorage = async (
fileChunk: any, fileChunk: any,
fileName: string, fileName: string,
forceDocument = false forceDocument = false,
): Promise<ForwardResult> => { ): Promise<ForwardResult> => {
try { try {
const caption = forceDocument ? `📁 ${fileName}` : fileName; const caption = forceDocument ? `📁 ${fileName}` : fileName;
const input: any = forceDocument ? { document: fileChunk, caption } : { photo: [fileChunk], caption }; const input: any = forceDocument
? { document: fileChunk, caption }
: { photo: [fileChunk], caption };
const result = await bot.telegram.sendPhoto(config.storageChatId, input); const result = await bot.telegram.sendPhoto(config.storageChatId, input);
@@ -33,7 +35,7 @@ export const forwardToStorage = async (
return { return {
telegramFileId: result.photo?.slice(-1)[0]?.file_id || '', telegramFileId: result.photo?.slice(-1)[0]?.file_id || '',
telegramFileUniqueId: result.photo?.slice(-1)[0]?.file_unique_id || '', telegramFileUniqueId: result.photo?.slice(-1)[0]?.file_unique_id || '',
storageMessageId: result.message_id storageMessageId: result.message_id,
}; };
} catch (error: any) { } catch (error: any) {
logger.error('Failed to forward file to storage', { fileName, error: error.message }); logger.error('Failed to forward file to storage', { fileName, error: error.message });
@@ -43,7 +45,7 @@ export const forwardToStorage = async (
export const getFileInfo = async ( export const getFileInfo = async (
telegramFileId: string, telegramFileId: string,
telegramFileUniqueId: string telegramFileUniqueId: string,
): Promise<TelegramFileInfo> => { ): Promise<TelegramFileInfo> => {
try { try {
const result = await fetch(`${TELEGRAM_API_URL}getFile`); const result = await fetch(`${TELEGRAM_API_URL}getFile`);
@@ -57,7 +59,7 @@ export const getFileInfo = async (
const fileResult = await fetch(`${TELEGRAM_API_URL}getInfo`, { const fileResult = await fetch(`${TELEGRAM_API_URL}getInfo`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_id: fileId }) body: JSON.stringify({ file_id: fileId }),
}); });
const fileInfo: any = await fileResult.json(); const fileInfo: any = await fileResult.json();
@@ -68,7 +70,7 @@ export const getFileInfo = async (
return { return {
file_size: fileInfo.result.file_size, file_size: fileInfo.result.file_size,
mime_type: fileInfo.result.mime_type, mime_type: fileInfo.result.mime_type,
file_path: fileInfo.result.file_path file_path: fileInfo.result.file_path,
}; };
} catch (error: any) { } catch (error: any) {
logger.error('Failed to get file info', { error: error.message }); logger.error('Failed to get file info', { error: error.message });
+27 -25
View File
@@ -1,43 +1,45 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test"; import { afterAll, beforeEach, describe, expect, it, mock } from 'bun:test';
const mockServe = mock((options) => { const mockServe = mock((options) => {
return { return {
port: options.port, port: options.port,
routes: options.routes, routes: options.routes,
stop: mock() stop: mock(),
}; };
}); });
const originalServe = Bun.serve; const originalServe = Bun.serve;
Bun.serve = mockServe; Bun.serve = mockServe;
const mockStartBot = mock(() => Promise.resolve({ const mockStartBot = mock(() =>
stop: mock() Promise.resolve({
stop: mock(),
}),
);
mock.module('../src/bot', () => ({
startBot: mockStartBot,
})); }));
mock.module("../src/bot", () => ({ mock.module('../src/routes/upload', () => ({
startBot: mockStartBot handleUpload: mock(),
})); }));
mock.module("../src/routes/upload", () => ({ mock.module('../src/routes/files', () => ({
handleUpload: mock()
}));
mock.module("../src/routes/files", () => ({
handleFileRedirect: mock(), handleFileRedirect: mock(),
handleFileInfo: mock() handleFileInfo: mock(),
})); }));
mock.module("../src/routes/health", () => ({ mock.module('../src/routes/health', () => ({
handleHealth: mock() handleHealth: mock(),
})); }));
mock.module("../src/utils/rateLimit", () => ({ mock.module('../src/utils/rateLimit', () => ({
cleanupRateLimitCache: mock() cleanupRateLimitCache: mock(),
})); }));
describe("Bootstrap Server", () => { describe('Bootstrap Server', () => {
beforeEach(() => { beforeEach(() => {
mockServe.mockClear(); mockServe.mockClear();
mockStartBot.mockClear(); mockStartBot.mockClear();
@@ -47,18 +49,18 @@ describe("Bootstrap Server", () => {
Bun.serve = originalServe; Bun.serve = originalServe;
}); });
it("should bootstrap the application successfully", async () => { it('should bootstrap the application successfully', async () => {
await import("../src/index"); await import('../src/index');
expect(mockServe).toHaveBeenCalled(); expect(mockServe).toHaveBeenCalled();
expect(mockStartBot).toHaveBeenCalled(); expect(mockStartBot).toHaveBeenCalled();
const serveCallArgs = mockServe.mock.calls[0][0]; const serveCallArgs = mockServe.mock.calls[0][0];
expect(serveCallArgs).toHaveProperty("port"); expect(serveCallArgs).toHaveProperty('port');
expect(serveCallArgs).toHaveProperty("routes"); expect(serveCallArgs).toHaveProperty('routes');
expect(serveCallArgs.routes).toHaveProperty("/api/upload"); expect(serveCallArgs.routes).toHaveProperty('/api/upload');
expect(serveCallArgs.routes).toHaveProperty("/f/:public_id"); expect(serveCallArgs.routes).toHaveProperty('/f/:public_id');
expect(serveCallArgs.routes).toHaveProperty("/file/:public_id/info"); expect(serveCallArgs.routes).toHaveProperty('/file/:public_id/info');
expect(serveCallArgs.routes).toHaveProperty("/health"); expect(serveCallArgs.routes).toHaveProperty('/health');
}); });
}); });
+60 -53
View File
@@ -1,11 +1,11 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect, mock, spyOn, beforeEach, afterAll } from "bun:test"; import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import logger from "../src/utils/logger"; import logger from '../src/utils/logger';
// Mock environment // Mock environment
process.env.BOT_TOKEN = "8605908810:AAFpUzlIBktfd_7wpEj7zMJob2CFxvG-ZGY"; process.env.BOT_TOKEN = '8605908810:AAFpUzlIBktfd_7wpEj7zMJob2CFxvG-ZGY';
process.env.STORAGE_CHANNEL_ID = "-1003996572954"; process.env.STORAGE_CHANNEL_ID = '-1003996572954';
process.env.BASE_URL = "https://tele.asepharyana.tech"; process.env.BASE_URL = 'https://tele.asepharyana.tech';
// Mock Telegraf // Mock Telegraf
const mockLaunch = mock(() => Promise.resolve()); const mockLaunch = mock(() => Promise.resolve());
@@ -13,7 +13,7 @@ const mockCommand = mock();
const mockOn = mock(); const mockOn = mock();
const mockUse = mock(); const mockUse = mock();
mock.module("telegraf", () => { mock.module('telegraf', () => {
return { return {
Telegraf: class { Telegraf: class {
constructor(token) { constructor(token) {
@@ -23,35 +23,37 @@ mock.module("telegraf", () => {
this.on = mockOn; this.on = mockOn;
this.use = mockUse; this.use = mockUse;
} }
} },
}; };
}); });
// Mock database // Mock database
const mockInsert = mock(() => ({ const mockInsert = mock(() => ({
values: mock(() => Promise.resolve()) values: mock(() => Promise.resolve()),
})); }));
mock.module("../src/db/index", () => ({ mock.module('../src/db/index', () => ({
db: { db: {
insert: mockInsert insert: mockInsert,
}, },
files: {} files: {},
})); }));
// Mock forwardToStorage // Mock forwardToStorage
const mockForwardToStorage = mock(() => Promise.resolve({ const mockForwardToStorage = mock(() =>
telegramFileId: "stored_file_id", Promise.resolve({
telegramFileUniqueId: "stored_unique_id", telegramFileId: 'stored_file_id',
storageMessageId: 9999 telegramFileUniqueId: 'stored_unique_id',
})); storageMessageId: 9999,
mock.module("../src/utils/telegram", () => ({ }),
forwardToStorage: mockForwardToStorage );
mock.module('../src/utils/telegram', () => ({
forwardToStorage: mockForwardToStorage,
})); }));
const infoSpy = spyOn(logger, "info"); const infoSpy = spyOn(logger, 'info');
const errorSpy = spyOn(logger, "error"); const errorSpy = spyOn(logger, 'error');
describe("Telegram Bot Handler", () => { describe('Telegram Bot Handler', () => {
beforeEach(() => { beforeEach(() => {
mockLaunch.mockClear(); mockLaunch.mockClear();
mockCommand.mockClear(); mockCommand.mockClear();
@@ -63,36 +65,36 @@ describe("Telegram Bot Handler", () => {
errorSpy.mockClear(); errorSpy.mockClear();
}); });
it("should initialize and launch the bot", async () => { it('should initialize and launch the bot', async () => {
const { startBot } = await import("../src/bot"); const { startBot } = await import('../src/bot');
const bot = await startBot(); const bot = await startBot();
expect(bot).toBeDefined(); expect(bot).toBeDefined();
expect(mockCommand).toHaveBeenCalledWith("start", expect.any(Function)); expect(mockCommand).toHaveBeenCalledWith('start', expect.any(Function));
expect(mockOn).toHaveBeenCalledWith( expect(mockOn).toHaveBeenCalledWith(
["document", "photo", "video", "audio", "voice", "animation"], ['document', 'photo', 'video', 'audio', 'voice', 'animation'],
expect.any(Function) expect.any(Function),
); );
expect(mockUse).toHaveBeenCalled(); expect(mockUse).toHaveBeenCalled();
expect(mockLaunch).toHaveBeenCalled(); expect(mockLaunch).toHaveBeenCalled();
}); });
it("should handle /start command", async () => { it('should handle /start command', async () => {
const { startBot } = await import("../src/bot"); const { startBot } = await import('../src/bot');
await startBot(); await startBot();
const startHandler = mockCommand.mock.calls.find(call => call[0] === "start")[1]; const startHandler = mockCommand.mock.calls.find((call) => call[0] === 'start')[1];
const replyMock = mock(() => Promise.resolve()); const replyMock = mock(() => Promise.resolve());
const ctx = { const ctx = {
reply: replyMock reply: replyMock,
}; };
await startHandler(ctx); await startHandler(ctx);
expect(replyMock).toHaveBeenCalledWith(expect.stringContaining("Halo")); expect(replyMock).toHaveBeenCalledWith(expect.stringContaining('Halo'));
}); });
it("should process document uploads and save to db", async () => { it('should process document uploads and save to db', async () => {
const { startBot } = await import("../src/bot"); const { startBot } = await import('../src/bot');
await startBot(); await startBot();
const fileHandler = mockOn.mock.calls[0][1]; const fileHandler = mockOn.mock.calls[0][1];
@@ -101,27 +103,30 @@ describe("Telegram Bot Handler", () => {
message: { message: {
message_id: 42, message_id: 42,
document: { document: {
file_id: "doc_123", file_id: 'doc_123',
file_unique_id: "doc_uniq_123", file_unique_id: 'doc_uniq_123',
file_size: 1024, file_size: 1024,
mime_type: "application/pdf", mime_type: 'application/pdf',
file_name: "cv.pdf" file_name: 'cv.pdf',
} },
}, },
from: { from: {
id: 999 id: 999,
}, },
reply: replyMock reply: replyMock,
}; };
await fileHandler(ctx); await fileHandler(ctx);
expect(mockForwardToStorage).toHaveBeenCalledWith("doc_123", "cv.pdf"); expect(mockForwardToStorage).toHaveBeenCalledWith('doc_123', 'cv.pdf');
expect(mockInsert).toHaveBeenCalled(); expect(mockInsert).toHaveBeenCalled();
expect(replyMock).toHaveBeenCalledWith(expect.stringContaining("File berhasil diupload"), expect.any(Object)); expect(replyMock).toHaveBeenCalledWith(
expect.stringContaining('File berhasil diupload'),
expect.any(Object),
);
}); });
it("should reject uploads exceeding max size limit", async () => { it('should reject uploads exceeding max size limit', async () => {
const { startBot } = await import("../src/bot"); const { startBot } = await import('../src/bot');
await startBot(); await startBot();
const fileHandler = mockOn.mock.calls[0][1]; const fileHandler = mockOn.mock.calls[0][1];
@@ -129,22 +134,24 @@ describe("Telegram Bot Handler", () => {
const ctx = { const ctx = {
message: { message: {
message_id: 42, message_id: 42,
photo: [{ photo: [
file_id: "photo_123", {
file_unique_id: "photo_uniq_123", file_id: 'photo_123',
file_size: 20 * 1024 * 1024, // 20MB exceeds 10MB limit file_unique_id: 'photo_uniq_123',
mime_type: "image/jpeg" file_size: 20 * 1024 * 1024, // 20MB exceeds 10MB limit
}] mime_type: 'image/jpeg',
},
],
}, },
from: { from: {
id: 999 id: 999,
}, },
reply: replyMock reply: replyMock,
}; };
await fileHandler(ctx); await fileHandler(ctx);
expect(mockForwardToStorage).not.toHaveBeenCalled(); expect(mockForwardToStorage).not.toHaveBeenCalled();
expect(replyMock).toHaveBeenCalledWith(expect.stringContaining("exceeds")); expect(replyMock).toHaveBeenCalledWith(expect.stringContaining('exceeds'));
}); });
afterAll(() => { afterAll(() => {
+7 -7
View File
@@ -1,20 +1,20 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect } from "bun:test"; import { describe, expect, it } from 'bun:test';
import { db, files } from "../src/db/index"; import { db, files } from '../src/db/index';
import { files as schemaFiles } from "../src/db/schema"; import { files as schemaFiles } from '../src/db/schema';
describe("Database Layer", () => { describe('Database Layer', () => {
it("should export db instance", () => { it('should export db instance', () => {
expect(db).toBeDefined(); expect(db).toBeDefined();
}); });
it("should export files schema from both index and schema", () => { it('should export files schema from both index and schema', () => {
expect(files).toBeDefined(); expect(files).toBeDefined();
expect(schemaFiles).toBeDefined(); expect(schemaFiles).toBeDefined();
expect(files).toBe(schemaFiles); expect(files).toBe(schemaFiles);
}); });
it("should have correct schema properties", () => { it('should have correct schema properties', () => {
expect(files.id).toBeDefined(); expect(files.id).toBeDefined();
expect(files.publicId).toBeDefined(); expect(files.publicId).toBeDefined();
expect(files.telegramFileId).toBeDefined(); expect(files.telegramFileId).toBeDefined();
+22 -22
View File
@@ -1,46 +1,46 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect, beforeAll } from "bun:test"; import { describe, expect, it } from 'bun:test';
import { config } from "../src/env"; import { config } from '../src/env';
describe("Environment Variables Validation", () => { describe('Environment Variables Validation', () => {
it("config should have all required fields", () => { it('config should have all required fields', () => {
expect(config).toHaveProperty("botToken"); expect(config).toHaveProperty('botToken');
expect(config).toHaveProperty("storageChatId"); expect(config).toHaveProperty('storageChatId');
expect(config).toHaveProperty("baseUrl"); expect(config).toHaveProperty('baseUrl');
expect(config).toHaveProperty("databaseUrl"); expect(config).toHaveProperty('databaseUrl');
expect(config).toHaveProperty("port"); expect(config).toHaveProperty('port');
expect(config).toHaveProperty("nodeEnv"); expect(config).toHaveProperty('nodeEnv');
expect(config).toHaveProperty("logLevel"); expect(config).toHaveProperty('logLevel');
expect(config).toHaveProperty("rateLimitWindowMs"); expect(config).toHaveProperty('rateLimitWindowMs');
expect(config).toHaveProperty("rateLimitMaxRequests"); expect(config).toHaveProperty('rateLimitMaxRequests');
}); });
it("config.botToken should return BOT_TOKEN from process.env", () => { 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", () => { it('config.storageChatId should be parsed as integer from STORAGE_CHANNEL_ID', () => {
expect(typeof config.storageChatId).toBe("number"); 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, 10));
}); });
it("config.port should default to 3000 when not specified", () => { it('config.port should default to 3000 when not specified', () => {
expect(typeof config.port).toBe("number"); expect(typeof config.port).toBe('number');
}); });
it("nodeEnv should be 'test' or 'development'", () => { it("nodeEnv should be 'test' or 'development'", () => {
expect(["test", "development"]).toContain(config.nodeEnv); expect(['test', 'development']).toContain(config.nodeEnv);
}); });
it("logLevel should default to 'info'", () => { it("logLevel should default to 'info'", () => {
expect(config.logLevel).toBe("info"); expect(config.logLevel).toBe('info');
}); });
it("rateLimitWindowMs should default to 60000 when not specified", () => { it('rateLimitWindowMs should default to 60000 when not specified', () => {
expect(config.rateLimitWindowMs).toBe(60000); expect(config.rateLimitWindowMs).toBe(60000);
}); });
it("rateLimitMaxRequests should default to 30 when not specified", () => { it('rateLimitMaxRequests should default to 30 when not specified', () => {
expect(config.rateLimitMaxRequests).toBe(30); expect(config.rateLimitMaxRequests).toBe(30);
}); });
}); });
+61 -55
View File
@@ -1,89 +1,95 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect } from "bun:test"; import { describe, expect, it } from 'bun:test';
import { getFileType, checkFileSize, extractFileName, extractMimeType } from "../src/utils/file"; import { checkFileSize, extractFileName, extractMimeType, getFileType } from '../src/utils/file';
describe("File Utilities", () => { describe('File Utilities', () => {
describe("getFileType", () => { describe('getFileType', () => {
it("should classify video mime types as video", () => { it('should classify video mime types as video', () => {
expect(getFileType("video/mp4", "")).toBe("video"); expect(getFileType('video/mp4', '')).toBe('video');
expect(getFileType("video/quicktime", "")).toBe("video"); expect(getFileType('video/quicktime', '')).toBe('video');
}); });
it("should classify audio mime types as audio", () => { it('should classify audio mime types as audio', () => {
expect(getFileType("audio/mpeg", "")).toBe("audio"); expect(getFileType('audio/mpeg', '')).toBe('audio');
expect(getFileType("audio/ogg", "")).toBe("audio"); expect(getFileType('audio/ogg', '')).toBe('audio');
}); });
it("should classify image mime types based on caption", () => { it('should classify image mime types based on caption', () => {
expect(getFileType("image/jpeg", "my photo")).toBe("photo"); expect(getFileType('image/jpeg', 'my photo')).toBe('photo');
expect(getFileType("image/png", "cool image.png")).toBe("photo"); expect(getFileType('image/png', 'cool image.png')).toBe('photo');
expect(getFileType("image/gif", "funny.gif")).toBe("animation"); expect(getFileType('image/gif', 'funny.gif')).toBe('animation');
expect(getFileType("image/png", "funny gif")).toBe("animation"); expect(getFileType('image/png', 'funny gif')).toBe('animation');
}); });
it("should classify voice and animation based on caption", () => { it('should classify voice and animation based on caption', () => {
expect(getFileType("application/octet-stream", "this is a voice note")).toBe("voice"); expect(getFileType('application/octet-stream', 'this is a voice note')).toBe('voice');
expect(getFileType("application/octet-stream", "cool animation")).toBe("animation"); expect(getFileType('application/octet-stream', 'cool animation')).toBe('animation');
}); });
it("should default to mime first segment or document", () => { it('should default to mime first segment or document', () => {
expect(getFileType("application/pdf", "")).toBe("application"); expect(getFileType('application/pdf', '')).toBe('application');
expect(getFileType(null, "")).toBe("document"); expect(getFileType(null, '')).toBe('document');
}); });
}); });
describe("checkFileSize", () => { describe('checkFileSize', () => {
it("should allow files under the size limit", () => { it('should allow files under the size limit', () => {
expect(checkFileSize(5 * 1024 * 1024, "photo")).toBe(true); // Photo limit is 10MB expect(checkFileSize(5 * 1024 * 1024, 'photo')).toBe(true); // Photo limit is 10MB
expect(checkFileSize(1 * 1024 * 1024 * 1024, "video")).toBe(true); // Video limit is 2GB expect(checkFileSize(1 * 1024 * 1024 * 1024, 'video')).toBe(true); // Video limit is 2GB
}); });
it("should block files exceeding the size limit", () => { it('should block files exceeding the size limit', () => {
expect(checkFileSize(15 * 1024 * 1024, "photo")).toBe(false); // Photo limit is 10MB expect(checkFileSize(15 * 1024 * 1024, 'photo')).toBe(false); // Photo limit is 10MB
expect(checkFileSize(3 * 1024 * 1024 * 1024, "video")).toBe(false); // Video limit is 2GB expect(checkFileSize(3 * 1024 * 1024 * 1024, 'video')).toBe(false); // Video limit is 2GB
}); });
it("should fall back to document limit if fileType is unknown", () => { it('should fall back to document limit if fileType is unknown', () => {
expect(checkFileSize(1 * 1024 * 1024 * 1024, "unknown")).toBe(true); // Document limit is 2GB expect(checkFileSize(1 * 1024 * 1024 * 1024, 'unknown')).toBe(true); // Document limit is 2GB
expect(checkFileSize(3 * 1024 * 1024 * 1024, "unknown")).toBe(false); expect(checkFileSize(3 * 1024 * 1024 * 1024, 'unknown')).toBe(false);
}); });
}); });
describe("extractFileName", () => { describe('extractFileName', () => {
it("should extract file name from headers if present", () => { it('should extract file name from headers if present', () => {
const req = { headers: { "x-file-name": "custom.txt" } }; const req = { headers: { 'x-file-name': 'custom.txt' } };
expect(extractFileName({}, req)).toBe("custom.txt"); expect(extractFileName({}, req)).toBe('custom.txt');
}); });
it("should extract file name from various message attachment types", () => { it('should extract file name from various message attachment types', () => {
expect(extractFileName({ document: { fileName: "doc.pdf" } }, null)).toBe("doc.pdf"); expect(extractFileName({ document: { fileName: 'doc.pdf' } }, null)).toBe('doc.pdf');
expect(extractFileName({ photo: [{ fileName: "low.jpg" }, { fileName: "high.jpg" }] }, null)).toBe("high.jpg"); expect(
expect(extractFileName({ audio: { fileName: "song.mp3" } }, null)).toBe("song.mp3"); extractFileName({ photo: [{ fileName: 'low.jpg' }, { fileName: 'high.jpg' }] }, null),
expect(extractFileName({ voice: { fileName: "voice.ogg" } }, null)).toBe("voice.ogg"); ).toBe('high.jpg');
expect(extractFileName({ animation: { fileName: "anim.gif" } }, null)).toBe("anim.gif"); expect(extractFileName({ audio: { fileName: 'song.mp3' } }, null)).toBe('song.mp3');
expect(extractFileName({ voice: { fileName: 'voice.ogg' } }, null)).toBe('voice.ogg');
expect(extractFileName({ animation: { fileName: 'anim.gif' } }, null)).toBe('anim.gif');
}); });
it("should return default filename if not found", () => { it('should return default filename if not found', () => {
expect(extractFileName({}, null)).toBe("file"); expect(extractFileName({}, null)).toBe('file');
}); });
}); });
describe("extractMimeType", () => { describe('extractMimeType', () => {
it("should extract mime type from headers if present", () => { it('should extract mime type from headers if present', () => {
const req = { headers: { "x-mime-type": "text/plain" } }; const req = { headers: { 'x-mime-type': 'text/plain' } };
expect(extractMimeType({}, req)).toBe("text/plain"); expect(extractMimeType({}, req)).toBe('text/plain');
}); });
it("should extract mime type from various message attachment types", () => { it('should extract mime type from various message attachment types', () => {
expect(extractMimeType({ document: { mimeType: "application/pdf" } }, null)).toBe("application/pdf"); expect(extractMimeType({ document: { mimeType: 'application/pdf' } }, null)).toBe(
expect(extractMimeType({ photo: [{ mimeType: "image/jpeg" }, { mimeType: "image/png" }] }, null)).toBe("image/png"); 'application/pdf',
expect(extractMimeType({ audio: { mimeType: "audio/mpeg" } }, null)).toBe("audio/mpeg"); );
expect(extractMimeType({ voice: { mimeType: "audio/ogg" } }, null)).toBe("audio/ogg"); expect(
expect(extractMimeType({ animation: { mimeType: "video/mp4" } }, null)).toBe("video/mp4"); extractMimeType({ photo: [{ mimeType: 'image/jpeg' }, { mimeType: 'image/png' }] }, null),
).toBe('image/png');
expect(extractMimeType({ audio: { mimeType: 'audio/mpeg' } }, null)).toBe('audio/mpeg');
expect(extractMimeType({ voice: { mimeType: 'audio/ogg' } }, null)).toBe('audio/ogg');
expect(extractMimeType({ animation: { mimeType: 'video/mp4' } }, null)).toBe('video/mp4');
}); });
it("should return default mime type if not found", () => { it('should return default mime type if not found', () => {
expect(extractMimeType({}, null)).toBe("application/octet-stream"); expect(extractMimeType({}, null)).toBe('application/octet-stream');
}); });
}); });
}); });
+83 -78
View File
@@ -1,44 +1,44 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test"; import { afterAll, beforeEach, describe, expect, it, mock } from 'bun:test';
// Mock database layer // Mock database layer
const mockSelect = mock(() => ({ const mockSelect = mock(() => ({
from: mock(() => ({ from: mock(() => ({
where: mock(() => ({ where: mock(() => ({
limit: mock(() => Promise.resolve([])) limit: mock(() => Promise.resolve([])),
})) })),
})) })),
})); }));
mock.module("../src/db/index", () => ({ mock.module('../src/db/index', () => ({
db: { db: {
select: mockSelect select: mockSelect,
}, },
files: { files: {
publicId: { publicId: {
equals: (val) => ({ type: "equals", value: val }) equals: (val) => ({ type: 'equals', value: val }),
} },
} },
})); }));
// Mock telegram utils // Mock telegram utils
const mockGetFile = mock(() => Promise.resolve({ file_path: "photos/file_0.jpg" })); const mockGetFile = mock(() => Promise.resolve({ file_path: 'photos/file_0.jpg' }));
mock.module("../src/utils/telegram", () => ({ mock.module('../src/utils/telegram', () => ({
getBot: () => ({ getBot: () => ({
telegram: { telegram: {
getFile: mockGetFile getFile: mockGetFile,
} },
}) }),
})); }));
// Mock rateLimit // Mock rateLimit
const mockCheckRateLimit = mock(() => true); const mockCheckRateLimit = mock(() => true);
mock.module("../src/utils/rateLimit", () => ({ mock.module('../src/utils/rateLimit', () => ({
checkRateLimit: mockCheckRateLimit checkRateLimit: mockCheckRateLimit,
})); }));
describe("File Route Handlers", () => { describe('File Route Handlers', () => {
let handleFileRedirect, handleFileInfo; let handleFileRedirect: any, handleFileInfo: any;
beforeEach(async () => { beforeEach(async () => {
mockSelect.mockClear(); mockSelect.mockClear();
@@ -46,141 +46,146 @@ describe("File Route Handlers", () => {
mockCheckRateLimit.mockClear(); mockCheckRateLimit.mockClear();
// Set up mock token // Set up mock token
process.env.BOT_TOKEN = "123456:ABC-DEF"; process.env.BOT_TOKEN = '123456:ABC-DEF';
const filesRoute = await import("../src/routes/files"); const filesRoute = await import('../src/routes/files');
handleFileRedirect = filesRoute.handleFileRedirect; handleFileRedirect = filesRoute.handleFileRedirect;
handleFileInfo = filesRoute.handleFileInfo; handleFileInfo = filesRoute.handleFileInfo;
}); });
describe("handleFileRedirect", () => { describe('handleFileRedirect', () => {
it("should return 429 if rate limit is exceeded", async () => { it('should return 429 if rate limit is exceeded', async () => {
mockCheckRateLimit.mockImplementationOnce(() => false); mockCheckRateLimit.mockImplementationOnce(() => false);
const req = new Request("http://localhost:3000/f/test-id"); const req = new Request('http://localhost:3000/f/test-id');
req.params = { public_id: "test-id" }; req.params = { public_id: 'test-id' };
const res = await handleFileRedirect(req); const res = await handleFileRedirect(req);
expect(res.status).toBe(429); expect(res.status).toBe(429);
const body = await res.json(); const body = await res.json();
expect(body.error).toBe("Rate limit exceeded"); expect(body.error).toBe('Rate limit exceeded');
}); });
it("should return 404 if file is not found in database", async () => { it('should return 404 if file is not found in database', async () => {
mockSelect.mockImplementationOnce(() => ({ mockSelect.mockImplementationOnce(() => ({
from: () => ({ from: () => ({
where: () => ({ where: () => ({
limit: () => Promise.resolve([]) limit: () => Promise.resolve([]),
}) }),
}) }),
})); }));
const req = new Request("http://localhost:3000/f/missing-id"); const req = new Request('http://localhost:3000/f/missing-id');
req.params = { public_id: "missing-id" }; req.params = { public_id: 'missing-id' };
const res = await handleFileRedirect(req); const res = await handleFileRedirect(req);
expect(res.status).toBe(404); expect(res.status).toBe(404);
const body = await res.json(); const body = await res.json();
expect(body.error).toBe("File not found"); expect(body.error).toBe('File not found');
}); });
it("should redirect to telegram file url if file is found", async () => { it('should redirect to telegram file url if file is found', async () => {
mockSelect.mockImplementationOnce(() => ({ mockSelect.mockImplementationOnce(() => ({
from: () => ({ from: () => ({
where: () => ({ where: () => ({
limit: () => Promise.resolve([{ limit: () =>
id: "uuid-123", Promise.resolve([
publicId: "test-id", {
telegramFileId: "tg-file-id", id: 'uuid-123',
fileName: "test.jpg" publicId: 'test-id',
}]) telegramFileId: 'tg-file-id',
}) fileName: 'test.jpg',
}) },
]),
}),
}),
})); }));
const req = new Request("http://localhost:3000/f/test-id"); const req = new Request('http://localhost:3000/f/test-id');
req.params = { public_id: "test-id" }; req.params = { public_id: 'test-id' };
const res = await handleFileRedirect(req); const res = await handleFileRedirect(req);
expect(res.status).toBe(302); expect(res.status).toBe(302);
expect(res.headers.get("Location")).toBe("https://api.telegram.org/file/bot123456:ABC-DEF/photos/file_0.jpg"); expect(res.headers.get('Location')).toBe(
'https://api.telegram.org/file/bot123456:ABC-DEF/photos/file_0.jpg',
);
}); });
it("should return 500 on database or external errors", async () => { it('should return 500 on database or external errors', async () => {
mockSelect.mockImplementationOnce(() => { mockSelect.mockImplementationOnce(() => {
throw new Error("DB Connection Error"); throw new Error('DB Connection Error');
}); });
const req = new Request("http://localhost:3000/f/test-id"); const req = new Request('http://localhost:3000/f/test-id');
req.params = { public_id: "test-id" }; req.params = { public_id: 'test-id' };
const res = await handleFileRedirect(req); const res = await handleFileRedirect(req);
expect(res.status).toBe(500); expect(res.status).toBe(500);
const body = await res.json(); const body = await res.json();
expect(body.error).toBe("Server error"); expect(body.error).toBe('Server error');
}); });
}); });
describe("handleFileInfo", () => { describe('handleFileInfo', () => {
it("should return 404 if file is not found in database", async () => { it('should return 404 if file is not found in database', async () => {
mockSelect.mockImplementationOnce(() => ({ mockSelect.mockImplementationOnce(() => ({
from: () => ({ from: () => ({
where: () => ({ where: () => ({
limit: () => Promise.resolve([]) limit: () => Promise.resolve([]),
}) }),
}) }),
})); }));
const req = new Request("http://localhost:3000/file/missing-id/info"); const req = new Request('http://localhost:3000/file/missing-id/info');
req.params = { public_id: "missing-id" }; req.params = { public_id: 'missing-id' };
const res = await handleFileInfo(req); const res = await handleFileInfo(req);
expect(res.status).toBe(404); expect(res.status).toBe(404);
const body = await res.json(); const body = await res.json();
expect(body.error).toBe("File not found"); expect(body.error).toBe('File not found');
}); });
it("should return file info JSON if file is found", async () => { it('should return file info JSON if file is found', async () => {
const dbFile = { const dbFile = {
publicId: "test-id", publicId: 'test-id',
fileName: "image.png", fileName: 'image.png',
mimeType: "image/png", mimeType: 'image/png',
sizeBytes: 2048, sizeBytes: 2048,
fileType: "photo", fileType: 'photo',
uploaderId: 99999, uploaderId: 99999,
createdAt: new Date("2026-05-18T00:00:00.000Z") createdAt: new Date('2026-05-18T00:00:00.000Z'),
}; };
mockSelect.mockImplementationOnce(() => ({ mockSelect.mockImplementationOnce(() => ({
from: () => ({ from: () => ({
where: () => ({ where: () => ({
limit: () => Promise.resolve([dbFile]) limit: () => Promise.resolve([dbFile]),
}) }),
}) }),
})); }));
const req = new Request("http://localhost:3000/file/test-id/info"); const req = new Request('http://localhost:3000/file/test-id/info');
req.params = { public_id: "test-id" }; req.params = { public_id: 'test-id' };
const res = await handleFileInfo(req); const res = await handleFileInfo(req);
expect(res.status).toBe(200); expect(res.status).toBe(200);
const body = await res.json(); const body = await res.json();
expect(body).toEqual({ expect(body).toEqual({
public_id: "test-id", public_id: 'test-id',
file_name: "image.png", file_name: 'image.png',
mime_type: "image/png", mime_type: 'image/png',
size_bytes: 2048, size_bytes: 2048,
file_type: "photo", file_type: 'photo',
uploader_id: 99999, uploader_id: 99999,
created_at: "2026-05-18T00:00:00.000Z" created_at: '2026-05-18T00:00:00.000Z',
}); });
}); });
it("should return 500 on database or external errors", async () => { it('should return 500 on database or external errors', async () => {
mockSelect.mockImplementationOnce(() => { mockSelect.mockImplementationOnce(() => {
throw new Error("DB Connection Error"); throw new Error('DB Connection Error');
}); });
const req = new Request("http://localhost:3000/file/test-id/info"); const req = new Request('http://localhost:3000/file/test-id/info');
req.params = { public_id: "test-id" }; req.params = { public_id: 'test-id' };
const res = await handleFileInfo(req); const res = await handleFileInfo(req);
expect(res.status).toBe(500); expect(res.status).toBe(500);
const body = await res.json(); const body = await res.json();
expect(body.error).toBe("Server error"); expect(body.error).toBe('Server error');
}); });
}); });
+15 -15
View File
@@ -1,42 +1,42 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect, mock, beforeEach } from "bun:test"; import { beforeEach, describe, expect, it, mock } from 'bun:test';
// Mock database layer // Mock database layer
const mockExecute = mock(() => Promise.resolve()); const mockExecute = mock(() => Promise.resolve());
mock.module("../src/db/index", () => ({ mock.module('../src/db/index', () => ({
db: { db: {
execute: mockExecute execute: mockExecute,
} },
})); }));
describe("Health Route Handler", () => { describe('Health Route Handler', () => {
let handleHealth; let handleHealth: any;
beforeEach(async () => { beforeEach(async () => {
mockExecute.mockClear(); mockExecute.mockClear();
const healthRoute = await import("../src/routes/health"); const healthRoute = await import('../src/routes/health');
handleHealth = healthRoute.handleHealth; handleHealth = healthRoute.handleHealth;
}); });
it("should return status 200 and ok when DB is healthy", async () => { it('should return status 200 and ok when DB is healthy', async () => {
const req = new Request("http://localhost:3000/health"); const req = new Request('http://localhost:3000/health');
const res = await handleHealth(req); const res = await handleHealth(req);
expect(res.status).toBe(200); expect(res.status).toBe(200);
const body = await res.json(); const body = await res.json();
expect(body).toEqual({ status: "ok" }); expect(body).toEqual({ status: 'ok' });
expect(mockExecute).toHaveBeenCalled(); expect(mockExecute).toHaveBeenCalled();
}); });
it("should return status 500 and error details when DB health check fails", async () => { it('should return status 500 and error details when DB health check fails', async () => {
mockExecute.mockImplementationOnce(() => Promise.reject(new Error("DB Connection Failed"))); mockExecute.mockImplementationOnce(() => Promise.reject(new Error('DB Connection Failed')));
const req = new Request("http://localhost:3000/health"); const req = new Request('http://localhost:3000/health');
const res = await handleHealth(req); const res = await handleHealth(req);
expect(res.status).toBe(500); expect(res.status).toBe(500);
const body = await res.json(); const body = await res.json();
expect(body.status).toBe("error"); expect(body.status).toBe('error');
expect(body.error).toBe("DB Connection Failed"); expect(body.error).toBe('DB Connection Failed');
}); });
}); });
+17 -17
View File
@@ -1,17 +1,17 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"; import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test';
import { checkRateLimit, cleanupRateLimitCache } from "../src/utils/rateLimit"; import logger from '../src/utils/logger';
import logger from "../src/utils/logger"; import { checkRateLimit, cleanupRateLimitCache } from '../src/utils/rateLimit';
// Spy on logger.warn // Spy on logger.warn
const warnSpy = spyOn(logger, "warn"); const warnSpy = spyOn(logger, 'warn');
describe("Rate Limiter", () => { describe('Rate Limiter', () => {
beforeEach(() => { beforeEach(() => {
warnSpy.mockClear(); warnSpy.mockClear();
// Set custom env variables for predictable tests // Set custom env variables for predictable tests
process.env.RATE_LIMIT_WINDOW_MS = "100"; // 100ms window process.env.RATE_LIMIT_WINDOW_MS = '100'; // 100ms window
process.env.RATE_LIMIT_MAX_REQUESTS = "3"; // max 3 requests process.env.RATE_LIMIT_MAX_REQUESTS = '3'; // max 3 requests
}); });
afterEach(() => { afterEach(() => {
@@ -19,16 +19,16 @@ describe("Rate Limiter", () => {
delete process.env.RATE_LIMIT_MAX_REQUESTS; delete process.env.RATE_LIMIT_MAX_REQUESTS;
}); });
it("should allow requests under the limit", () => { it('should allow requests under the limit', () => {
const key = "user-1"; const key = 'user-1';
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
expect(warnSpy).not.toHaveBeenCalled(); expect(warnSpy).not.toHaveBeenCalled();
}); });
it("should block requests exceeding the limit and log a warning", () => { it('should block requests exceeding the limit and log a warning', () => {
const key = "user-2"; const key = 'user-2';
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
@@ -37,12 +37,12 @@ describe("Rate Limiter", () => {
expect(checkRateLimit(key)).toBe(false); expect(checkRateLimit(key)).toBe(false);
expect(warnSpy).toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalled();
const callArgs = warnSpy.mock.calls[0]; const callArgs = warnSpy.mock.calls[0];
expect(callArgs[0]).toBe("Rate limit exceeded"); expect(callArgs[0]).toBe('Rate limit exceeded');
expect(callArgs[1].key).toBe(key); expect(callArgs[1].key).toBe(key);
}); });
it("should reset request count after the window passes", async () => { it('should reset request count after the window passes', async () => {
const key = "user-3"; const key = 'user-3';
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
@@ -55,9 +55,9 @@ describe("Rate Limiter", () => {
expect(checkRateLimit(key)).toBe(true); expect(checkRateLimit(key)).toBe(true);
}); });
it("should cleanup rate limit cache of expired keys", async () => { it('should cleanup rate limit cache of expired keys', async () => {
const key1 = "cleanup-1"; const key1 = 'cleanup-1';
const key2 = "cleanup-2"; const key2 = 'cleanup-2';
// Populate keys // Populate keys
expect(checkRateLimit(key1)).toBe(true); expect(checkRateLimit(key1)).toBe(true);
+81 -62
View File
@@ -1,33 +1,34 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"; import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import logger from "../src/utils/logger"; import logger from '../src/utils/logger';
import { config } from "../src/env";
// Mock Telegraf and fetch // Mock Telegraf and fetch
mock.module("telegraf", () => { mock.module('telegraf', () => {
return { return {
Telegraf: class { Telegraf: class {
constructor(token) { constructor(token) {
this.token = token; this.token = token;
this.telegram = { this.telegram = {
sendPhoto: mock(() => Promise.resolve({ sendPhoto: mock(() =>
message_id: 12345, Promise.resolve({
photo: [ message_id: 12345,
{ file_id: "photo_id_low", file_unique_id: "unique_id_low" }, photo: [
{ file_id: "photo_id_high", file_unique_id: "unique_id_high" } { file_id: 'photo_id_low', file_unique_id: 'unique_id_low' },
] { file_id: 'photo_id_high', file_unique_id: 'unique_id_high' },
})) ],
}),
),
}; };
} }
} },
}; };
}); });
const infoSpy = spyOn(logger, "info"); const infoSpy = spyOn(logger, 'info');
const errorSpy = spyOn(logger, "error"); const errorSpy = spyOn(logger, 'error');
describe("Telegram API Utilities", () => { describe('Telegram API Utilities', () => {
let forwardToStorage, getFileInfo, getBot; let forwardToStorage: any, getFileInfo: any, getBot: any;
beforeEach(async () => { beforeEach(async () => {
infoSpy.mockClear(); infoSpy.mockClear();
@@ -35,7 +36,7 @@ describe("Telegram API Utilities", () => {
global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({ ok: true })))); global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({ ok: true }))));
// Import dynamically so mocking is applied first // Import dynamically so mocking is applied first
const telegramUtils = await import("../src/utils/telegram"); const telegramUtils = await import('../src/utils/telegram');
forwardToStorage = telegramUtils.forwardToStorage; forwardToStorage = telegramUtils.forwardToStorage;
getFileInfo = telegramUtils.getFileInfo; getFileInfo = telegramUtils.getFileInfo;
getBot = telegramUtils.getBot; getBot = telegramUtils.getBot;
@@ -45,83 +46,101 @@ describe("Telegram API Utilities", () => {
delete global.fetch; delete global.fetch;
}); });
describe("getBot", () => { describe('getBot', () => {
it("should return the telegraf bot instance", () => { it('should return the telegraf bot instance', () => {
const bot = getBot(); const bot = getBot();
expect(bot).toBeDefined(); expect(bot).toBeDefined();
expect(bot.telegram).toBeDefined(); expect(bot.telegram).toBeDefined();
}); });
}); });
describe("forwardToStorage", () => { describe('forwardToStorage', () => {
it("should forward photo to storage chat and return file details", async () => { it('should forward photo to storage chat and return file details', async () => {
const chunk = Buffer.from("fake photo data"); const chunk = Buffer.from('fake photo data');
const fileName = "test_photo.jpg"; const fileName = 'test_photo.jpg';
const result = await forwardToStorage(chunk, fileName, false); const result = await forwardToStorage(chunk, fileName, false);
expect(result).toEqual({ expect(result).toEqual({
telegramFileId: "photo_id_high", telegramFileId: 'photo_id_high',
telegramFileUniqueId: "unique_id_high", telegramFileUniqueId: 'unique_id_high',
storageMessageId: 12345 storageMessageId: 12345,
}); });
expect(infoSpy).toHaveBeenCalledWith("File forwarded to storage", { expect(infoSpy).toHaveBeenCalledWith('File forwarded to storage', {
fileName, fileName,
message: 12345 message: 12345,
}); });
}); });
it("should handle error when forwarding fails", async () => { it('should handle error when forwarding fails', async () => {
const bot = getBot(); const bot = getBot();
bot.telegram.sendPhoto = mock(() => Promise.reject(new Error("Telegram send failed"))); bot.telegram.sendPhoto = mock(() => Promise.reject(new Error('Telegram send failed')));
const chunk = Buffer.from("fake photo data"); const chunk = Buffer.from('fake photo data');
const fileName = "test_photo.jpg"; const fileName = 'test_photo.jpg';
await expect(forwardToStorage(chunk, fileName, false)).rejects.toThrow("Telegram send failed"); await expect(forwardToStorage(chunk, fileName, false)).rejects.toThrow(
expect(errorSpy).toHaveBeenCalledWith("Failed to forward file to storage", { 'Telegram send failed',
);
expect(errorSpy).toHaveBeenCalledWith('Failed to forward file to storage', {
fileName, fileName,
error: "Telegram send failed" error: 'Telegram send failed',
}); });
}); });
}); });
describe("getFileInfo", () => { describe('getFileInfo', () => {
it("should fetch file details successfully", async () => { it('should fetch file details successfully', async () => {
global.fetch = mock((url, init) => { global.fetch = mock((url, _init) => {
if (url.endsWith("getFile")) { if (url.endsWith('getFile')) {
return Promise.resolve(new Response(JSON.stringify({ return Promise.resolve(
ok: true, new Response(
result: { file_id: "some_file_id" } JSON.stringify({
}))); ok: true,
} else if (url.endsWith("getInfo")) { result: { file_id: 'some_file_id' },
return Promise.resolve(new Response(JSON.stringify({ }),
ok: true, ),
result: { );
file_size: 98765, } else if (url.endsWith('getInfo')) {
mime_type: "image/jpeg", return Promise.resolve(
file_path: "photos/file_0.jpg" new Response(
} JSON.stringify({
}))); ok: true,
result: {
file_size: 98765,
mime_type: 'image/jpeg',
file_path: 'photos/file_0.jpg',
},
}),
),
);
} }
return Promise.reject(new Error("Unknown URL")); return Promise.reject(new Error('Unknown URL'));
}); });
const result = await getFileInfo("some_file_id", "some_unique_id"); const result = await getFileInfo('some_file_id', 'some_unique_id');
expect(result).toEqual({ expect(result).toEqual({
file_size: 98765, file_size: 98765,
mime_type: "image/jpeg", mime_type: 'image/jpeg',
file_path: "photos/file_0.jpg" file_path: 'photos/file_0.jpg',
}); });
}); });
it("should handle error when getFile fails", async () => { it('should handle error when getFile fails', async () => {
global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({ global.fetch = mock(() =>
ok: false, Promise.resolve(
description: "Bad Request: file_id invalid" new Response(
})))); JSON.stringify({
ok: false,
description: 'Bad Request: file_id invalid',
}),
),
),
);
await expect(getFileInfo("invalid_file_id", "invalid_unique_id")).rejects.toThrow("Bad Request: file_id invalid"); await expect(getFileInfo('invalid_file_id', 'invalid_unique_id')).rejects.toThrow(
'Bad Request: file_id invalid',
);
expect(errorSpy).toHaveBeenCalled(); expect(errorSpy).toHaveBeenCalled();
}); });
}); });
+60 -56
View File
@@ -1,120 +1,124 @@
// @ts-nocheck // @ts-nocheck
import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test"; import { afterAll, beforeEach, describe, expect, it, mock } from 'bun:test';
// Mock db // Mock db
const mockInsert = mock(() => ({ const mockInsert = mock(() => ({
values: mock(() => Promise.resolve()) values: mock(() => Promise.resolve()),
})); }));
mock.module("../src/db/index", () => ({ mock.module('../src/db/index', () => ({
db: { db: {
insert: mockInsert insert: mockInsert,
}, },
files: {} files: {},
})); }));
// Mock nanoid // Mock nanoid
mock.module("nanoid", () => ({ mock.module('nanoid', () => ({
nanoid: () => "mocked-nanoid-id" nanoid: () => 'mocked-nanoid-id',
})); }));
// Mock telegram utils // Mock telegram utils
mock.module("../src/utils/telegram", () => ({ mock.module('../src/utils/telegram', () => ({
forwardToStorage: mock(() => Promise.resolve({ forwardToStorage: mock(() =>
telegramFileId: "tg-file-id-123", Promise.resolve({
telegramFileUniqueId: "tg-unique-id-abc", telegramFileId: 'tg-file-id-123',
storageMessageId: 98765 telegramFileUniqueId: 'tg-unique-id-abc',
})), storageMessageId: 98765,
}),
),
getBot: () => ({ getBot: () => ({
telegram: { telegram: {
getFile: mock(() => Promise.resolve({ getFile: mock(() =>
file_id: "tg-file-id-123", Promise.resolve({
file_size: 1000, file_id: 'tg-file-id-123',
mime_type: "image/jpeg" file_size: 1000,
})) mime_type: 'image/jpeg',
} }),
}) ),
},
}),
})); }));
describe("Upload Route Handler", () => { describe('Upload Route Handler', () => {
let handleUpload; let handleUpload: any;
beforeEach(async () => { beforeEach(async () => {
mockInsert.mockClear(); mockInsert.mockClear();
const uploadRoute = await import("../src/routes/upload"); const uploadRoute = await import('../src/routes/upload');
handleUpload = uploadRoute.handleUpload; handleUpload = uploadRoute.handleUpload;
}); });
it("should reject unsupported content types with 400 status", async () => { it('should reject unsupported content types with 400 status', async () => {
const req = new Request("http://localhost:3000/api/upload", { const req = new Request('http://localhost:3000/api/upload', {
method: "POST", method: 'POST',
headers: { headers: {
"content-type": "text/plain" 'content-type': 'text/plain',
}, },
body: "plain text data" body: 'plain text data',
}); });
const res = await handleUpload(req); const res = await handleUpload(req);
expect(res.status).toBe(400); expect(res.status).toBe(400);
const body = await res.json(); const body = await res.json();
expect(body.error).toContain("Unsupported content type"); expect(body.error).toContain('Unsupported content type');
}); });
it("should process JSON upload (base64) successfully", async () => { it('should process JSON upload (base64) successfully', async () => {
const req = new Request("http://localhost:3000/api/upload", { const req = new Request('http://localhost:3000/api/upload', {
method: "POST", method: 'POST',
headers: { headers: {
"content-type": "application/json" 'content-type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
file: Buffer.from("hello world").toString("base64"), file: Buffer.from('hello world').toString('base64'),
fileName: "test.txt" fileName: 'test.txt',
}) }),
}); });
const res = await handleUpload(req); const res = await handleUpload(req);
expect(res.status).toBe(200); expect(res.status).toBe(200);
const body = await res.json(); const body = await res.json();
expect(body.public_id).toBe("mocked-nanoid-id"); expect(body.public_id).toBe('mocked-nanoid-id');
expect(body.telegram_file_id).toBe("tg-file-id-123"); expect(body.telegram_file_id).toBe('tg-file-id-123');
expect(body.telegram_file_unique_id).toBe("tg-unique-id-abc"); expect(body.telegram_file_unique_id).toBe('tg-unique-id-abc');
expect(body.file_name).toBe("test.txt"); expect(body.file_name).toBe('test.txt');
expect(body.file_type).toBe("document"); expect(body.file_type).toBe('document');
}); });
it("should reject JSON upload without file key", async () => { it('should reject JSON upload without file key', async () => {
const req = new Request("http://localhost:3000/api/upload", { const req = new Request('http://localhost:3000/api/upload', {
method: "POST", method: 'POST',
headers: { headers: {
"content-type": "application/json" 'content-type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
fileName: "test.txt" fileName: 'test.txt',
}) }),
}); });
const res = await handleUpload(req); const res = await handleUpload(req);
expect(res.status).toBe(400); expect(res.status).toBe(400);
const body = await res.json(); const body = await res.json();
expect(body.error).toContain("Invalid JSON"); expect(body.error).toContain('Invalid JSON');
}); });
it("should process multipart upload successfully", async () => { it('should process multipart upload successfully', async () => {
const formData = new FormData(); const formData = new FormData();
const fileBlob = new Blob([Buffer.from("multipart hello")], { type: "text/plain" }); const fileBlob = new Blob([Buffer.from('multipart hello')], { type: 'text/plain' });
formData.append("file", fileBlob, "test_multi.txt"); formData.append('file', fileBlob, 'test_multi.txt');
const req = new Request("http://localhost:3000/api/upload", { const req = new Request('http://localhost:3000/api/upload', {
method: "POST", method: 'POST',
body: formData body: formData,
}); });
const res = await handleUpload(req); const res = await handleUpload(req);
expect(res.status).toBe(200); expect(res.status).toBe(200);
const body = await res.json(); const body = await res.json();
expect(body.public_id).toBe("mocked-nanoid-id"); expect(body.public_id).toBe('mocked-nanoid-id');
expect(body.file_name).toBe("test_multi.txt"); expect(body.file_name).toBe('test_multi.txt');
}); });
afterAll(() => { afterAll(() => {