feat: trivia command!
This commit is contained in:
241
shared/modules/trivia/trivia.service.test.ts
Normal file
241
shared/modules/trivia/trivia.service.test.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user