Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/knowledgeBase/services/kbArticleSearch.server.test.ts
T

80 lines
2.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
createSearchClient: vi.fn(),
searchWithFallback: vi.fn(),
findKnowledgeBaseArticles: vi.fn(),
findKnowledgeBaseArticlesByIds: vi.fn(),
}));
vi.mock("@toolctrl/search", () => ({
createSearchClient: mocks.createSearchClient,
kbArticlesIndex: { name: "kb_articles", primaryKey: "id" },
searchWithFallback: mocks.searchWithFallback,
}));
vi.mock("../db/kbArticles.db.server", () => ({
findKnowledgeBaseArticles: mocks.findKnowledgeBaseArticles,
findKnowledgeBaseArticlesByIds: mocks.findKnowledgeBaseArticlesByIds,
}));
import { searchKnowledgeBaseArticles } from "./kbArticleSearch.server";
const originalEnv = { ...process.env };
describe("searchKnowledgeBaseArticles", () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.MEILI_URL = "http://meili.test";
delete process.env.SEARCH_BACKEND;
mocks.createSearchClient.mockReturnValue({ index: vi.fn() });
});
afterEach(() => {
process.env = { ...originalEnv };
});
it("hydrates Meilisearch hits by ID instead of scanning the full SQL search set", async () => {
const articleA = { id: "article_a", title: "A" };
const articleB = { id: "article_b", title: "B" };
mocks.searchWithFallback.mockResolvedValue({
hits: [{ document: { id: "article_b" } }, { document: { id: "article_a" } }],
estimatedTotalHits: 2,
processingTimeMs: 3,
});
mocks.findKnowledgeBaseArticlesByIds.mockResolvedValue([articleA, articleB]);
await expect(
searchKnowledgeBaseArticles({
knowledgeBaseSlug: "docs",
language: "de",
query: "billing",
tenantId: "tenant_1",
})
).resolves.toEqual([articleB, articleA]);
expect(mocks.findKnowledgeBaseArticlesByIds).toHaveBeenCalledWith({
knowledgeBaseSlug: "docs",
language: "de",
ids: ["article_b", "article_a"],
});
expect(mocks.findKnowledgeBaseArticles).not.toHaveBeenCalled();
});
it("escapes the language value in the Meilisearch filter to prevent filter injection", async () => {
mocks.searchWithFallback.mockResolvedValue({ hits: [], estimatedTotalHits: 0, processingTimeMs: 1 });
await searchKnowledgeBaseArticles({
knowledgeBaseSlug: "docs",
// A crafted language attempting to break out of the quoted filter string.
language: 'de" OR isPublished = "true',
query: "billing",
tenantId: "tenant_1",
});
const passedOptions = mocks.searchWithFallback.mock.calls[0]?.[0]?.options;
expect(passedOptions.extraFilters).toBe('language = "de\\" OR isPublished = \\"true" AND isPublished = true');
});
});