Files
aurorabot/shared/modules/trivia/trivia.service.test.ts
syntaxbullet 5d832c9601
All checks were successful
Deploy to Production / test (push) Successful in 38s
fix: update trivia test to mock events instead of dashboardService
The trivia service now emits domain events via systemEvents instead
of directly calling dashboardService.recordEvent. Updated the test
mock and assertions to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:37:31 +01:00

273 lines
9.4 KiB
TypeScript

import { describe, it, expect, mock, beforeEach } from "bun:test";
import { triviaService } from "./trivia.service";
import { users, userTimers, transactions } from "@db/schema";
import { TimerType } from "@shared/lib/constants";
// Define mock functions
const mockFindFirst = mock();
const mockFindMany = mock(() => Promise.resolve([]));
const mockInsert = mock();
const mockUpdate = mock();
const mockDelete = mock();
const mockValues = mock();
const mockReturning = mock();
const mockSet = mock();
const mockWhere = mock();
const mockOnConflictDoUpdate = mock();
// Chain setup
mockInsert.mockReturnValue({ values: mockValues });
mockValues.mockReturnValue({
returning: mockReturning,
onConflictDoUpdate: mockOnConflictDoUpdate
});
mockOnConflictDoUpdate.mockResolvedValue({});
mockUpdate.mockReturnValue({ set: mockSet });
mockSet.mockReturnValue({ where: mockWhere });
mockWhere.mockReturnValue({ returning: mockReturning });
// Mock DrizzleClient
mock.module("@shared/db/DrizzleClient", () => {
const createMockTx = () => ({
query: {
users: { findFirst: mockFindFirst },
userTimers: { findFirst: mockFindFirst },
userQuests: { findMany: mockFindMany },
},
insert: mockInsert,
update: mockUpdate,
delete: mockDelete,
});
return {
DrizzleClient: {
query: {
users: { findFirst: mockFindFirst },
userTimers: { findFirst: mockFindFirst },
},
insert: mockInsert,
update: mockUpdate,
delete: mockDelete,
transaction: async (cb: any) => cb(createMockTx())
}
};
});
// Mock Config
mock.module("@shared/lib/config", () => ({
config: {
trivia: {
entryFee: 50n,
rewardMultiplier: 2.0,
timeoutSeconds: 300,
cooldownMs: 60000,
categories: [9],
difficulty: 'medium'
}
}
}));
// Mock Events (trivia service emits domain events instead of calling dashboardService directly)
const mockEmit = mock(() => true);
mock.module("@shared/lib/events", () => ({
systemEvents: {
emit: mockEmit,
},
EVENTS: {
DOMAIN: {
TRIVIA_STARTED: "domain:trivia_started",
TRIVIA_WON: "domain:trivia_won",
}
}
}));
// Mock fetch for OpenTDB
global.fetch = 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'),
]
}]
})
})) as any;
describe("TriviaService", () => {
const TEST_USER_ID = "999999999";
const TEST_USERNAME = "testuser";
beforeEach(() => {
mockFindFirst.mockReset();
mockInsert.mockClear();
mockUpdate.mockClear();
mockDelete.mockClear();
mockValues.mockClear();
mockReturning.mockClear();
mockSet.mockClear();
mockWhere.mockClear();
mockOnConflictDoUpdate.mockClear();
mockEmit.mockClear();
// Clear active sessions
(triviaService as any).activeSessions.clear();
});
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);
});
});
describe("canPlayTrivia", () => {
it("should allow playing when no cooldown exists", async () => {
mockFindFirst.mockResolvedValue(undefined);
const result = await triviaService.canPlayTrivia(TEST_USER_ID);
expect(result.canPlay).toBe(true);
});
it("should prevent playing when on cooldown", async () => {
const future = new Date(Date.now() + 60000);
mockFindFirst.mockResolvedValue({ expiresAt: future });
const result = await triviaService.canPlayTrivia(TEST_USER_ID);
expect(result.canPlay).toBe(false);
expect(result.nextAvailable).toBe(future);
});
it("should allow playing when cooldown has expired", async () => {
const past = new Date(Date.now() - 1000);
mockFindFirst.mockResolvedValue({ expiresAt: past });
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 () => {
// Mock cooldown check (first call) and balance check (second call)
mockFindFirst
.mockResolvedValueOnce(undefined) // No cooldown
.mockResolvedValueOnce({ id: 1n, balance: 1000n }); // User balance
const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME);
expect(session).toBeDefined();
expect(session.userId).toBe(TEST_USER_ID);
expect(session.entryFee).toBe(50n);
// Check deduction
expect(mockUpdate).toHaveBeenCalledWith(users);
expect(mockSet).toHaveBeenCalledWith(expect.objectContaining({
// sql templating makes exact match hard, checking general invocation
}));
// Check transactions
expect(mockInsert).toHaveBeenCalledWith(transactions);
// Check cooldown set
expect(mockInsert).toHaveBeenCalledWith(userTimers);
expect(mockOnConflictDoUpdate).toHaveBeenCalled();
// Check domain event emitted
expect(mockEmit).toHaveBeenCalled();
});
it("should throw error if user has insufficient balance", async () => {
mockFindFirst
.mockResolvedValueOnce(undefined) // No cooldown
.mockResolvedValueOnce({ id: 1n, balance: 10n }); // Insufficient balance
expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME))
.rejects.toThrow("Insufficient funds");
});
it("should throw error if user is on cooldown", async () => {
mockFindFirst.mockResolvedValueOnce({ expiresAt: new Date(Date.now() + 60000) });
expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME))
.rejects.toThrow("cooldown");
});
});
describe("submitAnswer", () => {
it("should award prize for correct answer", async () => {
// Setup an active session manually
const session = {
sessionId: "test_session",
userId: TEST_USER_ID,
question: { correctAnswer: "4" },
potentialReward: 100n
};
(triviaService as any).activeSessions.set("test_session", session);
// Mock user balance fetch for reward update
mockFindFirst.mockResolvedValue({ id: 1n, balance: 950n });
const result = await triviaService.submitAnswer("test_session", TEST_USER_ID, true);
expect(result.correct).toBe(true);
expect(result.reward).toBe(100n);
// Verify balance update
expect(mockUpdate).toHaveBeenCalledWith(users);
expect(mockInsert).toHaveBeenCalledWith(transactions);
expect(mockEmit).toHaveBeenCalled();
});
it("should not award prize for incorrect answer", async () => {
const session = {
sessionId: "test_session",
userId: TEST_USER_ID,
question: { correctAnswer: "4" },
potentialReward: 100n
};
(triviaService as any).activeSessions.set("test_session", session);
const result = await triviaService.submitAnswer("test_session", TEST_USER_ID, false);
expect(result.correct).toBe(false);
expect(result.reward).toBe(0n);
// No balance update
expect(mockUpdate).not.toHaveBeenCalled();
});
it("should throw error if session doesn't exist", async () => {
expect(triviaService.submitAnswer("invalid", TEST_USER_ID, true))
.rejects.toThrow("Session not found");
});
it("should prevent double submission", async () => {
const session = {
sessionId: "test_session",
userId: TEST_USER_ID,
question: { correctAnswer: "4" },
potentialReward: 100n
};
(triviaService as any).activeSessions.set("test_session", session);
// Mock user for first success
mockFindFirst.mockResolvedValue({ id: 1n, balance: 950n });
await triviaService.submitAnswer("test_session", TEST_USER_ID, true);
// Second try
expect(triviaService.submitAnswer("test_session", TEST_USER_ID, true))
.rejects.toThrow("Session not found");
});
});
});