import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { KnowledgeService } from "./knowledge.service"; import { PrismaService } from "../prisma/prisma.service"; import { LinkSyncService } from "./services/link-sync.service"; import { NotFoundException } from "@nestjs/common"; describe("KnowledgeService - Version History", () => { let service: KnowledgeService; let prisma: PrismaService; let linkSync: LinkSyncService; const workspaceId = "workspace-123"; const userId = "user-456"; const entryId = "entry-789"; const slug = "test-entry"; const mockEntry = { id: entryId, workspaceId, slug, title: "Test Entry", content: "# Test Content", contentHtml: "

Test Content

", summary: "Test summary", status: "PUBLISHED", visibility: "WORKSPACE", createdAt: new Date("2026-01-01"), updatedAt: new Date("2026-01-20"), createdBy: userId, updatedBy: userId, }; const mockVersions = [ { id: "version-3", entryId, version: 3, title: "Test Entry v3", content: "# Version 3", summary: "Summary v3", createdAt: new Date("2026-01-20"), createdBy: userId, changeNote: "Updated content", author: { id: userId, name: "Test User", email: "test@example.com", }, }, { id: "version-2", entryId, version: 2, title: "Test Entry v2", content: "# Version 2", summary: "Summary v2", createdAt: new Date("2026-01-15"), createdBy: userId, changeNote: "Second version", author: { id: userId, name: "Test User", email: "test@example.com", }, }, { id: "version-1", entryId, version: 1, title: "Test Entry v1", content: "# Version 1", summary: "Summary v1", createdAt: new Date("2026-01-10"), createdBy: userId, changeNote: "Initial version", author: { id: userId, name: "Test User", email: "test@example.com", }, }, ]; const mockPrismaService = { knowledgeEntry: { findUnique: vi.fn(), update: vi.fn(), }, knowledgeEntryVersion: { count: vi.fn(), findMany: vi.fn(), findUnique: vi.fn(), create: vi.fn(), }, $transaction: vi.fn(), }; const mockLinkSyncService = { syncLinks: vi.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ KnowledgeService, { provide: PrismaService, useValue: mockPrismaService, }, { provide: LinkSyncService, useValue: mockLinkSyncService, }, ], }).compile(); service = module.get(KnowledgeService); prisma = module.get(PrismaService); linkSync = module.get(LinkSyncService); vi.clearAllMocks(); }); describe("findVersions", () => { it("should return paginated versions for an entry", async () => { mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry); mockPrismaService.knowledgeEntryVersion.count.mockResolvedValue(3); mockPrismaService.knowledgeEntryVersion.findMany.mockResolvedValue(mockVersions); const result = await service.findVersions(workspaceId, slug, 1, 20); expect(result).toEqual({ data: mockVersions, pagination: { page: 1, limit: 20, total: 3, totalPages: 1, }, }); expect(mockPrismaService.knowledgeEntry.findUnique).toHaveBeenCalledWith({ where: { workspaceId_slug: { workspaceId, slug, }, }, }); expect(mockPrismaService.knowledgeEntryVersion.count).toHaveBeenCalledWith({ where: { entryId }, }); expect(mockPrismaService.knowledgeEntryVersion.findMany).toHaveBeenCalledWith({ where: { entryId }, include: { author: { select: { id: true, name: true, email: true, }, }, }, orderBy: { version: "desc", }, skip: 0, take: 20, }); }); it("should handle pagination correctly", async () => { mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry); mockPrismaService.knowledgeEntryVersion.count.mockResolvedValue(50); mockPrismaService.knowledgeEntryVersion.findMany.mockResolvedValue([mockVersions[0]]); const result = await service.findVersions(workspaceId, slug, 2, 20); expect(result.pagination).toEqual({ page: 2, limit: 20, total: 50, totalPages: 3, }); expect(mockPrismaService.knowledgeEntryVersion.findMany).toHaveBeenCalledWith( expect.objectContaining({ skip: 20, // (page 2 - 1) * 20 take: 20, }) ); }); it("should throw NotFoundException if entry does not exist", async () => { mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(null); await expect(service.findVersions(workspaceId, slug)).rejects.toThrow(NotFoundException); expect(mockPrismaService.knowledgeEntryVersion.count).not.toHaveBeenCalled(); }); }); describe("findVersion", () => { it("should return a specific version", async () => { mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry); mockPrismaService.knowledgeEntryVersion.findUnique.mockResolvedValue(mockVersions[1]); const result = await service.findVersion(workspaceId, slug, 2); expect(result).toEqual(mockVersions[1]); expect(mockPrismaService.knowledgeEntryVersion.findUnique).toHaveBeenCalledWith({ where: { entryId_version: { entryId, version: 2, }, }, include: { author: { select: { id: true, name: true, email: true, }, }, }, }); }); it("should throw NotFoundException if entry does not exist", async () => { mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(null); await expect(service.findVersion(workspaceId, slug, 2)).rejects.toThrow(NotFoundException); }); it("should throw NotFoundException if version does not exist", async () => { mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry); mockPrismaService.knowledgeEntryVersion.findUnique.mockResolvedValue(null); await expect(service.findVersion(workspaceId, slug, 99)).rejects.toThrow(NotFoundException); }); }); describe("restoreVersion", () => { it("should restore a previous version and create a new version", async () => { const entryWithVersions = { ...mockEntry, versions: [mockVersions[0]], // Latest version is v3 tags: [], }; const updatedEntry = { ...mockEntry, title: "Test Entry v2", content: "# Version 2", contentHtml: "

Version 2

", summary: "Summary v2", tags: [], }; // Mock findVersion to return version 2 mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(mockEntry); mockPrismaService.knowledgeEntryVersion.findUnique.mockResolvedValue(mockVersions[1]); // Mock transaction mockPrismaService.$transaction.mockImplementation(async (callback) => { const tx = { knowledgeEntry: { update: vi.fn().mockResolvedValue(updatedEntry), findUnique: vi.fn().mockResolvedValue({ ...updatedEntry, tags: [], }), }, knowledgeEntryVersion: { create: vi.fn().mockResolvedValue({ id: "version-4", entryId, version: 4, title: "Test Entry v2", content: "# Version 2", summary: "Summary v2", createdAt: new Date(), createdBy: userId, changeNote: "Restored from version 2", }), }, }; return callback(tx); }); // Mock for findVersion call mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(entryWithVersions); const result = await service.restoreVersion(workspaceId, slug, 2, userId, "Custom restore note"); expect(result.title).toBe("Test Entry v2"); expect(result.content).toBe("# Version 2"); expect(mockLinkSyncService.syncLinks).toHaveBeenCalledWith( workspaceId, entryId, "# Version 2" ); }); it("should use default change note if not provided", async () => { const entryWithVersions = { ...mockEntry, versions: [mockVersions[0]], tags: [], }; mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(mockEntry); mockPrismaService.knowledgeEntryVersion.findUnique.mockResolvedValue(mockVersions[1]); mockPrismaService.$transaction.mockImplementation(async (callback) => { const createMock = vi.fn(); const tx = { knowledgeEntry: { update: vi.fn().mockResolvedValue(mockEntry), findUnique: vi.fn().mockResolvedValue({ ...mockEntry, tags: [] }), }, knowledgeEntryVersion: { create: createMock, }, }; await callback(tx); return { ...mockEntry, tags: [] }; }); mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(entryWithVersions); await service.restoreVersion(workspaceId, slug, 2, userId); // Verify transaction was called expect(mockPrismaService.$transaction).toHaveBeenCalled(); }); it("should throw NotFoundException if entry does not exist", async () => { mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(null); await expect(service.restoreVersion(workspaceId, slug, 2, userId)).rejects.toThrow( NotFoundException ); }); }); });