diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..5b4a262 --- /dev/null +++ b/src/index.js @@ -0,0 +1,50 @@ +import { serve } from 'bun'; +import logger from './utils/logger.js'; +import { config } from './env.js'; +import { startBot } from './bot.js'; +import { handleUpload } from './routes/upload.js'; +import { handleFileRedirect, handleFileInfo } from './routes/files.js'; +import { handleHealth } from './routes/health.js'; +import { cleanupRateLimitCache } from './utils/rateLimit.js'; + +const server = serve({ + port: config.port, + routes: { + '/api/upload': { + POST: handleUpload + }, + '/f/:public_id': { + GET: handleFileRedirect + }, + '/file/:public_id/info': { + GET: handleFileInfo + }, + '/health': { + GET: handleHealth + } + } +}); + +const bot = await startBot(); + +logger.info('Server started', { port: config.port, url: config.baseUrl }); + +const gracefulShutdown = async (signal) => { + logger.info('Graceful shutdown signal received', { signal }); + + logger.info('Closing HTTP server'); + server.stop(); + + logger.info('Stopping Telegram bot'); + await bot.stop(); + + logger.info('Server shutdown complete'); + process.exit(0); +}; + +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); +process.on('SIGINT', () => gracefulShutdown('SIGINT')); + +setInterval(cleanupRateLimitCache, 60000); + +logger.info('Application running successfully'); diff --git a/test/bootstrap.test.js b/test/bootstrap.test.js new file mode 100644 index 0000000..2713957 --- /dev/null +++ b/test/bootstrap.test.js @@ -0,0 +1,63 @@ +import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test"; + +const mockServe = mock((options) => { + return { + port: options.port, + routes: options.routes, + stop: mock() + }; +}); + +const originalServe = Bun.serve; +Bun.serve = mockServe; + +const mockStartBot = mock(() => Promise.resolve({ + stop: mock() +})); + +mock.module("../src/bot.js", () => ({ + startBot: mockStartBot +})); + +mock.module("../src/routes/upload.js", () => ({ + handleUpload: mock() +})); + +mock.module("../src/routes/files.js", () => ({ + handleFileRedirect: mock(), + handleFileInfo: mock() +})); + +mock.module("../src/routes/health.js", () => ({ + handleHealth: mock() +})); + +mock.module("../src/utils/rateLimit.js", () => ({ + cleanupRateLimitCache: mock() +})); + +describe("Bootstrap Server", () => { + beforeEach(() => { + mockServe.mockClear(); + mockStartBot.mockClear(); + }); + + afterAll(() => { + Bun.serve = originalServe; + }); + + it("should bootstrap the application successfully", async () => { + await import("../src/index.js"); + + expect(mockServe).toHaveBeenCalled(); + expect(mockStartBot).toHaveBeenCalled(); + + const serveCallArgs = mockServe.mock.calls[0][0]; + expect(serveCallArgs).toHaveProperty("port"); + expect(serveCallArgs).toHaveProperty("routes"); + expect(serveCallArgs.routes).toHaveProperty("/api/upload"); + expect(serveCallArgs.routes).toHaveProperty("/f/:public_id"); + expect(serveCallArgs.routes).toHaveProperty("/file/:public_id/info"); + expect(serveCallArgs.routes).toHaveProperty("/health"); + }); +});