Spaces:
Runtime error
Runtime error
| // webhook by-id operations (findOne/update/delete/test) must be scoped to the URL | |
| // :sessionId, and GET /webhooks must be scoped to the key's allowedSessions. Without it, an | |
| // OPERATOR key for one session can read/edit/delete/redirect/fire another session's webhook, | |
| // and enumerate every session's webhook URLs. These run against a real in-memory DB so the | |
| // scoping is exercised end-to-end, not asserted on a mock's WHERE clause. | |
| import { DataSource } from 'typeorm'; | |
| import { NotFoundException } from '@nestjs/common'; | |
| import { WebhookService } from './webhook.service'; | |
| import { Webhook } from './entities/webhook.entity'; | |
| import { Session, SessionStatus } from '../session/entities/session.entity'; | |
| describe('WebhookService session-scoped access', () => { | |
| let ds: DataSource; | |
| let service: WebhookService; | |
| let whA: Webhook; | |
| let whB: Webhook; | |
| beforeEach(async () => { | |
| ds = new DataSource({ | |
| type: 'better-sqlite3', | |
| database: ':memory:', | |
| entities: [Session, Webhook], | |
| synchronize: true, | |
| }); | |
| await ds.initialize(); | |
| const repo = ds.getRepository(Webhook); | |
| const cfg = { get: () => false }; // queue.enabled = false | |
| // 2nd arg is the delivery-failure repo, unused by the scoped read/update/delete paths under test. | |
| service = new WebhookService(repo, {} as never, cfg as never, {} as never, undefined); | |
| const sessions = ds.getRepository(Session); | |
| for (const id of ['sessA', 'sessB']) { | |
| await sessions.save(sessions.create({ id, name: id, status: SessionStatus.READY, config: {} })); | |
| } | |
| whA = await repo.save( | |
| repo.create({ | |
| sessionId: 'sessA', | |
| url: 'https://a.example/hook', | |
| events: ['message.received'], | |
| headers: {}, | |
| retryCount: 3, | |
| }), | |
| ); | |
| whB = await repo.save( | |
| repo.create({ | |
| sessionId: 'sessB', | |
| url: 'https://b.example/hook', | |
| events: ['message.received'], | |
| headers: {}, | |
| retryCount: 3, | |
| }), | |
| ); | |
| }); | |
| afterEach(async () => { | |
| await ds.destroy(); | |
| }); | |
| it('findOne returns a webhook only for its owning session', async () => { | |
| expect((await service.findOne('sessA', whA.id)).id).toBe(whA.id); | |
| await expect(service.findOne('sessA', whB.id)).rejects.toThrow(NotFoundException); | |
| }); | |
| it('update refuses (404) a webhook owned by another session and does not mutate it', async () => { | |
| await expect(service.update('sessA', whB.id, { url: 'https://evil.example/x' })).rejects.toThrow(NotFoundException); | |
| expect((await ds.getRepository(Webhook).findOneByOrFail({ id: whB.id })).url).toBe('https://b.example/hook'); | |
| }); | |
| it('delete refuses (404) a webhook owned by another session and does not remove it', async () => { | |
| await expect(service.delete('sessA', whB.id)).rejects.toThrow(NotFoundException); | |
| expect(await ds.getRepository(Webhook).countBy({ id: whB.id })).toBe(1); | |
| }); | |
| it('test refuses (404) to fire a webhook owned by another session', async () => { | |
| await expect(service.test('sessA', whB.id)).rejects.toThrow(NotFoundException); | |
| }); | |
| it('findAll scopes to allowedSessions when set, returns all when unrestricted', async () => { | |
| expect((await service.findAll(['sessA'])).map(w => w.id)).toEqual([whA.id]); | |
| expect((await service.findAll(null)).map(w => w.id).sort()).toEqual([whA.id, whB.id].sort()); | |
| expect((await service.findAll([])).length).toBe(2); // empty allowlist = unrestricted (matches the guard) | |
| }); | |
| }); | |