import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; import { triviaService } from "./trivia.service"; import { DrizzleClient } from "@shared/db/DrizzleClient"; import { users, userTimers } from "@db/schema"; import { eq, and } from "drizzle-orm"; import { config } from "@shared/lib/config"; import { TimerType } from "@shared/lib/constants"; // Mock fetch for OpenTDB API const mockFetch = mock(() => Promise.resolve({ json: () => Promise.resolve({ response_code: 0, results: [{ category: Buffer.from('General Knowledge').toString('base64'), type: 'multiple', difficulty: Buffer.from('medium').toString('base64'), question: Buffer.from('What is 2 + 2?').toString('base64'), correct_answer: Buffer.from('4').toString('base64'), incorrect_answers: [ Buffer.from('3').toString('base64'), Buffer.from('5').toString('base64'), Buffer.from('22').toString('base64'), ] }] }) })); global.fetch = mockFetch as any; describe("TriviaService", () => { const TEST_USER_ID = "999999999"; const TEST_USERNAME = "testuser"; beforeEach(async () => { // Clean up test data await DrizzleClient.delete(userTimers) .where(eq(userTimers.userId, BigInt(TEST_USER_ID))); // Ensure test user exists with sufficient balance await DrizzleClient.insert(users) .values({ id: BigInt(TEST_USER_ID), username: TEST_USERNAME, balance: 1000n, xp: 0n, }) .onConflictDoUpdate({ target: [users.id], set: { balance: 1000n, } }); }); afterEach(async () => { // Clean up await DrizzleClient.delete(userTimers) .where(eq(userTimers.userId, BigInt(TEST_USER_ID))); }); describe("fetchQuestion", () => { it("should fetch and decode a trivia question", async () => { const question = await triviaService.fetchQuestion(); expect(question).toBeDefined(); expect(question.question).toBe('What is 2 + 2?'); expect(question.correctAnswer).toBe('4'); expect(question.incorrectAnswers).toHaveLength(3); expect(question.type).toBe('multiple'); }); }); describe("canPlayTrivia", () => { it("should allow playing when no cooldown exists", async () => { const result = await triviaService.canPlayTrivia(TEST_USER_ID); expect(result.canPlay).toBe(true); expect(result.nextAvailable).toBeUndefined(); }); it("should prevent playing when on cooldown", async () => { const futureDate = new Date(Date.now() + 60000); await DrizzleClient.insert(userTimers).values({ userId: BigInt(TEST_USER_ID), type: TimerType.TRIVIA_COOLDOWN, key: 'default', expiresAt: futureDate, }); const result = await triviaService.canPlayTrivia(TEST_USER_ID); expect(result.canPlay).toBe(false); expect(result.nextAvailable).toBeDefined(); }); it("should allow playing when cooldown has expired", async () => { const pastDate = new Date(Date.now() - 1000); await DrizzleClient.insert(userTimers).values({ userId: BigInt(TEST_USER_ID), type: TimerType.TRIVIA_COOLDOWN, key: 'default', expiresAt: pastDate, }); const result = await triviaService.canPlayTrivia(TEST_USER_ID); expect(result.canPlay).toBe(true); }); }); describe("startTrivia", () => { it("should start a trivia session and deduct entry fee", async () => { const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME); expect(session).toBeDefined(); expect(session.sessionId).toContain(TEST_USER_ID); expect(session.userId).toBe(TEST_USER_ID); expect(session.question).toBeDefined(); expect(session.allAnswers).toHaveLength(4); expect(session.entryFee).toBe(config.trivia.entryFee); expect(session.potentialReward).toBeGreaterThan(0n); // Verify balance deduction const user = await DrizzleClient.query.users.findFirst({ where: eq(users.id, BigInt(TEST_USER_ID)) }); expect(user?.balance).toBe(1000n - config.trivia.entryFee); // Verify cooldown was set const cooldown = await DrizzleClient.query.userTimers.findFirst({ where: and( eq(userTimers.userId, BigInt(TEST_USER_ID)), eq(userTimers.type, TimerType.TRIVIA_COOLDOWN), eq(userTimers.key, 'default') ) }); expect(cooldown).toBeDefined(); }); it("should throw error if user has insufficient balance", async () => { // Set balance to less than entry fee await DrizzleClient.update(users) .set({ balance: 10n }) .where(eq(users.id, BigInt(TEST_USER_ID))); await expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME)) .rejects.toThrow('Insufficient funds'); }); it("should throw error if user is on cooldown", async () => { const futureDate = new Date(Date.now() + 60000); await DrizzleClient.insert(userTimers).values({ userId: BigInt(TEST_USER_ID), type: TimerType.TRIVIA_COOLDOWN, key: 'default', expiresAt: futureDate, }); await expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME)) .rejects.toThrow('cooldown'); }); }); describe("submitAnswer", () => { it("should award prize for correct answer", async () => { const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME); const balanceBefore = (await DrizzleClient.query.users.findFirst({ where: eq(users.id, BigInt(TEST_USER_ID)) }))!.balance!; const result = await triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true); expect(result.correct).toBe(true); expect(result.reward).toBe(session.potentialReward); expect(result.correctAnswer).toBe(session.question.correctAnswer); // Verify balance increase const user = await DrizzleClient.query.users.findFirst({ where: eq(users.id, BigInt(TEST_USER_ID)) }); expect(user?.balance).toBe(balanceBefore + session.potentialReward); }); it("should not award prize for incorrect answer", async () => { const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME); const balanceBefore = (await DrizzleClient.query.users.findFirst({ where: eq(users.id, BigInt(TEST_USER_ID)) }))!.balance!; const result = await triviaService.submitAnswer(session.sessionId, TEST_USER_ID, false); expect(result.correct).toBe(false); expect(result.reward).toBe(0n); expect(result.correctAnswer).toBe(session.question.correctAnswer); // Verify balance unchanged (already deducted at start) const user = await DrizzleClient.query.users.findFirst({ where: eq(users.id, BigInt(TEST_USER_ID)) }); expect(user?.balance).toBe(balanceBefore); }); it("should throw error if session doesn't exist", async () => { await expect(triviaService.submitAnswer("invalid_session", TEST_USER_ID, true)) .rejects.toThrow('Session not found'); }); it("should prevent double submission", async () => { const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME); await triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true); // Try to submit again await expect(triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true)) .rejects.toThrow('Session not found'); }); }); describe("getSession", () => { it("should retrieve active session", async () => { const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME); const retrieved = triviaService.getSession(session.sessionId); expect(retrieved).toBeDefined(); expect(retrieved?.sessionId).toBe(session.sessionId); }); it("should return undefined for non-existent session", () => { const retrieved = triviaService.getSession("invalid_session"); expect(retrieved).toBeUndefined(); }); }); });