feat: trivia command!

This commit is contained in:
syntaxbullet
2026-01-11 14:37:17 +01:00
parent 1cd3dbcd72
commit 35bd1f58dd
10 changed files with 1348 additions and 0 deletions

View 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();
});
});
});