forked from syntaxbullet/aurorabot
refactor: mock DrizzleClient and external dependencies in trivia service tests.
This commit is contained in:
@@ -1,13 +1,83 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
||||
import { describe, it, expect, mock, beforeEach } 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 { users, userTimers, transactions } from "@db/schema";
|
||||
import { TimerType } from "@shared/lib/constants";
|
||||
|
||||
// Mock fetch for OpenTDB API
|
||||
const mockFetch = mock(() => Promise.resolve({
|
||||
// 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();
|
||||
const mockRecordEvent = mock(() => Promise.resolve());
|
||||
|
||||
// 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 Dashboard Service
|
||||
mock.module("@shared/modules/dashboard/dashboard.service", () => ({
|
||||
dashboardService: {
|
||||
recordEvent: mockRecordEvent
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock fetch for OpenTDB
|
||||
global.fetch = mock(() => Promise.resolve({
|
||||
json: () => Promise.resolve({
|
||||
response_code: 0,
|
||||
results: [{
|
||||
@@ -23,39 +93,25 @@ const mockFetch = mock(() => Promise.resolve({
|
||||
]
|
||||
}]
|
||||
})
|
||||
}));
|
||||
|
||||
global.fetch = mockFetch as any;
|
||||
})) 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)));
|
||||
beforeEach(() => {
|
||||
mockFindFirst.mockReset();
|
||||
mockInsert.mockClear();
|
||||
mockUpdate.mockClear();
|
||||
mockDelete.mockClear();
|
||||
mockValues.mockClear();
|
||||
mockReturning.mockClear();
|
||||
mockSet.mockClear();
|
||||
mockWhere.mockClear();
|
||||
mockOnConflictDoUpdate.mockClear();
|
||||
mockRecordEvent.mockClear();
|
||||
// Clear active sessions
|
||||
(triviaService as any).activeSessions.clear();
|
||||
});
|
||||
|
||||
describe("fetchQuestion", () => {
|
||||
@@ -66,176 +122,146 @@ describe("TriviaService", () => {
|
||||
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 () => {
|
||||
mockFindFirst.mockResolvedValue(undefined);
|
||||
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 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).toBeDefined();
|
||||
expect(result.nextAvailable).toBe(future);
|
||||
});
|
||||
|
||||
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 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.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);
|
||||
expect(session.entryFee).toBe(50n);
|
||||
|
||||
// Verify balance deduction
|
||||
const user = await DrizzleClient.query.users.findFirst({
|
||||
where: eq(users.id, BigInt(TEST_USER_ID))
|
||||
});
|
||||
// Check deduction
|
||||
expect(mockUpdate).toHaveBeenCalledWith(users);
|
||||
expect(mockSet).toHaveBeenCalledWith(expect.objectContaining({
|
||||
// sql templating makes exact match hard, checking general invocation
|
||||
}));
|
||||
|
||||
expect(user?.balance).toBe(1000n - config.trivia.entryFee);
|
||||
// Check transactions
|
||||
expect(mockInsert).toHaveBeenCalledWith(transactions);
|
||||
|
||||
// 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')
|
||||
)
|
||||
});
|
||||
// Check cooldown set
|
||||
expect(mockInsert).toHaveBeenCalledWith(userTimers);
|
||||
expect(mockOnConflictDoUpdate).toHaveBeenCalled();
|
||||
|
||||
expect(cooldown).toBeDefined();
|
||||
// Check dashboard event
|
||||
expect(mockRecordEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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)));
|
||||
mockFindFirst
|
||||
.mockResolvedValueOnce(undefined) // No cooldown
|
||||
.mockResolvedValueOnce({ id: 1n, balance: 10n }); // Insufficient balance
|
||||
|
||||
await expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME))
|
||||
.rejects.toThrow('Insufficient funds');
|
||||
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);
|
||||
mockFindFirst.mockResolvedValueOnce({ expiresAt: 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');
|
||||
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!;
|
||||
// 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);
|
||||
|
||||
const result = await triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true);
|
||||
// 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(session.potentialReward);
|
||||
expect(result.correctAnswer).toBe(session.question.correctAnswer);
|
||||
expect(result.reward).toBe(100n);
|
||||
|
||||
// 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);
|
||||
// Verify balance update
|
||||
expect(mockUpdate).toHaveBeenCalledWith(users);
|
||||
expect(mockInsert).toHaveBeenCalledWith(transactions);
|
||||
expect(mockRecordEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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 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(session.sessionId, TEST_USER_ID, false);
|
||||
const result = await triviaService.submitAnswer("test_session", 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);
|
||||
// No balance update
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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');
|
||||
expect(triviaService.submitAnswer("invalid", 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);
|
||||
const session = {
|
||||
sessionId: "test_session",
|
||||
userId: TEST_USER_ID,
|
||||
question: { correctAnswer: "4" },
|
||||
potentialReward: 100n
|
||||
};
|
||||
(triviaService as any).activeSessions.set("test_session", session);
|
||||
|
||||
await triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true);
|
||||
// Mock user for first success
|
||||
mockFindFirst.mockResolvedValue({ id: 1n, balance: 950n });
|
||||
|
||||
// Try to submit again
|
||||
await expect(triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true))
|
||||
.rejects.toThrow('Session not found');
|
||||
});
|
||||
});
|
||||
await triviaService.submitAnswer("test_session", TEST_USER_ID, true);
|
||||
|
||||
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();
|
||||
// Second try
|
||||
expect(triviaService.submitAnswer("test_session", TEST_USER_ID, true))
|
||||
.rejects.toThrow("Session not found");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user