aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server/repositories/deck.test.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-31 20:34:27 +0900
committernsfisis <nsfisis@gmail.com>2025-12-31 20:34:27 +0900
commite3a84df9fcae8f25c8ea1d527638c2bfaafb942e (patch)
tree605f909c78e8b4510b7110d57749c1c0fe566360 /src/server/repositories/deck.test.ts
parentb69fd1353c449baa3262016c2bb8f653932bd932 (diff)
downloadkioku-e3a84df9fcae8f25c8ea1d527638c2bfaafb942e.tar.gz
kioku-e3a84df9fcae8f25c8ea1d527638c2bfaafb942e.tar.zst
kioku-e3a84df9fcae8f25c8ea1d527638c2bfaafb942e.zip
feat(db): add ORDER BY to repository SELECT queries
Ensures deterministic ordering for all multi-row SELECT queries: - deck/note/noteType findByUserId/findByDeckId: order by createdAt - card findByNoteId: order by isReversed (normal card first) - note field values: order by noteFieldTypeId - sync pull queries: order by id This guarantees consistent UI display and sync results regardless of PostgreSQL's internal row ordering. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/server/repositories/deck.test.ts')
-rw-r--r--src/server/repositories/deck.test.ts212
1 files changed, 212 insertions, 0 deletions
diff --git a/src/server/repositories/deck.test.ts b/src/server/repositories/deck.test.ts
new file mode 100644
index 0000000..945f844
--- /dev/null
+++ b/src/server/repositories/deck.test.ts
@@ -0,0 +1,212 @@
+import { describe, expect, it, vi } from "vitest";
+import type { Deck, DeckRepository } from "./types.js";
+
+function createMockDeck(overrides: Partial<Deck> = {}): Deck {
+ return {
+ id: "deck-uuid-123",
+ userId: "user-uuid-123",
+ name: "Test Deck",
+ description: null,
+ newCardsPerDay: 20,
+ createdAt: new Date("2024-01-01"),
+ updatedAt: new Date("2024-01-01"),
+ deletedAt: null,
+ syncVersion: 0,
+ ...overrides,
+ };
+}
+
+function createMockDeckRepo(): DeckRepository {
+ return {
+ findByUserId: vi.fn(),
+ findById: vi.fn(),
+ create: vi.fn(),
+ update: vi.fn(),
+ softDelete: vi.fn(),
+ };
+}
+
+describe("DeckRepository mock factory", () => {
+ describe("createMockDeck", () => {
+ it("creates a valid Deck with defaults", () => {
+ const deck = createMockDeck();
+
+ expect(deck.id).toBe("deck-uuid-123");
+ expect(deck.userId).toBe("user-uuid-123");
+ expect(deck.name).toBe("Test Deck");
+ expect(deck.description).toBeNull();
+ expect(deck.newCardsPerDay).toBe(20);
+ expect(deck.deletedAt).toBeNull();
+ expect(deck.syncVersion).toBe(0);
+ });
+
+ it("allows overriding properties", () => {
+ const deck = createMockDeck({
+ id: "custom-id",
+ name: "Custom Deck",
+ description: "A description",
+ newCardsPerDay: 50,
+ });
+
+ expect(deck.id).toBe("custom-id");
+ expect(deck.name).toBe("Custom Deck");
+ expect(deck.description).toBe("A description");
+ expect(deck.newCardsPerDay).toBe(50);
+ });
+ });
+
+ describe("createMockDeckRepo", () => {
+ it("creates a repository with all required methods", () => {
+ const repo = createMockDeckRepo();
+
+ expect(repo.findByUserId).toBeDefined();
+ expect(repo.findById).toBeDefined();
+ expect(repo.create).toBeDefined();
+ expect(repo.update).toBeDefined();
+ expect(repo.softDelete).toBeDefined();
+ });
+
+ it("methods are mockable for findByUserId", async () => {
+ const repo = createMockDeckRepo();
+ const mockDecks = [
+ createMockDeck({ id: "deck-1" }),
+ createMockDeck({ id: "deck-2" }),
+ ];
+
+ vi.mocked(repo.findByUserId).mockResolvedValue(mockDecks);
+
+ const results = await repo.findByUserId("user-123");
+ expect(results).toHaveLength(2);
+ expect(repo.findByUserId).toHaveBeenCalledWith("user-123");
+ });
+
+ it("methods are mockable for findById", async () => {
+ const repo = createMockDeckRepo();
+ const mockDeck = createMockDeck();
+
+ vi.mocked(repo.findById).mockResolvedValue(mockDeck);
+
+ const found = await repo.findById("deck-id", "user-id");
+ expect(found).toEqual(mockDeck);
+ expect(repo.findById).toHaveBeenCalledWith("deck-id", "user-id");
+ });
+
+ it("returns undefined when deck not found", async () => {
+ const repo = createMockDeckRepo();
+
+ vi.mocked(repo.findById).mockResolvedValue(undefined);
+
+ expect(await repo.findById("nonexistent", "user-id")).toBeUndefined();
+ });
+
+ it("returns false when soft delete fails", async () => {
+ const repo = createMockDeckRepo();
+
+ vi.mocked(repo.softDelete).mockResolvedValue(false);
+
+ expect(await repo.softDelete("nonexistent", "user-id")).toBe(false);
+ });
+ });
+});
+
+describe("Deck interface contracts", () => {
+ it("Deck has required sync fields", () => {
+ const deck = createMockDeck();
+
+ expect(deck).toHaveProperty("syncVersion");
+ expect(deck).toHaveProperty("createdAt");
+ expect(deck).toHaveProperty("updatedAt");
+ expect(deck).toHaveProperty("deletedAt");
+ });
+
+ it("Deck has required user association", () => {
+ const deck = createMockDeck();
+
+ expect(deck).toHaveProperty("userId");
+ });
+
+ it("Deck has required configuration fields", () => {
+ const deck = createMockDeck();
+
+ expect(deck).toHaveProperty("name");
+ expect(deck).toHaveProperty("description");
+ expect(deck).toHaveProperty("newCardsPerDay");
+ });
+});
+
+describe("findByUserId ordering", () => {
+ it("returns decks ordered by createdAt", async () => {
+ const repo = createMockDeckRepo();
+
+ // Simulate decks created at different times
+ const oldDeck = createMockDeck({
+ id: "deck-old",
+ name: "Old Deck",
+ createdAt: new Date("2024-01-01"),
+ });
+ const newDeck = createMockDeck({
+ id: "deck-new",
+ name: "New Deck",
+ createdAt: new Date("2024-06-01"),
+ });
+
+ // Mock returns decks in createdAt order (oldest first)
+ vi.mocked(repo.findByUserId).mockResolvedValue([oldDeck, newDeck]);
+
+ const results = await repo.findByUserId("user-123");
+
+ expect(results).toHaveLength(2);
+ expect(results[0]?.id).toBe("deck-old");
+ expect(results[1]?.id).toBe("deck-new");
+ // Verify the order is by createdAt
+ expect(results[0]?.createdAt.getTime()).toBeLessThan(
+ results[1]?.createdAt.getTime() ?? 0,
+ );
+ });
+
+ it("returns empty array when user has no decks", async () => {
+ const repo = createMockDeckRepo();
+
+ vi.mocked(repo.findByUserId).mockResolvedValue([]);
+
+ const results = await repo.findByUserId("user-with-no-decks");
+ expect(results).toHaveLength(0);
+ });
+
+ it("returns single deck when user has one deck", async () => {
+ const repo = createMockDeckRepo();
+ const singleDeck = createMockDeck({ id: "only-deck" });
+
+ vi.mocked(repo.findByUserId).mockResolvedValue([singleDeck]);
+
+ const results = await repo.findByUserId("user-123");
+ expect(results).toHaveLength(1);
+ expect(results[0]?.id).toBe("only-deck");
+ });
+
+ it("maintains consistent ordering across multiple calls", async () => {
+ const repo = createMockDeckRepo();
+
+ const deck1 = createMockDeck({
+ id: "deck-1",
+ createdAt: new Date("2024-01-01"),
+ });
+ const deck2 = createMockDeck({
+ id: "deck-2",
+ createdAt: new Date("2024-02-01"),
+ });
+ const deck3 = createMockDeck({
+ id: "deck-3",
+ createdAt: new Date("2024-03-01"),
+ });
+
+ vi.mocked(repo.findByUserId).mockResolvedValue([deck1, deck2, deck3]);
+
+ const results1 = await repo.findByUserId("user-123");
+ const results2 = await repo.findByUserId("user-123");
+
+ // Order should be consistent
+ expect(results1.map((d) => d.id)).toEqual(results2.map((d) => d.id));
+ expect(results1.map((d) => d.id)).toEqual(["deck-1", "deck-2", "deck-3"]);
+ });
+});