Spaces:
Runtime error
Runtime error
| import { BadRequestException, NotImplementedException } from '@nestjs/common'; | |
| import { SearchController } from './search.controller'; | |
| import { SearchService } from './search.service'; | |
| import { SearchQueryDto } from './dto/search-query.dto'; | |
| import type { ApiKey } from '../auth/entities/api-key.entity'; | |
| import type { SearchResults } from './search.types'; | |
| describe('SearchController', () => { | |
| const search = jest.fn(); | |
| const ctrl = new SearchController({ search } as unknown as SearchService); | |
| const ok = { hits: [], total: 0, tookMs: 1, provider: 'builtin-fts' } satisfies SearchResults; | |
| beforeEach(() => { | |
| search.mockReset(); | |
| }); | |
| it('throws 400 when q is empty', async () => { | |
| const dto: SearchQueryDto = { q: '' }; | |
| await expect(ctrl.search(dto, undefined)).rejects.toBeInstanceOf(BadRequestException); | |
| }); | |
| it('throws 400 when q is only whitespace', async () => { | |
| // @IsNotEmpty() on the DTO passes for ' ' (non-empty), so the controller's own trim guard is | |
| // what rejects whitespace-only q β this test pins that guard in place. | |
| const dto: SearchQueryDto = { q: ' ' }; | |
| await expect(ctrl.search(dto, undefined)).rejects.toBeInstanceOf(BadRequestException); | |
| expect(search).not.toHaveBeenCalled(); | |
| }); | |
| it('returns results and forwards the key allowedSessions as callerSessionIds', async () => { | |
| search.mockResolvedValue(ok); | |
| const apiKey = { allowedSessions: ['s1', 's2'] } as unknown as ApiKey; | |
| const dto: SearchQueryDto = { q: 'hello', limit: 5 }; | |
| const res = await ctrl.search(dto, apiKey); | |
| expect(search).toHaveBeenCalledWith(dto, ['s1', 's2']); | |
| expect(res).toBe(ok); | |
| }); | |
| it('passes undefined (no scope) for an unrestricted key (null allowedSessions)', async () => { | |
| search.mockResolvedValue(ok); | |
| // A null/empty allowlist (e.g. ADMIN) sees all sessions β mirrors GET /webhooks behavior. | |
| const apiKey = { allowedSessions: null } as unknown as ApiKey; | |
| const dto: SearchQueryDto = { q: 'hello' }; | |
| await ctrl.search(dto, apiKey); | |
| expect(search).toHaveBeenCalledWith(dto, undefined); | |
| }); | |
| it('derives callerSessionIds only from the key, never from the query (anti-smuggling)', async () => { | |
| search.mockResolvedValue(ok); | |
| // `sessionIds` is not a field on SearchQueryDto, so it cannot be expressed in the query at all; | |
| // the global ValidationPipe (forbidNonWhitelisted) would reject it, and SearchService clobbers | |
| // any sessionIds at the provider boundary. The controller itself derives scope solely from the | |
| // key β here there is no key β undefined. | |
| const dto: SearchQueryDto = { q: 'hello' }; | |
| await ctrl.search(dto, undefined); | |
| expect(search).toHaveBeenCalledWith(dto, undefined); | |
| }); | |
| it('propagates 501 (NotImplementedException) from the service when no provider is active', async () => { | |
| search.mockRejectedValue(new NotImplementedException('none')); | |
| await expect(ctrl.search({ q: 'x' }, undefined)).rejects.toBeInstanceOf(NotImplementedException); | |
| }); | |
| }); | |