From 5758df0eb59258da7aa8de7eb55faa52b5fa24f9 Mon Sep 17 00:00:00 2001 From: mashingaan Date: Tue, 3 Feb 2026 12:16:49 +0300 Subject: [PATCH] chore: test fixes and env token helpers --- README.md | 3 +- SECURITY.md | 2 +- .../src/__tests__/api.integration.test.ts | 18 ++++- .../broadcasts-api.integration.test.ts | 23 +++++- packages/core/src/db/postgres.ts | 42 ++++++++++- packages/core/src/index.ts | 34 +++++++-- packages/mini-app/src/pages/BotList.tsx | 33 +++++++-- packages/mini-app/src/utils/api.ts | 72 ++++++++++++++++--- .../shared/src/env/getTelegramBotToken.ts | 12 ++++ packages/shared/src/index.ts | 1 + packages/shared/src/test-utils/api-helpers.ts | 3 +- 11 files changed, 214 insertions(+), 29 deletions(-) create mode 100644 packages/shared/src/env/getTelegramBotToken.ts diff --git a/README.md b/README.md index d7025d8..378c22c 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ npm install ```env # Telegram TELEGRAM_BOT_TOKEN=your_bot_token_here +# BOT_TOKEN is deprecated (legacy). Use TELEGRAM_BOT_TOKEN instead. BOT_TOKEN=your_bot_token_here # Database @@ -320,7 +321,7 @@ docker-compose up -d See [SECURITY.md](./SECURITY.md) for details on validation, authentication, encryption, audit logging, and key rotation. -Required security env vars: `ENCRYPTION_KEY`, `BOT_TOKEN`. +Required security env vars: `ENCRYPTION_KEY`, `TELEGRAM_BOT_TOKEN` (legacy `BOT_TOKEN` is deprecated). ## πŸ” Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ diff --git a/SECURITY.md b/SECURITY.md index 569b9ce..6e668aa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,7 +9,7 @@ ## ΠŸΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния - ENCRYPTION_KEY (ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Π°Ρ, ΠΌΠΈΠ½ΠΈΠΌΡƒΠΌ 32 символа) - ENCRYPTION_KEY_VERSION (отслСТиваСт Π²Π΅Ρ€ΡΠΈΡŽ ΠΊΠ»ΡŽΡ‡Π°) -- BOT_TOKEN (Ρ‚ΠΎΠΊΠ΅Π½ Telegram-Π±ΠΎΡ‚Π° для Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ initData) +- TELEGRAM_BOT_TOKEN (Ρ‚ΠΎΠΊΠ΅Π½ Telegram-Π±ΠΎΡ‚Π° для Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ initData; legacy BOT_TOKEN deprecated) ## Ротация ΠΊΠ»ΡŽΡ‡Π΅ΠΉ 1. Π‘Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠΉΡ‚Π΅ Π½ΠΎΠ²Ρ‹ΠΉ ENCRYPTION_KEY diff --git a/packages/core/src/__tests__/api.integration.test.ts b/packages/core/src/__tests__/api.integration.test.ts index e960e6a..49202fd 100644 --- a/packages/core/src/__tests__/api.integration.test.ts +++ b/packages/core/src/__tests__/api.integration.test.ts @@ -15,7 +15,9 @@ const request = supertest.agent(app); let pool: ReturnType; let postgresPool: any; const encryptionKey = process.env.ENCRYPTION_KEY as string; -const botToken = process.env.BOT_TOKEN || 'test-bot-token'; +const botToken = process.env.TELEGRAM_BOT_TOKEN || 'test-bot-token'; +let previousTelegramBotToken: string | undefined; +let previousBotToken: string | undefined; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -60,6 +62,7 @@ async function deleteBotApi(userId: number, botId: string) { } beforeEach(async () => { + process.env.TELEGRAM_BOT_TOKEN = botToken; const redisClient = await redisModule.getRedisClientOptional(); await cleanupAllTestState(pool, redisClient); @@ -69,13 +72,24 @@ beforeEach(async () => { }); afterEach(() => { + if (previousTelegramBotToken === undefined) { + delete process.env.TELEGRAM_BOT_TOKEN; + } else { + process.env.TELEGRAM_BOT_TOKEN = previousTelegramBotToken; + } + if (previousBotToken === undefined) { + delete process.env.BOT_TOKEN; + } else { + process.env.BOT_TOKEN = previousBotToken; + } // Defensive reset in case a test fails before its own `finally` cleanup setRedisUnavailableForTests(false); setRedisAvailableForTests(true); }); beforeAll(async () => { - process.env.BOT_TOKEN = botToken; + previousTelegramBotToken = process.env.TELEGRAM_BOT_TOKEN; + previousBotToken = process.env.BOT_TOKEN; pool = createTestPostgresPool(); // Initialize databases explicitly to ensure dbInitialized flag is set before any /health request diff --git a/packages/core/src/__tests__/broadcasts-api.integration.test.ts b/packages/core/src/__tests__/broadcasts-api.integration.test.ts index 2530065..591e5c0 100644 --- a/packages/core/src/__tests__/broadcasts-api.integration.test.ts +++ b/packages/core/src/__tests__/broadcasts-api.integration.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import supertest from 'supertest'; import crypto from 'crypto'; import { createApp } from '../index'; @@ -11,7 +11,9 @@ const app = createApp(); const request = supertest.agent(app); let pool: ReturnType; const encryptionKey = process.env.ENCRYPTION_KEY as string; -const botToken = process.env.BOT_TOKEN || 'test-bot-token'; +const botToken = process.env.TELEGRAM_BOT_TOKEN || 'test-bot-token'; +let previousTelegramBotToken: string | undefined; +let previousBotToken: string | undefined; const rateLimitCooldownMs = 1500; async function seedBotUsers(botId: string, telegramIds: string[]) { @@ -38,13 +40,15 @@ async function seedBotUsers(botId: string, telegramIds: string[]) { } beforeEach(async () => { + process.env.TELEGRAM_BOT_TOKEN = botToken; const redisClient = await getRedisClientOptional(); await cleanupAllTestState(pool, redisClient); await new Promise((resolve) => setTimeout(resolve, rateLimitCooldownMs)); }); beforeAll(async () => { - process.env.BOT_TOKEN = botToken; + previousTelegramBotToken = process.env.TELEGRAM_BOT_TOKEN; + previousBotToken = process.env.BOT_TOKEN; pool = createTestPostgresPool(); const { initializeRateLimiters } = await import('../index'); @@ -57,6 +61,19 @@ afterAll(async () => { } }); +afterEach(() => { + if (previousTelegramBotToken === undefined) { + delete process.env.TELEGRAM_BOT_TOKEN; + } else { + process.env.TELEGRAM_BOT_TOKEN = previousTelegramBotToken; + } + if (previousBotToken === undefined) { + delete process.env.BOT_TOKEN; + } else { + process.env.BOT_TOKEN = previousBotToken; + } +}); + describe('Broadcasts API', () => { it('creates broadcast and returns total recipients', async () => { const botId = crypto.randomUUID(); diff --git a/packages/core/src/db/postgres.ts b/packages/core/src/db/postgres.ts index 5a797fc..da98399 100644 --- a/packages/core/src/db/postgres.ts +++ b/packages/core/src/db/postgres.ts @@ -83,7 +83,26 @@ type PostgresConnectionInfo = { user: string; }; -function getPostgresConnectionInfo(connectionString: string): PostgresConnectionInfo | null { +const LOCALHOST_HOSTS = new Set([ + 'localhost', + 'localhost.localdomain', + '127.0.0.1', + '127.0.1.1', + '::1', + '0.0.0.0', +]); + +function normalizeHost(host?: string): string { + if (!host) return ''; + return host.trim().toLowerCase().replace(/^\[/, '').replace(/\]$/, ''); +} + +function isLocalhostHost(host?: string): boolean { + const normalized = normalizeHost(host); + return LOCALHOST_HOSTS.has(normalized); +} + +export function getPostgresConnectionInfo(connectionString: string): PostgresConnectionInfo | null { const activePool = pool; try { @@ -223,11 +242,15 @@ async function connectWithRetry( urlValid, diagnostics, }, 'PostgreSQL connection state: error'); + const localhostHint = + isVercel && connectionInfo && isLocalhostHost(connectionInfo.host) + ? ' Check DATABASE_URL: localhost is unreachable on Vercel.' + : ''; throw new Error( `PostgreSQL connection failed after ${attempt} attempts ` + `(${formatPostgresConnectionInfo(connectionInfo)}). ` + `URL format: ${urlValid ? 'valid' : 'invalid'}. ` + - `Likely cause: ${diagnostics.category} (${diagnostics.hint})` + `Likely cause: ${diagnostics.category} (${diagnostics.hint}).${localhostHint}` ); } logger?.warn({ @@ -272,6 +295,21 @@ export async function initPostgres(loggerInstance: Logger): Promise { const poolConfig = getPostgresPoolConfig(); const connectionInfo = getPostgresConnectionInfo(connectionString); + if (isVercel && connectionInfo && isLocalhostHost(connectionInfo.host)) { + logger?.error({ + service: 'postgres', + environment: 'Vercel serverless', + detectedHost: connectionInfo.host, + vercelEnv: process.env.VERCEL_ENV, + hint: 'Use Neon (neon.tech) or Supabase (supabase.com) for serverless PostgreSQL', + }, '❌ Invalid DATABASE_URL: localhost detected on Vercel'); + throw new Error( + `DATABASE_URL points to localhost (${connectionInfo.host}) on Vercel. ` + + 'Use a production PostgreSQL service (Neon, Supabase, AWS RDS) with public endpoint. ' + + 'Update DATABASE_URL in Vercel Dashboard β†’ Settings β†’ Environment Variables.' + ); + } + const { max, idleTimeoutMillis, connectionTimeoutMillis } = poolConfig; logger?.info({ service: 'postgres', diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7322cc2..834b5a5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,9 +9,9 @@ import { Telegraf, session } from 'telegraf'; import { Scenes } from 'telegraf'; import pinoHttp from 'pino-http'; import { z } from 'zod'; -import { BOT_LIMITS, RATE_LIMITS, WEBHOOK_INTEGRATION_LIMITS, BotIdSchema, BroadcastIdSchema, CreateBotSchema, CreateBroadcastSchema, PaginationSchema, UpdateBotSchemaSchema, createLogger, createRateLimiter, errorMetricsMiddleware, getErrorMetrics, logBroadcastCreated, logRateLimitMetrics, metricsMiddleware, requestContextMiddleware, requestIdMiddleware, requireBotOwnership, validateBody, validateParams, validateQuery, validateTelegramWebAppData } from '@dialogue-constructor/shared'; +import { BOT_LIMITS, RATE_LIMITS, WEBHOOK_INTEGRATION_LIMITS, BotIdSchema, BroadcastIdSchema, CreateBotSchema, CreateBroadcastSchema, PaginationSchema, UpdateBotSchemaSchema, createLogger, createRateLimiter, errorMetricsMiddleware, getErrorMetrics, getTelegramBotToken, logBroadcastCreated, logRateLimitMetrics, metricsMiddleware, requestContextMiddleware, requestIdMiddleware, requireBotOwnership, validateBody, validateParams, validateQuery, validateTelegramWebAppData } from '@dialogue-constructor/shared'; import { getRequestId, validateBotSchema } from '@dialogue-constructor/shared/server'; -import { initPostgres, closePostgres, getPoolStats, getPostgresCircuitBreakerStats, getPostgresConnectRetryBudgetMs, getPostgresRetryStats, POSTGRES_RETRY_CONFIG, getPostgresClient, getPostgresPoolConfig } from './db/postgres'; +import { initPostgres, closePostgres, getPoolStats, getPostgresCircuitBreakerStats, getPostgresConnectRetryBudgetMs, getPostgresRetryStats, POSTGRES_RETRY_CONFIG, getPostgresClient, getPostgresPoolConfig, getPostgresConnectionInfo } from './db/postgres'; import { initRedis, closeRedis, getRedisCircuitBreakerStats, getRedisClientOptional, getRedisRetryStats } from './db/redis'; import { initializeBotsTable, getBotsByUserId, getBotsByUserIdPaginated, getBotById, updateBotSchema, createBot, deleteBot } from './db/bots'; import { exportBotUsersToCSV, getBotTelegramUserIds, getBotUsers, getBotUserStats } from './db/bot-users'; @@ -987,12 +987,26 @@ app.get('/health', async (req: Request, res: Response) => { const statusCode = status === 'error' ? 503 : 200; const timestamp = new Date().toISOString(); + const minimalConnectionInfo = (() => { + if (process.env.VERCEL !== '1') return null; + const dbUrl = process.env.DATABASE_URL; + if (!dbUrl) return null; + const info = getPostgresConnectionInfo(dbUrl); + if (!info) return null; + const host = info.host?.trim().toLowerCase().replace(/^\[/, '').replace(/\]$/, ''); + return { + host, + port: info.port, + database: info.database, + }; + })(); const minimalHealth = { status, timestamp, databases: { postgres: { status: postgresState, + ...(minimalConnectionInfo ? { connectionInfo: minimalConnectionInfo } : {}), }, redis: { status: redisState, @@ -1067,6 +1081,18 @@ app.get('/health', async (req: Request, res: Response) => { status: postgresState, pool: poolInfo, poolConfig: postgresPoolConfig, + connectionInfo: (() => { + const dbUrl = process.env.DATABASE_URL; + if (!dbUrl) return null; + const info = getPostgresConnectionInfo(dbUrl); + return info + ? { + host: info.host, + port: info.port, + database: info.database, + } + : null; + })(), }, redis: { status: redisState, @@ -1109,9 +1135,9 @@ async function requireUserId(req: Request, res: Response, next: Function) { return res.status(401).json({ error: 'Unauthorized' }); } - const botToken = process.env.BOT_TOKEN; + const botToken = getTelegramBotToken(); if (!botToken) { - return res.status(500).json({ error: 'BOT_TOKEN is not set' }); + return res.status(500).json({ error: 'TELEGRAM_BOT_TOKEN is not set' }); } const validation = validateTelegramWebAppData(initData, botToken); diff --git a/packages/mini-app/src/pages/BotList.tsx b/packages/mini-app/src/pages/BotList.tsx index 0061607..3715616 100644 --- a/packages/mini-app/src/pages/BotList.tsx +++ b/packages/mini-app/src/pages/BotList.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { api } from '../utils/api'; +import { api, formatApiError } from '../utils/api'; import { BotSummary } from '../types'; const WebApp = window.Telegram?.WebApp; @@ -34,8 +34,13 @@ export default function BotList() { setBots(data.bots); setPagination(data.pagination); } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Ошибка Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π±ΠΎΡ‚ΠΎΠ²'; - console.error('❌ Error loading bots:', err); + const status = (err as any).status; + const errorMessage = formatApiError(err); + console.error('❌ Error loading bots:', { + error: err, + status, + message: errorMessage, + }); setError(errorMessage); WebApp?.showAlert(errorMessage); } finally { @@ -55,9 +60,15 @@ export default function BotList() { setBots((prev) => [...prev, ...data.bots]); setPagination(data.pagination); } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Ошибка Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π±ΠΎΡ‚ΠΎΠ²'; - console.error('❌ Error loading more bots:', err); - WebApp?.showAlert(errorMessage); + const status = (err as any).status; + const errorMessage = formatApiError(err); + console.error('❌ Error loading more bots:', { + error: err, + status, + message: errorMessage, + }); + + // НС спамим ΠΌΠΎΠ΄Π°Π»ΡŒΠ½Ρ‹ΠΌ Π°Π»Π΅Ρ€Ρ‚ΠΎΠΌ ΠΏΡ€ΠΈ ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΠΈ (loadMore); ΠΏΡ€ΠΈ ΠΆΠ΅Π»Π°Π½ΠΈΠΈ β€” мягкий toast/ΠΈΠ½Π΄ΠΈΠΊΠ°Ρ‚ΠΎΡ€ } finally { setLoadingMore(false); } @@ -88,6 +99,16 @@ export default function BotList() {
❌
{error}
+
+ πŸ’‘ Если ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΎ Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅: ΠΎΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ F12 β†’ Network ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ запросы ΠΊ API +
diff --git a/packages/mini-app/src/utils/api.ts b/packages/mini-app/src/utils/api.ts index 03baaaa..05c98c6 100644 --- a/packages/mini-app/src/utils/api.ts +++ b/packages/mini-app/src/utils/api.ts @@ -88,11 +88,13 @@ async function apiRequest( const url = `${apiUrl}${endpoint}${endpoint.includes('?') ? '&' : '?'}user_id=${userId}`; console.log('πŸ“‘ API Request:', { + timestamp: new Date().toISOString(), method: options?.method || 'GET', url, userId, apiUrl, - isLocalhost: hostname, + hostname, + isLocalhost: hostname === 'localhost', }); try { @@ -121,20 +123,39 @@ async function apiRequest( if (!response.ok) { let errorData: ApiError; + let responseText = ''; try { - errorData = await response.json(); + responseText = await response.text(); + errorData = JSON.parse(responseText); } catch { errorData = { - error: `HTTP ${response.status}: ${response.statusText}`, + error: responseText || `HTTP ${response.status}: ${response.statusText}`, }; } - - console.error('❌ API Error:', errorData); - console.error('❌ Response details:', { + + const redactTextForLog = (s: string) => + s.replace(/("?(?:password|token|secret)"?\s*:\s*)"[^"]*"/gi, '$1"***"'); + const redactObjectForLog = (obj: any) => { + if (!obj || typeof obj !== 'object') return obj; + const out = Array.isArray(obj) ? [...obj] : { ...obj }; + for (const key of Object.keys(out)) { + if (/(password|token|secret)/i.test(key)) out[key] = '***'; + } + return out; + }; + + const responseTextForLog = redactTextForLog(responseText).substring(0, 500); + const errorDataForLog = redactObjectForLog(errorData); + + console.error('❌ API Error:', { + timestamp: new Date().toISOString(), url: response.url, - redirected: response.redirected, - type: response.type, + status: response.status, + statusText: response.statusText, + errorData: errorDataForLog, + responseText: responseTextForLog, }); + const message = errorData.error || errorData.message || `API request failed: ${response.status} ${response.statusText}`; const requestError = new Error(message); (requestError as any).status = response.status; @@ -147,16 +168,49 @@ async function apiRequest( return data; } catch (error) { console.error('❌ Request failed:', { + timestamp: new Date().toISOString(), error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, + status: (error as any).status, apiUrl, endpoint, + url, }); - console.error('❌ API Request Error:', error); throw error; } } +export function formatApiError(error: unknown): string { + const status = (error as any).status; + + if (status === 500) { + return 'Ошибка сСрвСра (500). Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, Π±Π°Π·Π° Π΄Π°Π½Π½Ρ‹Ρ… нСдоступна ΠΈΠ»ΠΈ нСвСрная конфигурация.'; + } + if (status === 503) { + return 'БСрвис Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ нСдоступСн (503). ΠŸΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ ΠΏΠΎΠ·ΠΆΠ΅.'; + } + if (status === 401 || status === 403) { + return 'Ошибка Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ. Π£Π±Π΅Π΄ΠΈΡ‚Π΅ΡΡŒ, Ρ‡Ρ‚ΠΎ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π·Π°ΠΏΡƒΡ‰Π΅Π½ΠΎ Π² Telegram.'; + } + if (status === 404) { + return 'РСсурс Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ (404).'; + } + + if (error instanceof TypeError) { + const msg = error.message || ''; + if (/failed to fetch|networkerror|load failed|fetch/i.test(msg)) { + return 'ΠŸΡ€ΠΎΠ±Π»Π΅ΠΌΠ° сСти ΠΈΠ»ΠΈ CORS. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ / ΠΏΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ ΠΏΠΎΠ·ΠΆΠ΅.'; + } + return 'ΠŸΡ€ΠΎΠ±Π»Π΅ΠΌΠ° сСти. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ / ΠΏΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ ΠΏΠΎΠ·ΠΆΠ΅.'; + } + + if (error instanceof Error) { + return error.message; + } + + return 'НСизвСстная ошибка. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒΡ‚Π΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΊ ΠΈΠ½Ρ‚Π΅Ρ€Π½Π΅Ρ‚Ρƒ.'; +} + export const api = { // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ список Π±ΠΎΡ‚ΠΎΠ² getBots: (params?: { limit?: number; offset?: number }): Promise<{ bots: BotSummary[]; pagination: { total: number; limit: number; offset: number; hasMore: boolean } }> => { diff --git a/packages/shared/src/env/getTelegramBotToken.ts b/packages/shared/src/env/getTelegramBotToken.ts new file mode 100644 index 0000000..1eae61b --- /dev/null +++ b/packages/shared/src/env/getTelegramBotToken.ts @@ -0,0 +1,12 @@ +let warned = false; + +export function getTelegramBotToken(): string | undefined { + const botToken = process.env.TELEGRAM_BOT_TOKEN ?? process.env.BOT_TOKEN; + + if (!process.env.TELEGRAM_BOT_TOKEN && process.env.BOT_TOKEN && !warned) { + warned = true; + console.warn('BOT_TOKEN is deprecated; use TELEGRAM_BOT_TOKEN instead'); + } + + return botToken; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 693c577..4e0c8ce 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -17,6 +17,7 @@ export * from './validation/bot-schema-validation'; export * from './validation/schemas'; export * from './db/bot-users'; export * from './db/bot-analytics'; +export * from './env/getTelegramBotToken'; export interface User { id: number; diff --git a/packages/shared/src/test-utils/api-helpers.ts b/packages/shared/src/test-utils/api-helpers.ts index d0aa87f..63ef4e8 100644 --- a/packages/shared/src/test-utils/api-helpers.ts +++ b/packages/shared/src/test-utils/api-helpers.ts @@ -3,6 +3,7 @@ import type { Express } from 'express'; import type { Test } from 'supertest'; import { expect } from 'vitest'; import crypto from 'crypto'; +import { getTelegramBotToken } from '../env/getTelegramBotToken'; export function createTestApp(): Express { const app = express(); @@ -28,7 +29,7 @@ export function buildTelegramInitData(userId: number, botToken: string): string } export function authenticateRequest(request: Test, userId = 1, botToken?: string): Test { - const token = botToken ?? process.env.BOT_TOKEN ?? 'test-bot-token'; + const token = botToken ?? getTelegramBotToken() ?? 'test-bot-token'; const initData = buildTelegramInitData(userId, token); return request.set('X-Telegram-Init-Data', initData); }