/** * GameModeManager Tests * Tests for mixed mode, credits, and payment logic */ // Mock the database const mockDb = { query: jest.fn() }; // Mock wallet service const mockWalletService = { createPaymentRequest: jest.fn(), processPayout: jest.fn() }; // Mock debug manager const mockDebugManager = { CONSOLE_LOGGING: false }; // Mock payment config manager const mockPaymentConfigManager = { getConfig: () => ({ paymentsEnabled: true, currency: { symbol: 'WOW', decimals: 11 }, modes: { direct: { enabled: true, price: 100000000000n }, credits: { enabled: true, creditsPerGame: 1, packages: [{ id: 'small', credits: 10, price: '500000000000', bonus: 0 }] } }, payouts: { rules: { direct: { multipliers: { escape: 2, escapeWithTreasure: 3 } }, credits: { enabled: false, multipliers: { escape: 1.5, escapeWithTreasure: 2 } } } }, preferences: { preferCreditsFirst: true } }), getLegacyGameMode: () => 'PAID_SINGLE', eventBus: { on: () => {} } }; const GameModeManager = require('../src/game/gameModeManager'); describe('GameModeManager', () => { let gmm; beforeEach(() => { jest.clearAllMocks(); mockDb.query.mockReset(); gmm = new GameModeManager(mockDb, mockWalletService, mockDebugManager, mockPaymentConfigManager); }); describe('getEffectiveModeForUser', () => { test('returns FREE mode when payments disabled', () => { gmm.paymentsEnabled = false; const result = gmm.getEffectiveModeForUser({ credits: 5 }); expect(result.mode).toBe('FREE'); }); test('returns PAID_CREDITS when user has credits and preferCreditsFirst', () => { gmm.paymentsEnabled = true; gmm.directModeEnabled = true; gmm.creditsModeEnabled = true; gmm.preferCreditsFirst = true; gmm.creditsPerGameCost = 1; const result = gmm.getEffectiveModeForUser({ credits: 5 }); expect(result.mode).toBe('PAID_CREDITS'); expect(result.hasCredits).toBe(true); expect(result.bothModesEnabled).toBe(true); }); test('returns PAID_SINGLE when user has no credits in mixed mode', () => { gmm.paymentsEnabled = true; gmm.directModeEnabled = true; gmm.creditsModeEnabled = true; gmm.preferCreditsFirst = true; gmm.creditsPerGameCost = 1; const result = gmm.getEffectiveModeForUser({ credits: 0 }); expect(result.mode).toBe('PAID_SINGLE'); expect(result.hasCredits).toBe(false); }); test('returns correct mode when only direct is enabled', () => { gmm.paymentsEnabled = true; gmm.directModeEnabled = true; gmm.creditsModeEnabled = false; const result = gmm.getEffectiveModeForUser({ credits: 10 }); expect(result.mode).toBe('PAID_SINGLE'); expect(result.bothModesEnabled).toBe(false); }); test('returns correct mode when only credits is enabled', () => { gmm.paymentsEnabled = true; gmm.directModeEnabled = false; gmm.creditsModeEnabled = true; const result = gmm.getEffectiveModeForUser({ credits: 5 }); expect(result.mode).toBe('PAID_CREDITS'); }); }); describe('calculatePayout', () => { test('calculates direct mode payout correctly', () => { gmm.singleGamePrice = 100000000000; // 1 WOW gmm.directPayoutMultipliers = { escape: 2, escapeWithTreasure: 3 }; const result = gmm.calculatePayout('PAID_SINGLE', { treasureFound: false }); expect(result.amount).toBe(200000000000); // 2x expect(result.multiplier).toBe(2); }); test('calculates treasure bonus correctly', () => { gmm.singleGamePrice = 100000000000; gmm.directPayoutMultipliers = { escape: 2, escapeWithTreasure: 3 }; const result = gmm.calculatePayout('PAID_SINGLE', { treasureFound: true }); expect(result.amount).toBe(300000000000); // 3x expect(result.multiplier).toBe(3); }); test('returns zero payout for credits mode when disabled', () => { gmm.creditsPayoutBaseValue = 100000000000; gmm.creditPayoutMultipliers = { escape: 0, escapeWithTreasure: 0 }; const result = gmm.calculatePayout('PAID_CREDITS', { treasureFound: false }); expect(result.amount).toBe(0); }); test('calculates credits mode payout when enabled', () => { gmm.creditsPayoutBaseValue = 50000000000; // 0.5 WOW base gmm.creditPayoutMultipliers = { escape: 1.5, escapeWithTreasure: 2 }; const result = gmm.calculatePayout('PAID_CREDITS', { treasureFound: false }); expect(result.amount).toBe(75000000000); // 0.5 * 1.5 }); }); describe('getPaymentOptionsForUser', () => { beforeEach(() => { mockDb.query.mockResolvedValue({ rows: [{ id: 1, credits: 5, payout_address: 'wow1addr' }] }); }); test('returns use_credit option when user has credits', async () => { gmm.paymentsEnabled = true; gmm.directModeEnabled = true; gmm.creditsModeEnabled = true; gmm.creditsPerGameCost = 1; const result = await gmm.getPaymentOptionsForUser('socket1'); const useCreditOption = result.options.find(o => o.type === 'use_credit'); expect(useCreditOption).toBeDefined(); expect(useCreditOption.mode).toBe('PAID_CREDITS'); }); test('returns pay_direct option', async () => { gmm.directModeEnabled = true; const result = await gmm.getPaymentOptionsForUser('socket1'); const directOption = result.options.find(o => o.type === 'pay_direct'); expect(directOption).toBeDefined(); expect(directOption.mode).toBe('PAID_SINGLE'); }); test('returns buy_credits option when credits mode enabled', async () => { gmm.creditsModeEnabled = true; const result = await gmm.getPaymentOptionsForUser('socket1'); const buyOption = result.options.find(o => o.type === 'buy_credits'); expect(buyOption).toBeDefined(); }); }); describe('processCreditsPackageConfirmation', () => { test('adds correct credits from package info', async () => { const user = { id: 1, credits: 0 }; mockDb.query .mockResolvedValueOnce({ rows: [user] }) // getOrCreateUser - first lookup .mockResolvedValueOnce({ rows: [] }) // getOrCreateUser - update last_active .mockResolvedValueOnce({ rows: [{ credits: 15 }] }) // UPDATE users .mockResolvedValueOnce({ rows: [] }) // UPDATE payments .mockResolvedValueOnce({ rows: [] }); // INSERT credit_transactions const result = await gmm.processCreditsPackageConfirmation('socket1', 123, { credits: 10, bonus: 5 }); expect(result.success).toBe(true); expect(result.creditsAdded).toBe(15); // 10 + 5 bonus }); test('falls back to default credits when package info missing and no description', async () => { const user = { id: 1, credits: 0 }; mockDb.query .mockResolvedValueOnce({ rows: [user] }) // getOrCreateUser - first lookup .mockResolvedValueOnce({ rows: [] }) // getOrCreateUser - update last_active .mockResolvedValueOnce({ rows: [] }) // SELECT payment - no description .mockResolvedValueOnce({ rows: [{ credits: 10 }] }) // UPDATE users .mockResolvedValueOnce({ rows: [] }) // UPDATE payments .mockResolvedValueOnce({ rows: [] }); // INSERT credit_transactions const result = await gmm.processCreditsPackageConfirmation('socket1', 123, null); expect(result.success).toBe(true); expect(result.creditsAdded).toBe(10); // Default fallback }); }); describe('canUserStartGame - mixed mode', () => { test('allows game with credits in mixed mode', async () => { gmm.paymentsEnabled = true; gmm.directModeEnabled = true; gmm.creditsModeEnabled = true; gmm.creditsPerGameCost = 1; mockDb.query .mockResolvedValueOnce({ rows: [{ id: 1, credits: 5 }] }) // getOrCreateUser lookup .mockResolvedValueOnce({ rows: [] }); // update last_active const result = await gmm.canUserStartGame('socket1'); expect(result.allowed).toBe(true); expect(result.useCredits).toBe(true); expect(result.effectiveMode).toBe('PAID_CREDITS'); }); test('allows game with confirmed payment when user has no credits', async () => { gmm.paymentsEnabled = true; gmm.directModeEnabled = true; gmm.creditsModeEnabled = true; gmm.creditsPerGameCost = 1; mockDb.query .mockResolvedValueOnce({ rows: [{ id: 1, credits: 0 }] }) // getOrCreateUser lookup .mockResolvedValueOnce({ rows: [] }) // update last_active .mockResolvedValueOnce({ rows: [{ id: 999 }] }); // confirmed payment query const result = await gmm.canUserStartGame('socket1'); expect(result.allowed).toBe(true); expect(result.paymentId).toBe(999); expect(result.effectiveMode).toBe('PAID_SINGLE'); }); test('denies game when no credits and no payment in mixed mode', async () => { gmm.paymentsEnabled = true; gmm.directModeEnabled = true; gmm.creditsModeEnabled = true; gmm.creditsPerGameCost = 1; mockDb.query .mockResolvedValueOnce({ rows: [{ id: 1, credits: 0 }] }) // getOrCreateUser lookup .mockResolvedValueOnce({ rows: [] }) // update last_active .mockResolvedValueOnce({ rows: [] }) // no confirmed payment .mockResolvedValueOnce({ rows: [{ id: 1, credits: 0 }] }) // getOrCreateUser for getPaymentOptions .mockResolvedValueOnce({ rows: [] }); // update last_active const result = await gmm.canUserStartGame('socket1'); expect(result.allowed).toBe(false); expect(result.action).toBe('choose_payment'); }); }); });