diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-04 22:31:13 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-04 22:43:15 +0900 |
| commit | a047cdd517efe7693ccd41162f9267f48cd67955 (patch) | |
| tree | 969c6582d53429085c066aa88881d09f42185aca /src/server/routes/study.test.ts | |
| parent | 87d925c8dfb9c0502a739275df19d1dde8b32230 (diff) | |
| download | kioku-a047cdd517efe7693ccd41162f9267f48cd67955.tar.gz kioku-a047cdd517efe7693ccd41162f9267f48cd67955.tar.zst kioku-a047cdd517efe7693ccd41162f9267f48cd67955.zip | |
feat(study): enforce newCardsPerDay limit in study API
Split due card fetching into new cards and review cards, applying
the deck's newCardsPerDay limit to new cards while leaving review
cards unrestricted. New cards are placed before review cards in
the response.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/server/routes/study.test.ts')
| -rw-r--r-- | src/server/routes/study.test.ts | 104 |
1 files changed, 99 insertions, 5 deletions
diff --git a/src/server/routes/study.test.ts b/src/server/routes/study.test.ts index a5ac817..aeba46b 100644 --- a/src/server/routes/study.test.ts +++ b/src/server/routes/study.test.ts @@ -28,6 +28,8 @@ function createMockCardRepo(): CardRepository { countDueCards: vi.fn(), findDueCardsWithNoteData: vi.fn(), findDueCardsForStudy: vi.fn(), + findDueNewCardsForStudy: vi.fn(), + findDueReviewCardsForStudy: vi.fn(), updateFSRSFields: vi.fn(), }; } @@ -45,6 +47,7 @@ function createMockDeckRepo(): DeckRepository { function createMockReviewLogRepo(): ReviewLogRepository { return { create: vi.fn(), + countTodayNewCardReviews: vi.fn().mockResolvedValue(0), }; } @@ -170,7 +173,8 @@ describe("GET /api/decks/:deckId/study", () => { vi.mocked(mockDeckRepo.findById).mockResolvedValue( createMockDeck({ id: DECK_ID }), ); - vi.mocked(mockCardRepo.findDueCardsForStudy).mockResolvedValue([]); + vi.mocked(mockCardRepo.findDueNewCardsForStudy).mockResolvedValue([]); + vi.mocked(mockCardRepo.findDueReviewCardsForStudy).mockResolvedValue([]); const res = await app.request(`/api/decks/${DECK_ID}/study`, { method: "GET", @@ -184,7 +188,12 @@ describe("GET /api/decks/:deckId/study", () => { DECK_ID, "user-uuid-123", ); - expect(mockCardRepo.findDueCardsForStudy).toHaveBeenCalledWith( + expect(mockCardRepo.findDueNewCardsForStudy).toHaveBeenCalledWith( + DECK_ID, + expect.any(Date), + 20, + ); + expect(mockCardRepo.findDueReviewCardsForStudy).toHaveBeenCalledWith( DECK_ID, expect.any(Date), 100, @@ -192,24 +201,31 @@ describe("GET /api/decks/:deckId/study", () => { }); it("returns due cards", async () => { - const mockCards = [ + const newCards = [ createMockCardForStudy({ id: "card-1", front: "Q1", back: "A1", + state: CardState.New, fieldValuesMap: {}, }), + ]; + const reviewCards = [ createMockCardForStudy({ id: "card-2", front: "Q2", back: "A2", + state: CardState.Review, fieldValuesMap: {}, }), ]; vi.mocked(mockDeckRepo.findById).mockResolvedValue( createMockDeck({ id: DECK_ID }), ); - vi.mocked(mockCardRepo.findDueCardsForStudy).mockResolvedValue(mockCards); + vi.mocked(mockCardRepo.findDueNewCardsForStudy).mockResolvedValue(newCards); + vi.mocked(mockCardRepo.findDueReviewCardsForStudy).mockResolvedValue( + reviewCards, + ); const res = await app.request(`/api/decks/${DECK_ID}/study`, { method: "GET", @@ -241,7 +257,10 @@ describe("GET /api/decks/:deckId/study", () => { vi.mocked(mockDeckRepo.findById).mockResolvedValue( createMockDeck({ id: DECK_ID }), ); - vi.mocked(mockCardRepo.findDueCardsForStudy).mockResolvedValue(mockCards); + vi.mocked(mockCardRepo.findDueNewCardsForStudy).mockResolvedValue( + mockCards, + ); + vi.mocked(mockCardRepo.findDueReviewCardsForStudy).mockResolvedValue([]); const res = await app.request(`/api/decks/${DECK_ID}/study`, { method: "GET", @@ -255,6 +274,81 @@ describe("GET /api/decks/:deckId/study", () => { expect(body.cards?.[0]?.fieldValuesMap?.Front).toBe("Question"); }); + it("limits new cards based on newCardsPerDay", async () => { + vi.mocked(mockDeckRepo.findById).mockResolvedValue( + createMockDeck({ id: DECK_ID, newCardsPerDay: 5 }), + ); + vi.mocked(mockReviewLogRepo.countTodayNewCardReviews).mockResolvedValue(3); + vi.mocked(mockCardRepo.findDueNewCardsForStudy).mockResolvedValue([]); + vi.mocked(mockCardRepo.findDueReviewCardsForStudy).mockResolvedValue([]); + + await app.request(`/api/decks/${DECK_ID}/study`, { + method: "GET", + headers: { Authorization: `Bearer ${authToken}` }, + }); + + expect(mockCardRepo.findDueNewCardsForStudy).toHaveBeenCalledWith( + DECK_ID, + expect.any(Date), + 2, + ); + }); + + it("returns 0 new cards when daily limit is reached", async () => { + vi.mocked(mockDeckRepo.findById).mockResolvedValue( + createMockDeck({ id: DECK_ID, newCardsPerDay: 5 }), + ); + vi.mocked(mockReviewLogRepo.countTodayNewCardReviews).mockResolvedValue(5); + vi.mocked(mockCardRepo.findDueNewCardsForStudy).mockResolvedValue([]); + vi.mocked(mockCardRepo.findDueReviewCardsForStudy).mockResolvedValue([]); + + await app.request(`/api/decks/${DECK_ID}/study`, { + method: "GET", + headers: { Authorization: `Bearer ${authToken}` }, + }); + + expect(mockCardRepo.findDueNewCardsForStudy).toHaveBeenCalledWith( + DECK_ID, + expect.any(Date), + 0, + ); + }); + + it("places new cards before review cards in response", async () => { + const newCards = [ + createMockCardForStudy({ + id: "new-1", + state: CardState.New, + fieldValuesMap: {}, + }), + ]; + const reviewCards = [ + createMockCardForStudy({ + id: "review-1", + state: CardState.Review, + fieldValuesMap: {}, + }), + ]; + vi.mocked(mockDeckRepo.findById).mockResolvedValue( + createMockDeck({ id: DECK_ID }), + ); + vi.mocked(mockCardRepo.findDueNewCardsForStudy).mockResolvedValue(newCards); + vi.mocked(mockCardRepo.findDueReviewCardsForStudy).mockResolvedValue( + reviewCards, + ); + + const res = await app.request(`/api/decks/${DECK_ID}/study`, { + method: "GET", + headers: { Authorization: `Bearer ${authToken}` }, + }); + + expect(res.status).toBe(200); + const body = (await res.json()) as StudyResponse; + expect(body.cards).toHaveLength(2); + expect(body.cards?.[0]?.id).toBe("new-1"); + expect(body.cards?.[1]?.id).toBe("review-1"); + }); + it("returns 404 for non-existent deck", async () => { vi.mocked(mockDeckRepo.findById).mockResolvedValue(undefined); |
