Skip to content
Closed
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
50 changes: 50 additions & 0 deletions backend/__tests__/unit/routes/telegram.webhook.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,56 @@ describe('bridge command surface (/mode /status /mute /help)', () => {
);
});

// The real buffer handler is async and can reject (it awaits a Mongo
// write). A synchronous mock here would let an un-awaited
// `return events(req, res)` pass — the rejection must reach the catch so
// the claim is released and Telegram's redelivery is a retry, not a
// duplicate-acked drop.
it('releases the claim when the async buffer handler rejects', async () => {
const integration = {
_id: 'integration-1',
type: 'telegram',
config: { chatId: '42' },
};
Integration.findOne = jest.fn().mockResolvedValue(integration);
const events = jest.fn(async () => {
throw new Error('provider write failed');
});
registry.get.mockReturnValue({ getWebhookHandlers: () => ({ events }) });
const res = await request(app)
.post('/api/webhooks/telegram')
.send(liveUpdate(6));
expect(res.status).toBe(500);
expect(events).toHaveBeenCalled();
expect(WebhookDelivery.deleteOne).toHaveBeenCalledWith(
{ provider: 'telegram', deliveryId: '6' },
);
});

// Updates with no update_id must bypass the claim entirely: without the
// guard they would all claim the literal key 'undefined' — the first one
// takes it and every later un-id'd update is acked and dropped.
it('processes every update that carries no update_id', async () => {
const integration = {
_id: 'integration-1',
type: 'telegram',
podId: 'pod-1',
config: { chatId: '42', liveRelay: true },
};
Integration.findOne = jest.fn().mockResolvedValue(integration);
bridge.relayTelegramMessageToPod.mockResolvedValue({});
const noIdUpdate = liveUpdate(undefined);
delete noIdUpdate.update_id;

const first = await request(app).post('/api/webhooks/telegram').send(noIdUpdate);
const second = await request(app).post('/api/webhooks/telegram').send(noIdUpdate);

expect(first.status).toBe(200);
expect(second.status).toBe(200);
expect(bridge.relayTelegramMessageToPod).toHaveBeenCalledTimes(2);
expect(WebhookDelivery.create).not.toHaveBeenCalled();
});

it('a dedup-store outage does not take the bridge down', async () => {
WebhookDelivery.create.mockRejectedValueOnce(new Error('mongo down'));
const integration = {
Expand Down
6 changes: 6 additions & 0 deletions backend/models/WebhookDelivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import mongoose, { Document, Schema } from 'mongoose';
// routes/webhooks/telegram.ts.)
// The TTL bounds the table: a delivery id only needs to be remembered for as
// long as the provider keeps redelivering it.
//
// Key scope: {provider, deliveryId} assumes ONE id space per provider.
// Telegram's update_id is sequential per BOT — correct while the route serves
// a single bot; a second bot on the same route would collide id spaces and
// drop legitimate updates as duplicates. Widen the key (e.g. include bot id)
// before multi-bot.
export interface IWebhookDelivery extends Document {
provider: string;
deliveryId: string;
Expand Down
5 changes: 4 additions & 1 deletion backend/routes/webhooks/telegram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,10 @@ router.post('/', async (req: any, res: any) => {

const provider = registry.get('telegram', integration);
const { events } = provider.getWebhookHandlers();
return events(req, res);
// await, not return: a rejected promise returned from inside `try` escapes
// this catch (express 4 won't catch it either), and the claim above would
// survive to swallow Telegram's redelivery.
return await events(req, res);
} catch (error) {
console.error('Telegram webhook error', error);
// Forget-on-error: the 500 makes Telegram redeliver this update_id; the
Expand Down
Loading