diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-05 23:34:24 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-05 23:34:24 +0900 |
| commit | 19bf3a9b2cf91e49af8c70f974a5b3fcf2bcd869 (patch) | |
| tree | 591fde02774492b90c8219cb389dd318178e54f5 /src/server/repositories/purge.test.ts | |
| parent | 504ff72fea72eb3d7c4cf45be1bd9620cb12a796 (diff) | |
| download | kioku-19bf3a9b2cf91e49af8c70f974a5b3fcf2bcd869.tar.gz kioku-19bf3a9b2cf91e49af8c70f974a5b3fcf2bcd869.tar.zst kioku-19bf3a9b2cf91e49af8c70f974a5b3fcf2bcd869.zip | |
feat(server): add purge mechanism for soft-deleted records
Automatically removes soft-deleted records older than 90 days.
Runs on server startup and every 24 hours thereafter.
Respects foreign key dependencies when deleting.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/server/repositories/purge.test.ts')
| -rw-r--r-- | src/server/repositories/purge.test.ts | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/src/server/repositories/purge.test.ts b/src/server/repositories/purge.test.ts new file mode 100644 index 0000000..efddd80 --- /dev/null +++ b/src/server/repositories/purge.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, it, vi } from "vitest"; +import type { PurgeOptions, PurgeRepository, PurgeResult } from "./purge.js"; + +function createMockPurgeResult( + overrides: Partial<PurgeResult> = {}, +): PurgeResult { + return { + reviewLogs: 0, + noteFieldValues: 0, + cards: 0, + notes: 0, + noteFieldTypes: 0, + noteTypes: 0, + decks: 0, + ...overrides, + }; +} + +function createMockPurgeRepo(): PurgeRepository { + return { + purgeDeletedRecords: vi.fn(), + }; +} + +describe("PurgeRepository mock factory", () => { + describe("createMockPurgeResult", () => { + it("creates a valid PurgeResult with all zeros by default", () => { + const result = createMockPurgeResult(); + + expect(result.reviewLogs).toBe(0); + expect(result.noteFieldValues).toBe(0); + expect(result.cards).toBe(0); + expect(result.notes).toBe(0); + expect(result.noteFieldTypes).toBe(0); + expect(result.noteTypes).toBe(0); + expect(result.decks).toBe(0); + }); + + it("allows overriding properties", () => { + const result = createMockPurgeResult({ + cards: 5, + notes: 3, + decks: 1, + }); + + expect(result.cards).toBe(5); + expect(result.notes).toBe(3); + expect(result.decks).toBe(1); + expect(result.reviewLogs).toBe(0); + }); + }); + + describe("createMockPurgeRepo", () => { + it("creates a repository with purgeDeletedRecords method", () => { + const repo = createMockPurgeRepo(); + + expect(repo.purgeDeletedRecords).toBeDefined(); + }); + + it("purgeDeletedRecords is mockable", async () => { + const repo = createMockPurgeRepo(); + const mockResult = createMockPurgeResult({ + cards: 10, + notes: 5, + reviewLogs: 25, + }); + + vi.mocked(repo.purgeDeletedRecords).mockResolvedValue(mockResult); + + const options: PurgeOptions = { retentionDays: 90 }; + const result = await repo.purgeDeletedRecords(options); + + expect(result.cards).toBe(10); + expect(result.notes).toBe(5); + expect(result.reviewLogs).toBe(25); + expect(repo.purgeDeletedRecords).toHaveBeenCalledWith(options); + }); + + it("respects batchSize option", async () => { + const repo = createMockPurgeRepo(); + const mockResult = createMockPurgeResult({ cards: 100 }); + + vi.mocked(repo.purgeDeletedRecords).mockResolvedValue(mockResult); + + const options: PurgeOptions = { retentionDays: 30, batchSize: 500 }; + await repo.purgeDeletedRecords(options); + + expect(repo.purgeDeletedRecords).toHaveBeenCalledWith({ + retentionDays: 30, + batchSize: 500, + }); + }); + }); +}); + +describe("PurgeResult interface contracts", () => { + it("PurgeResult has all required fields", () => { + const result = createMockPurgeResult(); + + expect(result).toHaveProperty("reviewLogs"); + expect(result).toHaveProperty("noteFieldValues"); + expect(result).toHaveProperty("cards"); + expect(result).toHaveProperty("notes"); + expect(result).toHaveProperty("noteFieldTypes"); + expect(result).toHaveProperty("noteTypes"); + expect(result).toHaveProperty("decks"); + }); + + it("all fields are numbers", () => { + const result = createMockPurgeResult({ + reviewLogs: 10, + noteFieldValues: 20, + cards: 30, + notes: 40, + noteFieldTypes: 50, + noteTypes: 60, + decks: 70, + }); + + expect(typeof result.reviewLogs).toBe("number"); + expect(typeof result.noteFieldValues).toBe("number"); + expect(typeof result.cards).toBe("number"); + expect(typeof result.notes).toBe("number"); + expect(typeof result.noteFieldTypes).toBe("number"); + expect(typeof result.noteTypes).toBe("number"); + expect(typeof result.decks).toBe("number"); + }); +}); + +describe("PurgeOptions interface contracts", () => { + it("retentionDays is required", async () => { + const repo = createMockPurgeRepo(); + vi.mocked(repo.purgeDeletedRecords).mockResolvedValue( + createMockPurgeResult(), + ); + + const options: PurgeOptions = { retentionDays: 90 }; + await repo.purgeDeletedRecords(options); + + expect(repo.purgeDeletedRecords).toHaveBeenCalledWith( + expect.objectContaining({ retentionDays: 90 }), + ); + }); + + it("batchSize is optional", async () => { + const repo = createMockPurgeRepo(); + vi.mocked(repo.purgeDeletedRecords).mockResolvedValue( + createMockPurgeResult(), + ); + + const optionsWithoutBatch: PurgeOptions = { retentionDays: 90 }; + const optionsWithBatch: PurgeOptions = { + retentionDays: 90, + batchSize: 500, + }; + + await repo.purgeDeletedRecords(optionsWithoutBatch); + await repo.purgeDeletedRecords(optionsWithBatch); + + expect(repo.purgeDeletedRecords).toHaveBeenCalledTimes(2); + }); +}); + +describe("Purge deletion order", () => { + it("returns counts for all entity types", async () => { + const repo = createMockPurgeRepo(); + const mockResult = createMockPurgeResult({ + reviewLogs: 100, + noteFieldValues: 50, + cards: 25, + notes: 20, + noteFieldTypes: 10, + noteTypes: 5, + decks: 2, + }); + + vi.mocked(repo.purgeDeletedRecords).mockResolvedValue(mockResult); + + const result = await repo.purgeDeletedRecords({ retentionDays: 90 }); + + expect(result.reviewLogs).toBe(100); + expect(result.noteFieldValues).toBe(50); + expect(result.cards).toBe(25); + expect(result.notes).toBe(20); + expect(result.noteFieldTypes).toBe(10); + expect(result.noteTypes).toBe(5); + expect(result.decks).toBe(2); + }); + + it("returns zero counts when no records to purge", async () => { + const repo = createMockPurgeRepo(); + vi.mocked(repo.purgeDeletedRecords).mockResolvedValue( + createMockPurgeResult(), + ); + + const result = await repo.purgeDeletedRecords({ retentionDays: 90 }); + + const total = + result.reviewLogs + + result.noteFieldValues + + result.cards + + result.notes + + result.noteFieldTypes + + result.noteTypes + + result.decks; + + expect(total).toBe(0); + }); +}); |
