refactor: mock DrizzleClient and external dependencies in trivia service tests.
Some checks failed
Deploy to Production / test (push) Failing after 27s
Deploy to Production / build (push) Has been skipped
Deploy to Production / deploy (push) Has been skipped

This commit is contained in:
syntaxbullet
2026-01-30 16:17:00 +01:00
parent 9a2fc101da
commit 119301f1c3

View File

@@ -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 { triviaService } from "./trivia.service";
import { DrizzleClient } from "@shared/db/DrizzleClient"; import { users, userTimers, transactions } from "@db/schema";
import { users, userTimers } from "@db/schema";
import { eq, and } from "drizzle-orm";
import { config } from "@shared/lib/config";
import { TimerType } from "@shared/lib/constants"; import { TimerType } from "@shared/lib/constants";
// Mock fetch for OpenTDB API // Define mock functions
const mockFetch = mock(() => Promise.resolve({ 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({ json: () => Promise.resolve({
response_code: 0, response_code: 0,
results: [{ results: [{
@@ -23,39 +93,25 @@ const mockFetch = mock(() => Promise.resolve({
] ]
}] }]
}) })
})); })) as any;
global.fetch = mockFetch as any;
describe("TriviaService", () => { describe("TriviaService", () => {
const TEST_USER_ID = "999999999"; const TEST_USER_ID = "999999999";
const TEST_USERNAME = "testuser"; const TEST_USERNAME = "testuser";
beforeEach(async () => { beforeEach(() => {
// Clean up test data mockFindFirst.mockReset();
await DrizzleClient.delete(userTimers) mockInsert.mockClear();
.where(eq(userTimers.userId, BigInt(TEST_USER_ID))); mockUpdate.mockClear();
mockDelete.mockClear();
// Ensure test user exists with sufficient balance mockValues.mockClear();
await DrizzleClient.insert(users) mockReturning.mockClear();
.values({ mockSet.mockClear();
id: BigInt(TEST_USER_ID), mockWhere.mockClear();
username: TEST_USERNAME, mockOnConflictDoUpdate.mockClear();
balance: 1000n, mockRecordEvent.mockClear();
xp: 0n, // Clear active sessions
}) (triviaService as any).activeSessions.clear();
.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", () => { describe("fetchQuestion", () => {
@@ -66,176 +122,146 @@ describe("TriviaService", () => {
expect(question.question).toBe('What is 2 + 2?'); expect(question.question).toBe('What is 2 + 2?');
expect(question.correctAnswer).toBe('4'); expect(question.correctAnswer).toBe('4');
expect(question.incorrectAnswers).toHaveLength(3); expect(question.incorrectAnswers).toHaveLength(3);
expect(question.type).toBe('multiple');
}); });
}); });
describe("canPlayTrivia", () => { describe("canPlayTrivia", () => {
it("should allow playing when no cooldown exists", async () => { it("should allow playing when no cooldown exists", async () => {
mockFindFirst.mockResolvedValue(undefined);
const result = await triviaService.canPlayTrivia(TEST_USER_ID); const result = await triviaService.canPlayTrivia(TEST_USER_ID);
expect(result.canPlay).toBe(true); expect(result.canPlay).toBe(true);
expect(result.nextAvailable).toBeUndefined();
}); });
it("should prevent playing when on cooldown", async () => { it("should prevent playing when on cooldown", async () => {
const futureDate = new Date(Date.now() + 60000); const future = new Date(Date.now() + 60000);
mockFindFirst.mockResolvedValue({ expiresAt: future });
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); const result = await triviaService.canPlayTrivia(TEST_USER_ID);
expect(result.canPlay).toBe(false); expect(result.canPlay).toBe(false);
expect(result.nextAvailable).toBeDefined(); expect(result.nextAvailable).toBe(future);
}); });
it("should allow playing when cooldown has expired", async () => { it("should allow playing when cooldown has expired", async () => {
const pastDate = new Date(Date.now() - 1000); const past = new Date(Date.now() - 1000);
mockFindFirst.mockResolvedValue({ expiresAt: past });
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); const result = await triviaService.canPlayTrivia(TEST_USER_ID);
expect(result.canPlay).toBe(true); expect(result.canPlay).toBe(true);
}); });
}); });
describe("startTrivia", () => { describe("startTrivia", () => {
it("should start a trivia session and deduct entry fee", async () => { 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); const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME);
expect(session).toBeDefined(); expect(session).toBeDefined();
expect(session.sessionId).toContain(TEST_USER_ID);
expect(session.userId).toBe(TEST_USER_ID); expect(session.userId).toBe(TEST_USER_ID);
expect(session.question).toBeDefined(); expect(session.entryFee).toBe(50n);
expect(session.allAnswers).toHaveLength(4);
expect(session.entryFee).toBe(config.trivia.entryFee);
expect(session.potentialReward).toBeGreaterThan(0n);
// Verify balance deduction // Check deduction
const user = await DrizzleClient.query.users.findFirst({ expect(mockUpdate).toHaveBeenCalledWith(users);
where: eq(users.id, BigInt(TEST_USER_ID)) 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 // Check cooldown set
const cooldown = await DrizzleClient.query.userTimers.findFirst({ expect(mockInsert).toHaveBeenCalledWith(userTimers);
where: and( expect(mockOnConflictDoUpdate).toHaveBeenCalled();
eq(userTimers.userId, BigInt(TEST_USER_ID)),
eq(userTimers.type, TimerType.TRIVIA_COOLDOWN),
eq(userTimers.key, 'default')
)
});
expect(cooldown).toBeDefined(); // Check dashboard event
expect(mockRecordEvent).toHaveBeenCalled();
}); });
it("should throw error if user has insufficient balance", async () => { it("should throw error if user has insufficient balance", async () => {
// Set balance to less than entry fee mockFindFirst
await DrizzleClient.update(users) .mockResolvedValueOnce(undefined) // No cooldown
.set({ balance: 10n }) .mockResolvedValueOnce({ id: 1n, balance: 10n }); // Insufficient balance
.where(eq(users.id, BigInt(TEST_USER_ID)));
await expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME)) expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME))
.rejects.toThrow('Insufficient funds'); .rejects.toThrow("Insufficient funds");
}); });
it("should throw error if user is on cooldown", async () => { 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({ expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME))
userId: BigInt(TEST_USER_ID), .rejects.toThrow("cooldown");
type: TimerType.TRIVIA_COOLDOWN,
key: 'default',
expiresAt: futureDate,
});
await expect(triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME))
.rejects.toThrow('cooldown');
}); });
}); });
describe("submitAnswer", () => { describe("submitAnswer", () => {
it("should award prize for correct answer", async () => { it("should award prize for correct answer", async () => {
const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME); // Setup an active session manually
const balanceBefore = (await DrizzleClient.query.users.findFirst({ const session = {
where: eq(users.id, BigInt(TEST_USER_ID)) sessionId: "test_session",
}))!.balance!; 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.correct).toBe(true);
expect(result.reward).toBe(session.potentialReward); expect(result.reward).toBe(100n);
expect(result.correctAnswer).toBe(session.question.correctAnswer);
// Verify balance increase // Verify balance update
const user = await DrizzleClient.query.users.findFirst({ expect(mockUpdate).toHaveBeenCalledWith(users);
where: eq(users.id, BigInt(TEST_USER_ID)) expect(mockInsert).toHaveBeenCalledWith(transactions);
}); expect(mockRecordEvent).toHaveBeenCalled();
expect(user?.balance).toBe(balanceBefore + session.potentialReward);
}); });
it("should not award prize for incorrect answer", async () => { it("should not award prize for incorrect answer", async () => {
const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME); const session = {
const balanceBefore = (await DrizzleClient.query.users.findFirst({ sessionId: "test_session",
where: eq(users.id, BigInt(TEST_USER_ID)) userId: TEST_USER_ID,
}))!.balance!; 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.correct).toBe(false);
expect(result.reward).toBe(0n); expect(result.reward).toBe(0n);
expect(result.correctAnswer).toBe(session.question.correctAnswer);
// Verify balance unchanged (already deducted at start) // No balance update
const user = await DrizzleClient.query.users.findFirst({ expect(mockUpdate).not.toHaveBeenCalled();
where: eq(users.id, BigInt(TEST_USER_ID))
});
expect(user?.balance).toBe(balanceBefore);
}); });
it("should throw error if session doesn't exist", async () => { it("should throw error if session doesn't exist", async () => {
await expect(triviaService.submitAnswer("invalid_session", TEST_USER_ID, true)) expect(triviaService.submitAnswer("invalid", TEST_USER_ID, true))
.rejects.toThrow('Session not found'); .rejects.toThrow("Session not found");
}); });
it("should prevent double submission", async () => { 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 triviaService.submitAnswer("test_session", TEST_USER_ID, true);
await expect(triviaService.submitAnswer(session.sessionId, TEST_USER_ID, true))
.rejects.toThrow('Session not found');
});
});
describe("getSession", () => { // Second try
it("should retrieve active session", async () => { expect(triviaService.submitAnswer("test_session", TEST_USER_ID, true))
const session = await triviaService.startTrivia(TEST_USER_ID, TEST_USERNAME); .rejects.toThrow("Session not found");
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();
}); });
}); });
}); });