feat: rewire entry point to new architecture
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
import { config } from '../env';
|
||||||
|
|
||||||
|
export { config };
|
||||||
+12
-67
@@ -1,21 +1,14 @@
|
|||||||
import { serve } from 'bun';
|
import { serve } from 'bun';
|
||||||
import { startBot } from './bot';
|
import { config } from './config/index';
|
||||||
import { config } from './env';
|
import { startBot } from './interfaces/bot/handler';
|
||||||
import { handleLogin, handleLogout, handleMe } from './routes/auth';
|
import { routes } from './interfaces/http/routes/index';
|
||||||
import { handleFileInfo, handleFileRedirect } from './routes/files';
|
import { isS3Request } from './interfaces/s3/auth';
|
||||||
import { handleHealth } from './routes/health';
|
import { handleS3Request } from './interfaces/http/controllers/s3-controller';
|
||||||
import { handleHome } from './routes/home';
|
import { extractS3BucketFromHost } from './interfaces/s3/virtual-host';
|
||||||
import { handleS3Request } from './routes/s3';
|
import { fileInfoCache } from './infrastructure/cache/index';
|
||||||
import { handleSwaggerHtml, handleSwaggerJson } from './routes/swagger';
|
import { cleanupRateLimitCache } from './interfaces/http/middleware/rate-limit';
|
||||||
import { handleUpload } from './routes/upload';
|
import { logger } from './shared/logger/index';
|
||||||
import { handleWebApiV1 } from './routes/web-api';
|
import { metricsCollector } from './shared/metrics/index';
|
||||||
import { requireAuth } from './utils/auth';
|
|
||||||
import { fileInfoCache } from './utils/cache';
|
|
||||||
import logger from './utils/logger';
|
|
||||||
import { metricsCollector } from './utils/metrics';
|
|
||||||
import { cleanupRateLimitCache, withRateLimit } from './utils/rateLimit';
|
|
||||||
import { isS3Request } from './utils/s3/auth';
|
|
||||||
import { extractS3BucketFromHost } from './utils/s3/virtual-host';
|
|
||||||
|
|
||||||
// ─── Auto-run migration at startup ──────────────────────────────────────────
|
// ─── Auto-run migration at startup ──────────────────────────────────────────
|
||||||
try {
|
try {
|
||||||
@@ -58,55 +51,7 @@ const handleMaybeS3Root = (req: Request): Response | Promise<Response> => {
|
|||||||
|
|
||||||
const server = serve({
|
const server = serve({
|
||||||
port: config.port,
|
port: config.port,
|
||||||
routes: {
|
routes,
|
||||||
'/api/upload': {
|
|
||||||
POST: withRateLimit(handleUpload),
|
|
||||||
},
|
|
||||||
'/f/:public_id': {
|
|
||||||
GET: withRateLimit(handleFileRedirect),
|
|
||||||
},
|
|
||||||
'/file/:public_id/info': {
|
|
||||||
GET: withRateLimit(handleFileInfo),
|
|
||||||
},
|
|
||||||
'/health': {
|
|
||||||
GET: handleHealth,
|
|
||||||
},
|
|
||||||
'/docs': {
|
|
||||||
GET: handleSwaggerHtml,
|
|
||||||
},
|
|
||||||
'/swagger.json': {
|
|
||||||
GET: handleSwaggerJson,
|
|
||||||
},
|
|
||||||
'/': {
|
|
||||||
GET: (req: Request) => {
|
|
||||||
const headers = Object.fromEntries(req.headers);
|
|
||||||
if (shouldHandleS3(req, headers)) {
|
|
||||||
return handleS3Request(req, getS3RouteBucket(req));
|
|
||||||
}
|
|
||||||
return handleHome();
|
|
||||||
},
|
|
||||||
PUT: handleMaybeS3Root,
|
|
||||||
HEAD: handleMaybeS3Root,
|
|
||||||
DELETE: handleMaybeS3Root,
|
|
||||||
POST: handleMaybeS3Root,
|
|
||||||
OPTIONS: handleMaybeS3Root,
|
|
||||||
},
|
|
||||||
'/api/v1/auth/login': {
|
|
||||||
POST: withRateLimit(handleLogin),
|
|
||||||
},
|
|
||||||
'/api/v1/auth/logout': {
|
|
||||||
POST: handleLogout,
|
|
||||||
},
|
|
||||||
'/api/v1/auth/me': {
|
|
||||||
GET: handleMe,
|
|
||||||
},
|
|
||||||
'/api/v1/*': {
|
|
||||||
GET: requireAuth(handleWebApiV1),
|
|
||||||
POST: requireAuth(handleWebApiV1),
|
|
||||||
DELETE: requireAuth(handleWebApiV1),
|
|
||||||
PUT: requireAuth(handleWebApiV1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fetch: async (req: Request) => {
|
fetch: async (req: Request) => {
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
return handleS3Request(req, getS3RouteBucket(req));
|
return handleS3Request(req, getS3RouteBucket(req));
|
||||||
@@ -163,4 +108,4 @@ setInterval(
|
|||||||
5 * 60 * 1000,
|
5 * 60 * 1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info('Application running successfully');
|
logger.info('Application running successfully');
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export { fileInfoCache, Cache } from '../../utils/cache';
|
||||||
@@ -1,29 +1,62 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { type Context, Telegraf } from 'telegraf';
|
import { type Context, Telegraf } from 'telegraf';
|
||||||
import { db, files as fileSchema } from './db';
|
import { config } from '../../env';
|
||||||
import { findFileByUniqueId } from './db/files';
|
import type { NewFile } from '../../domain/entities/file';
|
||||||
import { config } from './env';
|
import type { IFileRepository } from '../../domain/ports/file-repository';
|
||||||
|
import type { ITelegramService } from '../../domain/ports/telegram-service';
|
||||||
|
import { DrizzleFileRepository } from '../../infrastructure/persistence/repositories/file-repository';
|
||||||
|
import { botPool } from '../../infrastructure/telegram/bot-pool';
|
||||||
import {
|
import {
|
||||||
detectFileType,
|
detectFileType,
|
||||||
extractFileFromMessage,
|
extractFileFromMessage,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
getFileSizeLimit,
|
getFileSizeLimit,
|
||||||
type TelegramMediaMessage,
|
type TelegramMediaMessage,
|
||||||
} from './utils/file';
|
} from '../../shared/utils/file';
|
||||||
import logger from './utils/logger';
|
import logger from '../../utils/logger';
|
||||||
import { forwardToStorage } from './utils/telegram';
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal bot context shape used by the media event handler.
|
||||||
|
*
|
||||||
|
* Represents the subset of Telegraf's Context that the handler requires
|
||||||
|
* for processing incoming media messages.
|
||||||
|
*/
|
||||||
type BotContext = {
|
type BotContext = {
|
||||||
|
/** The incoming media message with file attachments. */
|
||||||
message: TelegramMediaMessage;
|
message: TelegramMediaMessage;
|
||||||
|
/** The sender of the message. */
|
||||||
from: { id: number };
|
from: { id: number };
|
||||||
|
/** The chat where the message was sent, if available. */
|
||||||
chat?: { id: number };
|
chat?: { id: number };
|
||||||
|
/**
|
||||||
|
* Reply to the message with text.
|
||||||
|
*
|
||||||
|
* @param text - The reply text.
|
||||||
|
* @param extra - Optional reply parameters (e.g. reply_parameters for threading).
|
||||||
|
*/
|
||||||
reply: (text: string, extra?: { reply_parameters: { message_id: number } }) => Promise<unknown>;
|
reply: (text: string, extra?: { reply_parameters: { message_id: number } }) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duck-typed object that exposes a Telegraf-style `on()` method
|
||||||
|
* for registering event handlers on multiple event types.
|
||||||
|
*/
|
||||||
type MediaEventRegistrar = {
|
type MediaEventRegistrar = {
|
||||||
|
/**
|
||||||
|
* Register a handler for the given event types.
|
||||||
|
*
|
||||||
|
* @param events - Array of event type strings (e.g. "document", "photo").
|
||||||
|
* @param handler - Async handler receiving the bot context.
|
||||||
|
*/
|
||||||
on: (events: string[], handler: (ctx: BotContext) => Promise<unknown>) => void;
|
on: (events: string[], handler: (ctx: BotContext) => Promise<unknown>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replies to a Telegram message with a download URL for the uploaded file.
|
||||||
|
*
|
||||||
|
* @param ctx - The bot context for the incoming message.
|
||||||
|
* @param publicId - The public identifier of the uploaded file.
|
||||||
|
*/
|
||||||
const replyWithDownloadUrl = async (ctx: BotContext, publicId: string): Promise<void> => {
|
const replyWithDownloadUrl = async (ctx: BotContext, publicId: string): Promise<void> => {
|
||||||
const url = `${config.baseUrl}/f/${publicId}`;
|
const url = `${config.baseUrl}/f/${publicId}`;
|
||||||
await ctx.reply(`File berhasil diupload! 📎\n\nDownload: ${url}`, {
|
await ctx.reply(`File berhasil diupload! 📎\n\nDownload: ${url}`, {
|
||||||
@@ -31,7 +64,33 @@ const replyWithDownloadUrl = async (ctx: BotContext, publicId: string): Promise<
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startBot = async (): Promise<Telegraf<Context>> => {
|
/**
|
||||||
|
* Start the Telegram bot and register message handlers.
|
||||||
|
*
|
||||||
|
* Creates a Telegraf instance, registers a `/start` command handler,
|
||||||
|
* logging middleware, and media event handlers for all supported file types.
|
||||||
|
* Incoming media files are deduplicated by their Telegram unique ID,
|
||||||
|
* forwarded to the storage channel, and persisted with a public download URL.
|
||||||
|
*
|
||||||
|
* @param deps - Optional external dependencies for testing or DI override.
|
||||||
|
* @param deps.telegramService - The Telegram service used to forward files to
|
||||||
|
* the storage channel. Defaults to the singleton BotPool instance.
|
||||||
|
* @param deps.fileRepo - The file repository used for deduplication queries
|
||||||
|
* and persisting new file records. Defaults to a new DrizzleFileRepository.
|
||||||
|
* @returns The launched Telegraf bot instance, suitable for graceful shutdown
|
||||||
|
* via `bot.stop(signal)`.
|
||||||
|
*/
|
||||||
|
export async function startBot(
|
||||||
|
deps: {
|
||||||
|
/** The Telegram service to forward files to storage. */
|
||||||
|
telegramService?: ITelegramService;
|
||||||
|
/** The file repository for deduplication and persistence. */
|
||||||
|
fileRepo?: IFileRepository;
|
||||||
|
} = {},
|
||||||
|
): Promise<Telegraf<Context>> {
|
||||||
|
const telegramService = deps.telegramService ?? botPool;
|
||||||
|
const fileRepo = deps.fileRepo ?? new DrizzleFileRepository();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bot = new Telegraf(config.botToken);
|
const bot = new Telegraf(config.botToken);
|
||||||
|
|
||||||
@@ -74,7 +133,7 @@ export const startBot = async (): Promise<Telegraf<Context>> => {
|
|||||||
return ctx.reply(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`);
|
return ctx.reply(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await findFileByUniqueId(fileObj.file_unique_id);
|
const existing = await fileRepo.findByUniqueId(fileObj.file_unique_id);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await replyWithDownloadUrl(ctx, existing.publicId);
|
await replyWithDownloadUrl(ctx, existing.publicId);
|
||||||
@@ -87,25 +146,36 @@ export const startBot = async (): Promise<Telegraf<Context>> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await forwardToStorage(file_id, fileName, fileType);
|
const result = await telegramService.forwardToStorage(file_id, fileName, fileType);
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
const uploaded = {
|
const uploaded: NewFile = {
|
||||||
publicId: publicId,
|
publicId,
|
||||||
telegramFileId: result.telegramFileId,
|
telegramFileId: result.telegramFileId,
|
||||||
telegramFileUniqueId: result.telegramFileUniqueId,
|
telegramFileUniqueId: result.telegramFileUniqueId,
|
||||||
storageChatId: config.storageChatId,
|
storageChatId: config.storageChatId,
|
||||||
storageMessageId: result.storageMessageId,
|
storageMessageId: result.storageMessageId,
|
||||||
fileName: fileName,
|
fileName,
|
||||||
mimeType: mime_type || 'application/octet-stream',
|
mimeType: mime_type || 'application/octet-stream',
|
||||||
sizeBytes: fileSize,
|
sizeBytes: fileSize,
|
||||||
fileType: fileType,
|
fileType,
|
||||||
uploaderId: ctx.from.id,
|
uploaderId: ctx.from.id,
|
||||||
createdAt: new Date(),
|
fileHash: null,
|
||||||
updatedAt: new Date(),
|
archiveTelegramFileId: null,
|
||||||
|
archiveStorageMessageId: null,
|
||||||
|
archiveFileName: null,
|
||||||
|
archiveEntryName: null,
|
||||||
|
archiveMimeType: null,
|
||||||
|
archiveSizeBytes: null,
|
||||||
|
bucketId: null,
|
||||||
|
s3Key: null,
|
||||||
|
storageBackend: 'telegram',
|
||||||
|
isDeleted: false,
|
||||||
|
multipartUploadId: null,
|
||||||
|
partCount: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.insert(fileSchema).values(uploaded);
|
await fileRepo.create(uploaded);
|
||||||
|
|
||||||
await replyWithDownloadUrl(ctx, publicId);
|
await replyWithDownloadUrl(ctx, publicId);
|
||||||
|
|
||||||
@@ -134,4 +204,4 @@ export const startBot = async (): Promise<Telegraf<Context>> => {
|
|||||||
logger.error('Failed to start bot', { error: getErrorMessage(error) });
|
logger.error('Failed to start bot', { error: getErrorMessage(error) });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { withRateLimit, cleanupRateLimitCache, checkRateLimit, clearRateLimitCache, getRateLimitStats } from '../../../utils/rateLimit';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { isS3Request, buildCanonicalQueryString, verifyPresignedUrl, verifySignature } from '../../utils/s3/auth';
|
||||||
|
export type { SigV4Result, VerifyPresignedUrlInput } from '../../utils/s3/auth';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { extractS3BucketFromHost } from '../../utils/s3/virtual-host';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import logger from '../../utils/logger';
|
||||||
|
|
||||||
|
export { logger };
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { metricsCollector, MetricsCollector } from '../../utils/metrics';
|
||||||
@@ -36,7 +36,7 @@ const mockRequireAuth = mock(
|
|||||||
Response.json({ error: 'Unauthorized' }, { status: 401 }),
|
Response.json({ error: 'Unauthorized' }, { status: 401 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
mock.module('../src/bot', () => ({
|
mock.module('../src/interfaces/bot/handler', () => ({
|
||||||
startBot: mockStartBot,
|
startBot: mockStartBot,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -65,6 +65,9 @@ mock.module('../src/routes/auth', () => ({
|
|||||||
|
|
||||||
mock.module('../src/utils/rateLimit', () => ({
|
mock.module('../src/utils/rateLimit', () => ({
|
||||||
cleanupRateLimitCache: mock(),
|
cleanupRateLimitCache: mock(),
|
||||||
|
clearRateLimitCache: mock(),
|
||||||
|
checkRateLimit: mock(() => true),
|
||||||
|
getRateLimitStats: mock(() => ({})),
|
||||||
withRateLimit: <T extends Request>(
|
withRateLimit: <T extends Request>(
|
||||||
handler: (req: T) => Promise<Response>,
|
handler: (req: T) => Promise<Response>,
|
||||||
): ((req: T) => Promise<Response>) => handler,
|
): ((req: T) => Promise<Response>) => handler,
|
||||||
|
|||||||
+65
-65
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
||||||
import type { TelegramMediaMessage } from '../src/utils/file';
|
import type { TelegramMediaMessage } from '../src/shared/utils/file';
|
||||||
import logger from '../src/utils/logger';
|
import logger from '../src/utils/logger';
|
||||||
|
|
||||||
// Mock environment
|
// Mock environment
|
||||||
@@ -46,61 +46,43 @@ mock.module('telegraf', () => ({
|
|||||||
Telegraf: MockTelegraf,
|
Telegraf: MockTelegraf,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock database
|
|
||||||
const mockInsert = mock(() => ({
|
|
||||||
values: mock(() => Promise.resolve()),
|
|
||||||
}));
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
files: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
mock.module('../src/db/files', () => ({
|
|
||||||
findFileByUniqueId: mockFindFileByUniqueId,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock forwardToStorage
|
|
||||||
const mockForwardToStorage = mock(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
telegramFileId: 'stored_file_id',
|
|
||||||
telegramFileUniqueId: 'stored_unique_id',
|
|
||||||
storageMessageId: 9999,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
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', () => {
|
||||||
|
let mockTelegramService: { forwardToStorage: ReturnType<typeof mock> };
|
||||||
|
let mockFileRepo: { findByUniqueId: ReturnType<typeof mock>; create: ReturnType<typeof mock> };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLaunch.mockClear();
|
mockLaunch.mockClear();
|
||||||
mockCommand.mockClear();
|
mockCommand.mockClear();
|
||||||
mockOn.mockClear();
|
mockOn.mockClear();
|
||||||
mockUse.mockClear();
|
mockUse.mockClear();
|
||||||
mockInsert.mockClear();
|
|
||||||
mockFindFileByUniqueId.mockClear();
|
|
||||||
mockFindFileByUniqueId.mockResolvedValue(null);
|
|
||||||
mockForwardToStorage.mockClear();
|
|
||||||
infoSpy.mockClear();
|
infoSpy.mockClear();
|
||||||
errorSpy.mockClear();
|
errorSpy.mockClear();
|
||||||
|
|
||||||
|
mockTelegramService = {
|
||||||
|
forwardToStorage: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
telegramFileId: 'stored_file_id',
|
||||||
|
telegramFileUniqueId: 'stored_unique_id',
|
||||||
|
storageMessageId: 9999,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFileRepo = {
|
||||||
|
findByUniqueId: mock((): Promise<unknown> => Promise.resolve(null)),
|
||||||
|
create: mock(() => Promise.resolve()),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
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/interfaces/bot/handler');
|
||||||
const bot = await startBot();
|
const bot = await startBot({
|
||||||
|
telegramService: mockTelegramService,
|
||||||
|
fileRepo: mockFileRepo,
|
||||||
|
});
|
||||||
|
|
||||||
expect(bot).toBeDefined();
|
expect(bot).toBeDefined();
|
||||||
expect(mockCommand).toHaveBeenCalledWith('start', expect.any(Function));
|
expect(mockCommand).toHaveBeenCalledWith('start', expect.any(Function));
|
||||||
@@ -113,8 +95,11 @@ describe('Telegram Bot Handler', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle /start command', async () => {
|
it('should handle /start command', async () => {
|
||||||
const { startBot } = await import('../src/bot');
|
const { startBot } = await import('../src/interfaces/bot/handler');
|
||||||
await startBot();
|
await startBot({
|
||||||
|
telegramService: mockTelegramService,
|
||||||
|
fileRepo: mockFileRepo,
|
||||||
|
});
|
||||||
|
|
||||||
const startHandler = getStartHandler();
|
const startHandler = getStartHandler();
|
||||||
const replyMock = mock(() => Promise.resolve());
|
const replyMock = mock(() => Promise.resolve());
|
||||||
@@ -127,8 +112,11 @@ describe('Telegram Bot Handler', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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/interfaces/bot/handler');
|
||||||
await startBot();
|
await startBot({
|
||||||
|
telegramService: mockTelegramService,
|
||||||
|
fileRepo: mockFileRepo,
|
||||||
|
});
|
||||||
|
|
||||||
const fileHandler = getFileHandler();
|
const fileHandler = getFileHandler();
|
||||||
const replyMock = mock(() => Promise.resolve());
|
const replyMock = mock(() => Promise.resolve());
|
||||||
@@ -150,8 +138,8 @@ describe('Telegram Bot Handler', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await fileHandler(ctx);
|
await fileHandler(ctx);
|
||||||
expect(mockForwardToStorage).toHaveBeenCalledWith('doc_123', 'cv.pdf', 'document');
|
expect(mockTelegramService.forwardToStorage).toHaveBeenCalledWith('doc_123', 'cv.pdf', 'document');
|
||||||
expect(mockInsert).toHaveBeenCalled();
|
expect(mockFileRepo.create).toHaveBeenCalled();
|
||||||
expect(replyMock).toHaveBeenCalledWith(
|
expect(replyMock).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('File berhasil diupload'),
|
expect.stringContaining('File berhasil diupload'),
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -159,8 +147,11 @@ describe('Telegram Bot Handler', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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/interfaces/bot/handler');
|
||||||
await startBot();
|
await startBot({
|
||||||
|
telegramService: mockTelegramService,
|
||||||
|
fileRepo: mockFileRepo,
|
||||||
|
});
|
||||||
|
|
||||||
const fileHandler = getFileHandler();
|
const fileHandler = getFileHandler();
|
||||||
const replyMock = mock(() => Promise.resolve());
|
const replyMock = mock(() => Promise.resolve());
|
||||||
@@ -183,18 +174,21 @@ describe('Telegram Bot Handler', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await fileHandler(ctx);
|
await fileHandler(ctx);
|
||||||
expect(mockForwardToStorage).not.toHaveBeenCalled();
|
expect(mockTelegramService.forwardToStorage).not.toHaveBeenCalled();
|
||||||
expect(replyMock).toHaveBeenCalledWith(expect.stringContaining('exceeds'));
|
expect(replyMock).toHaveBeenCalledWith(expect.stringContaining('exceeds'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return existing download link for duplicates without uploading again', async () => {
|
it('should return existing download link for duplicates without uploading again', async () => {
|
||||||
const { startBot } = await import('../src/bot');
|
const { startBot } = await import('../src/interfaces/bot/handler');
|
||||||
await startBot();
|
await startBot({
|
||||||
|
telegramService: mockTelegramService,
|
||||||
|
fileRepo: mockFileRepo,
|
||||||
|
});
|
||||||
|
|
||||||
const fileHandler = getFileHandler();
|
const fileHandler = getFileHandler();
|
||||||
const replyMock = mock(() => Promise.resolve());
|
const replyMock = mock(() => Promise.resolve());
|
||||||
|
|
||||||
mockFindFileByUniqueId.mockResolvedValueOnce({
|
mockFileRepo.findByUniqueId.mockResolvedValueOnce({
|
||||||
publicId: 'already_exists_abc',
|
publicId: 'already_exists_abc',
|
||||||
telegramFileId: 'stored_file_id',
|
telegramFileId: 'stored_file_id',
|
||||||
telegramFileUniqueId: 'doc_uniq_123',
|
telegramFileUniqueId: 'doc_uniq_123',
|
||||||
@@ -218,8 +212,8 @@ describe('Telegram Bot Handler', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await fileHandler(ctx);
|
await fileHandler(ctx);
|
||||||
expect(mockForwardToStorage).not.toHaveBeenCalled();
|
expect(mockTelegramService.forwardToStorage).not.toHaveBeenCalled();
|
||||||
expect(mockInsert).not.toHaveBeenCalled();
|
expect(mockFileRepo.create).not.toHaveBeenCalled();
|
||||||
expect(replyMock).toHaveBeenCalledWith(
|
expect(replyMock).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('already_exists_abc'),
|
expect.stringContaining('already_exists_abc'),
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -227,8 +221,11 @@ describe('Telegram Bot Handler', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should process sticker uploads', async () => {
|
it('should process sticker uploads', async () => {
|
||||||
const { startBot } = await import('../src/bot');
|
const { startBot } = await import('../src/interfaces/bot/handler');
|
||||||
await startBot();
|
await startBot({
|
||||||
|
telegramService: mockTelegramService,
|
||||||
|
fileRepo: mockFileRepo,
|
||||||
|
});
|
||||||
|
|
||||||
const fileHandler = getFileHandler();
|
const fileHandler = getFileHandler();
|
||||||
const replyMock = mock(() => Promise.resolve());
|
const replyMock = mock(() => Promise.resolve());
|
||||||
@@ -248,8 +245,8 @@ describe('Telegram Bot Handler', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await fileHandler(ctx);
|
await fileHandler(ctx);
|
||||||
expect(mockForwardToStorage).toHaveBeenCalledWith('sticker_123', 'file', 'sticker');
|
expect(mockTelegramService.forwardToStorage).toHaveBeenCalledWith('sticker_123', 'file', 'sticker');
|
||||||
expect(mockInsert).toHaveBeenCalled();
|
expect(mockFileRepo.create).toHaveBeenCalled();
|
||||||
expect(replyMock).toHaveBeenCalledWith(
|
expect(replyMock).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('File berhasil diupload'),
|
expect.stringContaining('File berhasil diupload'),
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -257,8 +254,11 @@ describe('Telegram Bot Handler', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should process video note uploads', async () => {
|
it('should process video note uploads', async () => {
|
||||||
const { startBot } = await import('../src/bot');
|
const { startBot } = await import('../src/interfaces/bot/handler');
|
||||||
await startBot();
|
await startBot({
|
||||||
|
telegramService: mockTelegramService,
|
||||||
|
fileRepo: mockFileRepo,
|
||||||
|
});
|
||||||
|
|
||||||
const fileHandler = getFileHandler();
|
const fileHandler = getFileHandler();
|
||||||
const replyMock = mock(() => Promise.resolve());
|
const replyMock = mock(() => Promise.resolve());
|
||||||
@@ -278,8 +278,8 @@ describe('Telegram Bot Handler', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await fileHandler(ctx);
|
await fileHandler(ctx);
|
||||||
expect(mockForwardToStorage).toHaveBeenCalledWith('video_note_123', 'file', 'video_note');
|
expect(mockTelegramService.forwardToStorage).toHaveBeenCalledWith('video_note_123', 'file', 'video_note');
|
||||||
expect(mockInsert).toHaveBeenCalled();
|
expect(mockFileRepo.create).toHaveBeenCalled();
|
||||||
expect(replyMock).toHaveBeenCalledWith(
|
expect(replyMock).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('File berhasil diupload'),
|
expect.stringContaining('File berhasil diupload'),
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -289,4 +289,4 @@ describe('Telegram Bot Handler', () => {
|
|||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user