import { beforeEach, describe, expect, test, vi } from "vitest"; import { runWithRlsContext } from "@hivegrid/db/rls-context.server"; const getAllKnowledgeBaseArticlesMock = vi.fn(); const createKnowledgeBaseArticleMock = vi.fn(); const getAllKnowledgeBaseCategoriesMock = vi.fn(); const getFixedTMock = vi.fn(); const knowledgeBaseArticleCountMock = vi.fn(); const knowledgeBaseCategoryCountMock = vi.fn(); const knowledgeBaseDeleteMock = vi.fn(); const getKnowledgeBaseByIdMock = vi.fn(); const cachifiedMock = vi.fn(); vi.mock("../db/kbArticles.db.server", () => ({ createKnowledgeBaseArticle: (...args: unknown[]) => createKnowledgeBaseArticleMock(...args), getAllKnowledgeBaseArticles: (...args: unknown[]) => getAllKnowledgeBaseArticlesMock(...args), getFeaturedKnowledgeBaseArticles: vi.fn(), getKbArticleById: vi.fn(), getKbArticleByIdInKnowledgeBase: vi.fn(), getKbArticleBySlug: vi.fn(), })); vi.mock("../db/kbCategories.db.server", () => ({ createKnowledgeBaseCategory: vi.fn(), getAllKnowledgeBaseCategories: (...args: unknown[]) => getAllKnowledgeBaseCategoriesMock(...args), getKbCategoryById: vi.fn(), getKbCategoryBySlug: vi.fn(), })); vi.mock("../db/knowledgeBase.db.server", () => ({ getAllKnowledgeBases: vi.fn(), getKnowledgeBaseById: (...args: unknown[]) => getKnowledgeBaseByIdMock(...args), getKnowledgeBaseBySlug: vi.fn(), })); vi.mock("../services/kbArticleSearch.server", () => ({ searchKnowledgeBaseArticles: vi.fn(), })); vi.mock("~/locale/i18next.server", () => ({ remixI18Next: { getFixedT: (...args: unknown[]) => getFixedTMock(...args), }, })); vi.mock("~/utils/cache.server", () => ({ cachified: (...args: unknown[]) => cachifiedMock(...args), })); vi.mock("~/utils/db.server", () => ({ db: { knowledgeBaseArticle: { count: (...args: unknown[]) => knowledgeBaseArticleCountMock(...args) }, knowledgeBaseCategory: { count: (...args: unknown[]) => knowledgeBaseCategoryCountMock(...args) }, knowledgeBase: { delete: (...args: unknown[]) => knowledgeBaseDeleteMock(...args) }, }, })); describe("KnowledgeBaseServiceServer", () => { beforeEach(() => { vi.clearAllMocks(); getAllKnowledgeBaseArticlesMock.mockResolvedValue([ { id: "article-1", slug: "untitled", order: 1, }, ]); getAllKnowledgeBaseCategoriesMock.mockResolvedValue([]); createKnowledgeBaseArticleMock.mockImplementation(async (input) => ({ id: "created", ...input })); getFixedTMock.mockImplementation(async (locale: string) => (key: string, vars?: Record) => `${locale}:${key}:${vars?.number ?? ""}`); knowledgeBaseArticleCountMock.mockResolvedValue(0); knowledgeBaseCategoryCountMock.mockResolvedValue(0); knowledgeBaseDeleteMock.mockResolvedValue({ id: "kb-1" }); getKnowledgeBaseByIdMock.mockResolvedValue(null); cachifiedMock.mockImplementation(async ({ getFreshValue }: { getFreshValue: () => unknown }) => getFreshValue()); }); test("newArticle localizes generated untitled titles and persists the resolved language", async () => { const KnowledgeBaseServiceServer = (await import("./KnowledgeBaseService.server")).default; const created = await KnowledgeBaseServiceServer.newArticle({ kb: { id: "kb-1", slug: "docs", defaultLanguage: "de", } as never, params: {}, userId: "user-1", position: "last", }); expect(getFixedTMock).toHaveBeenCalledWith("de", "translations"); expect(createKnowledgeBaseArticleMock).toHaveBeenCalledWith( expect.objectContaining({ title: "de:knowledgeBase.adminCategories.untitledNumbered:1", language: "de" }) ); expect(created).toMatchObject({ title: "de:knowledgeBase.adminCategories.untitledNumbered:1", language: "de" }); }); test("public article DTOs never expose draft content or author identity", async () => { const publishedAt = new Date("2026-07-14T00:00:00.000Z"); getAllKnowledgeBaseArticlesMock.mockResolvedValueOnce([ { id: "article-public", createdAt: publishedAt, updatedAt: publishedAt, slug: "published", title: "Published", description: "Public description", order: 0, contentDraft: "private draft", contentPublished: "public content", contentPublishedAsText: "public content", contentType: "markdown", language: "de", featuredOrder: null, createdByUser: { firstName: "Private", lastName: "Author", avatar: null }, publishedAt, sectionId: null, relatedArticles: [], seoImage: "", }, ]); const KnowledgeBaseServiceServer = (await import("./KnowledgeBaseService.server")).default; const kb = { id: "kb-public", slug: "docs", title: "Docs", defaultLanguage: "de", } as never; const articles = await runWithRlsContext({ publicKnowledgeBaseId: "kb-public" }, () => KnowledgeBaseServiceServer.getArticles({ kb, categoryId: "category-public", language: "de", query: undefined, params: {}, request: new Request("https://example.test/docs"), }) ); expect(articles).toHaveLength(1); expect(articles[0]).toMatchObject({ contentDraft: "", contentPublished: "public content", createdByUser: null, }); }); test("public KB lookup re-reads the capability-bound id instead of using the shared slug cache", async () => { getKnowledgeBaseByIdMock.mockResolvedValueOnce({ id: "kb-public", createdAt: new Date("2026-07-14T00:00:00.000Z"), updatedAt: new Date("2026-07-14T00:00:00.000Z"), basePath: "/docs", slug: "docs", title: "Docs", description: "Public docs", defaultLanguage: "de", layout: "docs", color: 0, enabled: true, languages: '["de"]', links: "[]", logo: "", seoImage: "", _count: { categories: 1, articles: 1, views: 0 }, }); const KnowledgeBaseServiceServer = (await import("./KnowledgeBaseService.server")).default; await expect( runWithRlsContext({ publicKnowledgeBaseId: "kb-public" }, () => KnowledgeBaseServiceServer.get({ slug: "docs", enabled: true, request: new Request("https://example.test/docs") }) ) ).resolves.toMatchObject({ id: "kb-public", slug: "docs", enabled: true }); expect(getKnowledgeBaseByIdMock).toHaveBeenCalledWith("kb-public"); expect(cachifiedMock).not.toHaveBeenCalled(); }); test("del exposes coded delete guard errors for localized route handling", async () => { const { default: KnowledgeBaseServiceServer, getKnowledgeBaseActionErrorKey } = await import("./KnowledgeBaseService.server"); knowledgeBaseArticleCountMock.mockResolvedValueOnce(1); try { await KnowledgeBaseServiceServer.del({ id: "kb-1" } as never); throw new Error("Expected delete to reject"); } catch (error) { expect(error).toMatchObject({ code: "cannot-delete-with-articles" }); expect(getKnowledgeBaseActionErrorKey(error)).toBe("knowledgeBase.admin.errors.cannotDeleteWithArticles"); } }); test("duplicate and new article validation errors map to semantic i18n keys", async () => { const { default: KnowledgeBaseServiceServer, getKnowledgeBaseActionErrorKey } = await import("./KnowledgeBaseService.server"); await expect( KnowledgeBaseServiceServer.duplicateArticle({ kb: { id: "kb-1", slug: "docs", defaultLanguage: "en" } as never, language: "en", articleId: "missing" }) ).rejects.toMatchObject({ code: "invalid-article" }); try { await KnowledgeBaseServiceServer.newArticle({ kb: { id: "kb-1", slug: "docs", defaultLanguage: "en" } as never, params: {}, userId: "user-1", position: "last", categoryId: "missing", }); } catch (error) { expect(getKnowledgeBaseActionErrorKey(error)).toBe("knowledgeBase.adminCategories.errors.categoryNotFound"); } }); test("public lookup errors map to public i18n keys", async () => { const { default: KnowledgeBaseServiceServer, getKnowledgeBasePublicErrorKey } = await import("./KnowledgeBaseService.server"); cachifiedMock.mockResolvedValueOnce(null); try { await KnowledgeBaseServiceServer.get({ slug: "missing", enabled: true, request: new Request("https://example.com/help") }); throw new Error("Expected lookup to reject"); } catch (error) { expect(error).toMatchObject({ code: "not-found" }); expect(getKnowledgeBasePublicErrorKey(error)).toBe("knowledgeBase.public.errors.notFound"); } cachifiedMock.mockResolvedValueOnce({ title: "Draft docs", enabled: false }); try { await KnowledgeBaseServiceServer.get({ slug: "draft", enabled: true, request: new Request("https://example.com/help") }); throw new Error("Expected disabled knowledge base to reject"); } catch (error) { expect(error).toMatchObject({ code: "disabled" }); expect(getKnowledgeBasePublicErrorKey(error)).toBe("knowledgeBase.public.errors.disabled"); } }); });