113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { SearchService } from './search.service';
|
|
import * as searchPkg from '@tower/search';
|
|
|
|
jest.mock('@tower/search', () => ({
|
|
createMeiliClient: jest.fn(),
|
|
configureIndex: jest.fn().mockResolvedValue(undefined),
|
|
MESSAGES_INDEX: 'tower-messages',
|
|
}));
|
|
|
|
describe('SearchService', () => {
|
|
let service: SearchService;
|
|
const mockSearch = jest.fn();
|
|
const mockIndex = jest.fn().mockReturnValue({ search: mockSearch });
|
|
const mockClient = { index: mockIndex };
|
|
|
|
beforeEach(async () => {
|
|
jest.clearAllMocks();
|
|
(searchPkg.createMeiliClient as jest.Mock).mockReturnValue(mockClient);
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
SearchService,
|
|
{ provide: ConfigService, useValue: { get: jest.fn().mockReturnValue('') } },
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<SearchService>(SearchService);
|
|
await service.onModuleInit();
|
|
});
|
|
|
|
it('calls configureIndex on init', () => {
|
|
expect(searchPkg.configureIndex).toHaveBeenCalledWith(mockClient);
|
|
});
|
|
|
|
it('always filters by tenantId', async () => {
|
|
mockSearch.mockResolvedValue({ hits: [], totalHits: 0 });
|
|
await service.search('tnt-1', 'test');
|
|
expect(mockSearch).toHaveBeenCalledWith(
|
|
'test',
|
|
expect.objectContaining({ filter: 'tenantId = "tnt-1"' }),
|
|
);
|
|
});
|
|
|
|
it('returns hits and total', async () => {
|
|
mockSearch.mockResolvedValue({ hits: [{ id: 'msg-1', content: 'hello' }], totalHits: 1 });
|
|
const result = await service.search('tnt-1', 'hello');
|
|
expect(result.hits).toHaveLength(1);
|
|
expect(result.total).toBe(1);
|
|
expect(result.query).toBe('hello');
|
|
});
|
|
|
|
it('applies sourceGroupId filter alongside tenant', async () => {
|
|
mockSearch.mockResolvedValue({ hits: [], totalHits: 0 });
|
|
await service.search('tnt-1', 'hello', 'grp-1');
|
|
expect(mockSearch).toHaveBeenCalledWith(
|
|
'hello',
|
|
expect.objectContaining({ filter: 'tenantId = "tnt-1" AND sourceGroupId = "grp-1"' }),
|
|
);
|
|
});
|
|
|
|
it('applies tags filter alongside tenant', async () => {
|
|
mockSearch.mockResolvedValue({ hits: [], totalHits: 0 });
|
|
await service.search('tnt-1', 'hello', undefined, ['#important']);
|
|
expect(mockSearch).toHaveBeenCalledWith(
|
|
'hello',
|
|
expect.objectContaining({ filter: 'tenantId = "tnt-1" AND tags = "#important"' }),
|
|
);
|
|
});
|
|
|
|
it('combines groupId and tags filters with AND, all behind tenant', async () => {
|
|
mockSearch.mockResolvedValue({ hits: [], totalHits: 0 });
|
|
await service.search('tnt-1', 'hello', 'grp-1', ['#important', '#event']);
|
|
expect(mockSearch).toHaveBeenCalledWith(
|
|
'hello',
|
|
expect.objectContaining({
|
|
filter:
|
|
'tenantId = "tnt-1" AND sourceGroupId = "grp-1" AND tags = "#important" AND tags = "#event"',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('defaults page to 1 and hitsPerPage to 20', async () => {
|
|
mockSearch.mockResolvedValue({ hits: [], totalHits: 0 });
|
|
await service.search('tnt-1', 'hello');
|
|
expect(mockSearch).toHaveBeenCalledWith(
|
|
'hello',
|
|
expect.objectContaining({ page: 1, hitsPerPage: 20 }),
|
|
);
|
|
});
|
|
|
|
it('escapes double-quotes in filter values to prevent injection', async () => {
|
|
mockSearch.mockResolvedValue({ hits: [], totalHits: 0 });
|
|
await service.search('tnt-1', 'hello', 'grp"1"OR id EXISTS');
|
|
expect(mockSearch).toHaveBeenCalledWith(
|
|
'hello',
|
|
expect.objectContaining({
|
|
filter: 'tenantId = "tnt-1" AND sourceGroupId = "grp\\"1\\"OR id EXISTS"',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('clamps page to min 1 and limit to max 100', async () => {
|
|
mockSearch.mockResolvedValue({ hits: [], totalHits: 0 });
|
|
await service.search('tnt-1', 'hello', undefined, undefined, 0, 999);
|
|
expect(mockSearch).toHaveBeenCalledWith(
|
|
'hello',
|
|
expect.objectContaining({ page: 1, hitsPerPage: 100 }),
|
|
);
|
|
});
|
|
});
|