Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions src/middlewares/shutdownMode.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
jest.mock('@/helpers/log', () => ({
log: { error: jest.fn(), info: jest.fn() },
}))

import { log } from '@/helpers/log'
import { SHUTDOWN_MESSAGE, shutdownMode } from '@/middlewares/shutdownMode'

const makeCtx = () =>
({
reply: jest.fn().mockResolvedValue(undefined),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
type Ctx = {
chat?: { id: number }
reply: jest.Mock
}

const makeCtx = (overrides: Partial<Ctx> = {}): Ctx => ({
chat: { id: 1 },
reply: jest.fn().mockResolvedValue(undefined),
...overrides,
})

describe('shutdownMode middleware', () => {
const originalEnv = process.env

beforeEach(() => {
jest.clearAllMocks()
})

afterEach(() => {
process.env = { ...originalEnv }
})
Expand All @@ -17,7 +31,8 @@ describe('shutdownMode middleware', () => {
delete process.env.SHUTDOWN_MODE
const ctx = makeCtx()
const next = jest.fn()
await shutdownMode(ctx, next)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await shutdownMode(ctx as any, next)
expect(next).toHaveBeenCalledTimes(1)
expect(ctx.reply).not.toHaveBeenCalled()
})
Expand All @@ -26,8 +41,35 @@ describe('shutdownMode middleware', () => {
process.env.SHUTDOWN_MODE = 'true'
const ctx = makeCtx()
const next = jest.fn()
await shutdownMode(ctx, next)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await shutdownMode(ctx as any, next)
expect(next).not.toHaveBeenCalled()
expect(ctx.reply).toHaveBeenCalledWith(SHUTDOWN_MESSAGE)
})

// The Telegraf 3.x polling loop kills itself permanently on any middleware
// throw (telegraf.js fetchUpdates -> handleUpdates rejection sets
// polling.started = false). The two cases below pin the two ways this
// middleware used to leak a rejection.
it('skips chatless updates instead of throwing on ctx.reply', async () => {
process.env.SHUTDOWN_MODE = 'true'
const ctx = makeCtx({ chat: undefined })
const next = jest.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await expect(shutdownMode(ctx as any, next)).resolves.toBeUndefined()
expect(next).not.toHaveBeenCalled()
expect(ctx.reply).not.toHaveBeenCalled()
})

it('swallows reply failures (e.g. user blocked the bot)', async () => {
process.env.SHUTDOWN_MODE = 'true'
const ctx = makeCtx({
reply: jest.fn().mockRejectedValue(new Error('Forbidden: bot blocked')),
})
const next = jest.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await expect(shutdownMode(ctx as any, next)).resolves.toBeUndefined()
expect(ctx.reply).toHaveBeenCalledWith(SHUTDOWN_MESSAGE)
expect(log.error).toHaveBeenCalled()
})
})
16 changes: 15 additions & 1 deletion src/middlewares/shutdownMode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Context } from 'telegraf'

import { isShutdownMode } from '@/helpers/isShutdownMode'
import { log } from '@/helpers/log'

/* eslint-disable max-len */
export const SHUTDOWN_MESSAGE = `привет. я сделал гуся пять лет назад — тогда нигде не было удобных алертов по ценам. начал для себя, потом подтянулись люди.
Expand All @@ -23,5 +24,18 @@ export const SHUTDOWN_MESSAGE = `привет. я сделал гуся пять
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function shutdownMode(ctx: Context, next: () => any) {
if (!isShutdownMode()) return next()
await ctx.reply(SHUTDOWN_MESSAGE)
// Skip updates without a chat (my_chat_member, chat_join_request, etc.).
// ctx.reply asserts ctx.chat and throws otherwise; an unhandled throw inside
// a middleware kills Telegraf 3.x's polling loop permanently (see
// node_modules/telegraf/telegraf.js fetchUpdates: it flips polling.started
// to false on any handleUpdates rejection), so a single chatless update
// would stop the bot from ever reading another /help.
if (!ctx.chat) return
try {
await ctx.reply(SHUTDOWN_MESSAGE)
} catch (e) {
// Per-send failures (e.g. user blocked the bot, Telegram rate-limit) must
// not escape: same polling-loop kill switch as above.
log.error('[SHUTDOWN MODE] reply failed', e)
}
}
Loading