mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-24 17:48:34 +02:00
Payment Fixes: - Prevent credits going negative with WHERE credits >= $1 check - completeGame now uses recorded payment_mode from game record instead of global gameMode - Add duplicate payout prevention by checking for existing payouts before processing Dead Code Cleanup: - Remove unused MoneroPayService (WalletRPCService is the actual service) - Remove legacy MoneroPay references from comments Tests: - Add 50 comprehensive payment/payout tests (payment_payout_comprehensive.test.js) - Expand WalletRPCService tests (4 -> 31 tests) - Total: 106 tests passing across 10 test suites Documentation: - Update TODO.md with recent fixes and correct test counts - Update migration comments to reflect actual wallet service used
642 lines
26 KiB
JavaScript
642 lines
26 KiB
JavaScript
/**
|
|
* Comprehensive Payment and Payout Tests
|
|
* Deep testing of all payment flows, credits, payouts, and edge cases
|
|
*/
|
|
|
|
const GameModeManager = require('../src/game/gameModeManager');
|
|
|
|
// Mock dependencies
|
|
const createMockDb = () => ({
|
|
query: jest.fn()
|
|
});
|
|
|
|
const createMockWalletService = () => ({
|
|
createPaymentRequest: jest.fn(),
|
|
processPayout: jest.fn(),
|
|
startPaymentMonitoring: jest.fn(),
|
|
stopPaymentMonitoring: jest.fn()
|
|
});
|
|
|
|
const createMockDebugManager = () => ({
|
|
CONSOLE_LOGGING: false,
|
|
getCurrentBlockHeight: () => 12345
|
|
});
|
|
|
|
const createMockPaymentConfig = (overrides = {}) => ({
|
|
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 },
|
|
{ id: 'medium', credits: 25, price: '1000000000000', bonus: 2 }
|
|
] }
|
|
},
|
|
payouts: {
|
|
rules: {
|
|
direct: { enabled: true, multipliers: { escape: 2, escapeWithTreasure: 3 } },
|
|
credits: { enabled: false, multipliers: { escape: 1.5, escapeWithTreasure: 2 }, baseValue: 50000000000n }
|
|
}
|
|
},
|
|
preferences: { preferCreditsFirst: true },
|
|
earlyEntry: { enabled: true, allowInFreeMode: true, allowInCreditsMode: true },
|
|
...overrides
|
|
}),
|
|
getLegacyGameMode: () => 'PAID_SINGLE',
|
|
eventBus: { on: () => {} }
|
|
});
|
|
|
|
describe('Payment & Payout Comprehensive Tests', () => {
|
|
let gmm;
|
|
let mockDb;
|
|
let mockWalletService;
|
|
let mockDebugManager;
|
|
let mockPaymentConfig;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockDb = createMockDb();
|
|
mockWalletService = createMockWalletService();
|
|
mockDebugManager = createMockDebugManager();
|
|
mockPaymentConfig = createMockPaymentConfig();
|
|
|
|
gmm = new GameModeManager(mockDb, mockWalletService, mockDebugManager, mockPaymentConfig);
|
|
});
|
|
|
|
describe('Credit Deduction - Negative Balance Prevention', () => {
|
|
test('should fail gracefully when user has insufficient credits', async () => {
|
|
const user = { id: 1, credits: 0 };
|
|
gmm.creditsPerGameCost = 1;
|
|
|
|
// The credits update should return 0 rows because WHERE credits >= 1 fails
|
|
// (when user.credits = 0 and we require >= 1)
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // UPDATE users ... WHERE credits >= 1 - returns empty!
|
|
|
|
const result = await gmm._processGameStartWithCredits(user, 'socket1', 'game123');
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.reason).toBe('Insufficient credits');
|
|
expect(result.creditsRequired).toBe(1);
|
|
});
|
|
|
|
test('should succeed when user has exactly enough credits', async () => {
|
|
const user = { id: 1, credits: 1 };
|
|
gmm.creditsPerGameCost = 1;
|
|
|
|
// UPDATE returns the new balance of 0
|
|
mockDb.query.mockResolvedValueOnce({ rows: [{ credits: 0 }] }); // UPDATE succeeds
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // UPDATE games
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // INSERT credit_transactions
|
|
|
|
const result = await gmm._processGameStartWithCredits(user, 'socket1', 'game123');
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.creditsRemaining).toBe(0);
|
|
expect(result.creditsSpent).toBe(1);
|
|
});
|
|
|
|
test('should deduct correct amount for multi-credit games', async () => {
|
|
const user = { id: 1, credits: 5 };
|
|
gmm.creditsPerGameCost = 2;
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [{ credits: 3 }] }); // UPDATE succeeds
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // UPDATE games
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // INSERT credit_transactions
|
|
|
|
const result = await gmm._processGameStartWithCredits(user, 'socket1', 'game123');
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.creditsRemaining).toBe(3);
|
|
expect(result.creditsSpent).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('Payout Calculation', () => {
|
|
test('direct mode escape payout calculated correctly', () => {
|
|
gmm.singleGamePrice = 100000000000;
|
|
gmm.directPayoutMultipliers = { escape: 2, escapeWithTreasure: 3 };
|
|
|
|
const result = gmm.calculatePayout('PAID_SINGLE', { treasureFound: false });
|
|
|
|
expect(result.amount).toBe(200000000000);
|
|
expect(result.multiplier).toBe(2);
|
|
expect(result.base).toBe(100000000000);
|
|
});
|
|
|
|
test('direct mode treasure payout calculated correctly', () => {
|
|
gmm.singleGamePrice = 100000000000;
|
|
gmm.directPayoutMultipliers = { escape: 2, escapeWithTreasure: 3 };
|
|
|
|
const result = gmm.calculatePayout('PAID_SINGLE', { treasureFound: true });
|
|
|
|
expect(result.amount).toBe(300000000000);
|
|
expect(result.multiplier).toBe(3);
|
|
});
|
|
|
|
test('credits mode uses base value not single game price', () => {
|
|
gmm.singleGamePrice = 100000000000;
|
|
gmm.creditsPayoutBaseValue = 50000000000;
|
|
gmm.creditPayoutMultipliers = { escape: 2, escapeWithTreasure: 3 };
|
|
|
|
const result = gmm.calculatePayout('PAID_CREDITS', { treasureFound: false });
|
|
|
|
expect(result.amount).toBe(100000000000); // 50000000000 * 2
|
|
expect(result.base).toBe(50000000000);
|
|
});
|
|
|
|
test('zero multiplier returns zero payout', () => {
|
|
gmm.creditPayoutMultipliers = { escape: 0, escapeWithTreasure: 0 };
|
|
gmm.creditsPayoutBaseValue = 100000000000;
|
|
|
|
const result = gmm.calculatePayout('PAID_CREDITS', { treasureFound: false });
|
|
|
|
expect(result.amount).toBe(0);
|
|
});
|
|
|
|
test('fractional multipliers work correctly', () => {
|
|
gmm.singleGamePrice = 100000000000;
|
|
gmm.directPayoutMultipliers = { escape: 1.5, escapeWithTreasure: 2.5 };
|
|
|
|
const result = gmm.calculatePayout('PAID_SINGLE', { treasureFound: false });
|
|
|
|
expect(result.amount).toBe(150000000000);
|
|
});
|
|
});
|
|
|
|
describe('Mixed Mode Payment Selection', () => {
|
|
beforeEach(() => {
|
|
gmm.paymentsEnabled = true;
|
|
gmm.directModeEnabled = true;
|
|
gmm.creditsModeEnabled = true;
|
|
gmm.creditsPerGameCost = 1;
|
|
});
|
|
|
|
test('prefers credits when user has credits and preferCreditsFirst is true', async () => {
|
|
gmm.preferCreditsFirst = true;
|
|
const user = { id: 1, credits: 5, payout_address: 'wow1addr' };
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [user] }); // getOrCreateUser
|
|
mockDb.query.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('uses direct payment when user has no credits', async () => {
|
|
gmm.preferCreditsFirst = true;
|
|
const user = { id: 1, credits: 0, payout_address: 'wow1addr' };
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [user] }); // getOrCreateUser
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // update last_active
|
|
// Has confirmed payment
|
|
mockDb.query.mockResolvedValueOnce({ rows: [{ id: 123 }] }); // confirmed payment query
|
|
|
|
const result = await gmm.canUserStartGame('socket1');
|
|
|
|
expect(result.allowed).toBe(true);
|
|
expect(result.paymentId).toBe(123);
|
|
expect(result.effectiveMode).toBe('PAID_SINGLE');
|
|
});
|
|
|
|
test('denies when no credits and no confirmed payment', async () => {
|
|
const user = { id: 1, credits: 0, payout_address: 'wow1addr' };
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [user] }); // getOrCreateUser
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // update last_active
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // no confirmed payment
|
|
// getPaymentOptionsForUser will be called
|
|
mockDb.query.mockResolvedValueOnce({ rows: [user] }); // getOrCreateUser again
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // update last_active
|
|
|
|
const result = await gmm.canUserStartGame('socket1');
|
|
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.action).toBe('choose_payment');
|
|
});
|
|
});
|
|
|
|
describe('Credits Package Confirmation', () => {
|
|
test('adds credits from package info correctly', async () => {
|
|
const user = { id: 1, credits: 0 };
|
|
|
|
mockDb.query
|
|
.mockResolvedValueOnce({ rows: [user] }) // getOrCreateUser
|
|
.mockResolvedValueOnce({ rows: [] }) // 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);
|
|
expect(result.newBalance).toBe(15);
|
|
});
|
|
|
|
test('parses credits from payment description as fallback', async () => {
|
|
const user = { id: 1, credits: 5 };
|
|
|
|
mockDb.query
|
|
.mockResolvedValueOnce({ rows: [user] }) // getOrCreateUser
|
|
.mockResolvedValueOnce({ rows: [] }) // update last_active
|
|
.mockResolvedValueOnce({ rows: [{ description: 'Wowngeon 25 credits package (WOW)' }] }) // SELECT payments
|
|
.mockResolvedValueOnce({ rows: [{ credits: 30 }] }) // 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(25); // Parsed from description
|
|
});
|
|
|
|
test('defaults to 10 credits when no info available', async () => {
|
|
const user = { id: 1, credits: 0 };
|
|
|
|
mockDb.query
|
|
.mockResolvedValueOnce({ rows: [user] }) // getOrCreateUser
|
|
.mockResolvedValueOnce({ rows: [] }) // update last_active
|
|
.mockResolvedValueOnce({ rows: [{ description: 'Some payment' }] }) // SELECT payments - no credits pattern
|
|
.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('Game Completion & Payout Flow', () => {
|
|
beforeEach(() => {
|
|
gmm.gameMode = 'PAID_SINGLE';
|
|
gmm.singleGamePrice = 100000000000;
|
|
gmm.directPayoutMultipliers = { escape: 2, escapeWithTreasure: 3 };
|
|
});
|
|
|
|
test('processes winning game with payout correctly', async () => {
|
|
mockWalletService.processPayout.mockResolvedValueOnce({
|
|
success: true,
|
|
txHash: 'abc123',
|
|
fee: 1000000
|
|
});
|
|
|
|
// Game record lookup
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payment_mode: 'direct' }]
|
|
});
|
|
// Game update
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] });
|
|
// Check for existing payout
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] });
|
|
// User lookup
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payout_address: 'wow1testaddress' }]
|
|
});
|
|
|
|
const result = await gmm.completeGame('socket1', 'game123', true, false, { moves: 50 });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.payout).toBeDefined();
|
|
expect(result.payout.amount).toBe(200000000000);
|
|
expect(result.payout.multiplier).toBe(2);
|
|
expect(mockWalletService.processPayout).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
amount: 200000000000,
|
|
address: 'wow1testaddress'
|
|
})
|
|
);
|
|
});
|
|
|
|
test('treasure bonus applied correctly to payout', async () => {
|
|
mockWalletService.processPayout.mockResolvedValueOnce({
|
|
success: true,
|
|
txHash: 'abc123',
|
|
fee: 1000000
|
|
});
|
|
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payment_mode: 'direct' }]
|
|
});
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] });
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // no existing payout
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payout_address: 'wow1testaddress' }]
|
|
});
|
|
|
|
const result = await gmm.completeGame('socket1', 'game123', true, true, { moves: 30 });
|
|
|
|
expect(result.payout.amount).toBe(300000000000); // 3x for treasure
|
|
expect(result.payout.multiplier).toBe(3);
|
|
expect(result.payout.treasure).toBe(true);
|
|
});
|
|
|
|
test('no payout for losing game', async () => {
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payment_mode: 'direct' }]
|
|
});
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // game update
|
|
|
|
const result = await gmm.completeGame('socket1', 'game123', false, false, {});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.payout).toBeNull();
|
|
expect(mockWalletService.processPayout).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('no payout when user has no payout address', async () => {
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payment_mode: 'direct' }]
|
|
});
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // game update
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // no existing payout
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payout_address: null }] // No address
|
|
});
|
|
|
|
const result = await gmm.completeGame('socket1', 'game123', true, false, {});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.payout).toBeNull();
|
|
expect(result.reason).toBe('No payout address');
|
|
expect(mockWalletService.processPayout).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('prevents duplicate payout for same game', async () => {
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payment_mode: 'direct' }]
|
|
});
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // game update
|
|
// Existing payout found
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 999 }]
|
|
});
|
|
|
|
const result = await gmm.completeGame('socket1', 'game123', true, false, {});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.payout).toBeNull();
|
|
expect(result.reason).toBe('Payout already processed');
|
|
expect(mockWalletService.processPayout).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('uses recorded payment_mode not current gameMode', async () => {
|
|
// Current global mode is PAID_SINGLE
|
|
gmm.gameMode = 'PAID_SINGLE';
|
|
// But the game was started in credits mode
|
|
gmm.creditsPayoutEnabled = true;
|
|
gmm.creditsPayoutBaseValue = 50000000000;
|
|
gmm.creditPayoutMultipliers = { escape: 1.5, escapeWithTreasure: 2 };
|
|
|
|
mockWalletService.processPayout.mockResolvedValueOnce({
|
|
success: true,
|
|
txHash: 'xyz789'
|
|
});
|
|
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payment_mode: 'credits' }] // Game was started with credits
|
|
});
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // game update
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // no existing payout
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payout_address: 'wow1testaddress' }]
|
|
});
|
|
|
|
const result = await gmm.completeGame('socket1', 'game123', true, false, {});
|
|
|
|
// Should use credits multipliers, not direct
|
|
expect(result.payout.amount).toBe(75000000000); // 50000000000 * 1.5
|
|
expect(result.payout.multiplier).toBe(1.5);
|
|
});
|
|
});
|
|
|
|
describe('Payment Request Creation', () => {
|
|
test('creates single_game payment request correctly', async () => {
|
|
const user = { id: 1, credits: 0 };
|
|
gmm.singleGamePrice = 100000000000;
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [user] }); // getOrCreateUser
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // update last_active
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // no existing payment
|
|
|
|
mockWalletService.createPaymentRequest.mockResolvedValueOnce({
|
|
address: 'wow1paymentaddr',
|
|
expiresAt: new Date()
|
|
});
|
|
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 999, expires_at: new Date() }]
|
|
}); // INSERT payment
|
|
|
|
const result = await gmm.createPaymentRequest('socket1', 'single_game');
|
|
|
|
expect(result.address).toBe('wow1paymentaddr');
|
|
expect(result.amount).toBe(100000000000);
|
|
expect(result.paymentType).toBe('single_game');
|
|
expect(mockWalletService.createPaymentRequest).toHaveBeenCalledWith(
|
|
100000000000,
|
|
expect.stringContaining('single game'),
|
|
1,
|
|
'socket1'
|
|
);
|
|
});
|
|
|
|
test('creates credits_package payment request correctly', async () => {
|
|
const user = { id: 1, credits: 0 };
|
|
gmm.creditsPackagePrice = 500000000000;
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [user] }); // getOrCreateUser
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // update last_active
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // no existing payment
|
|
|
|
mockWalletService.createPaymentRequest.mockResolvedValueOnce({
|
|
address: 'wow1creditspayment',
|
|
expiresAt: new Date()
|
|
});
|
|
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1000, expires_at: new Date() }]
|
|
}); // INSERT payment
|
|
|
|
const result = await gmm.createPaymentRequest('socket1', 'credits_package');
|
|
|
|
expect(result.address).toBe('wow1creditspayment');
|
|
expect(result.paymentType).toBe('credits_package');
|
|
expect(result.package).toBeDefined();
|
|
});
|
|
|
|
test('reuses existing pending payment request', async () => {
|
|
const user = { id: 1, credits: 0 };
|
|
gmm.singleGamePrice = 100000000000;
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [user] }); // getOrCreateUser
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // update last_active
|
|
// Existing pending payment
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{
|
|
id: 555,
|
|
subaddress: 'wow1existingaddr',
|
|
expected_amount: '100000000000',
|
|
payment_type: 'single_game',
|
|
status: 'pending',
|
|
expires_at: new Date(Date.now() + 1800000)
|
|
}]
|
|
});
|
|
|
|
const result = await gmm.createPaymentRequest('socket1', 'single_game', { reuseExisting: true });
|
|
|
|
expect(result.id).toBe(555);
|
|
expect(result.address).toBe('wow1existingaddr');
|
|
expect(result.reused).toBe(true);
|
|
// Should not create a new payment
|
|
expect(mockWalletService.createPaymentRequest).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('throws on invalid payment type', async () => {
|
|
const user = { id: 1, credits: 0 };
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [user] }); // getOrCreateUser
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // update last_active
|
|
|
|
await expect(gmm.createPaymentRequest('socket1', 'invalid_type'))
|
|
.rejects.toThrow(/Invalid payment type/);
|
|
});
|
|
});
|
|
|
|
describe('Game Mode Info', () => {
|
|
test('returns complete game mode info', () => {
|
|
gmm.gameMode = 'PAID_SINGLE';
|
|
gmm.cryptoType = 'WOW';
|
|
gmm.singleGamePrice = 100000000000;
|
|
gmm.paymentsEnabled = true;
|
|
gmm.directModeEnabled = true;
|
|
gmm.creditsModeEnabled = false;
|
|
|
|
const info = gmm.getGameModeInfo();
|
|
|
|
expect(info.mode).toBe('PAID_SINGLE');
|
|
expect(info.cryptoType).toBe('WOW');
|
|
expect(info.singleGamePrice).toBe(100000000000);
|
|
expect(info.paymentsEnabled).toBe(true);
|
|
expect(info.directModeEnabled).toBe(true);
|
|
expect(info.creditsModeEnabled).toBe(false);
|
|
expect(info.earlyEntry).toBeDefined();
|
|
expect(info.payoutMultipliers).toBeDefined();
|
|
});
|
|
|
|
test('includes testnet warning for XMR stagenet', () => {
|
|
gmm.cryptoType = 'XMR';
|
|
gmm.network = 'stagenet';
|
|
gmm.isTestNetwork = true;
|
|
|
|
const info = gmm.getGameModeInfo();
|
|
|
|
expect(info.isTestNetwork).toBe(true);
|
|
expect(info.testnetWarning).toContain('STAGENET');
|
|
});
|
|
|
|
test('no testnet warning for WOW (only has mainnet)', () => {
|
|
gmm.cryptoType = 'WOW';
|
|
gmm.network = 'mainnet';
|
|
gmm.isTestNetwork = false;
|
|
|
|
const info = gmm.getGameModeInfo();
|
|
|
|
expect(info.isTestNetwork).toBe(false);
|
|
expect(info.testnetWarning).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('User Stats', () => {
|
|
test('returns user statistics from database function', async () => {
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{
|
|
total_games: 100,
|
|
games_won: 45,
|
|
win_rate: 0.45,
|
|
total_paid: 5000000000000,
|
|
total_won: 6000000000000,
|
|
net_profit: 1000000000000,
|
|
credits_remaining: 15
|
|
}]
|
|
});
|
|
|
|
const stats = await gmm.getUserStats('socket1');
|
|
|
|
expect(stats.total_games).toBe(100);
|
|
expect(stats.games_won).toBe(45);
|
|
expect(stats.net_profit).toBe(1000000000000);
|
|
});
|
|
|
|
test('returns default stats when none found', async () => {
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] });
|
|
|
|
const stats = await gmm.getUserStats('socket1');
|
|
|
|
expect(stats.total_games).toBe(0);
|
|
expect(stats.games_won).toBe(0);
|
|
expect(stats.net_profit).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases and Error Handling', () => {
|
|
let gmm;
|
|
let mockDb;
|
|
let mockWalletService;
|
|
|
|
beforeEach(() => {
|
|
mockDb = createMockDb();
|
|
mockWalletService = createMockWalletService();
|
|
gmm = new GameModeManager(mockDb, mockWalletService, createMockDebugManager(), createMockPaymentConfig());
|
|
});
|
|
|
|
test('handles wallet service error during payout', async () => {
|
|
gmm.gameMode = 'PAID_SINGLE';
|
|
|
|
mockWalletService.processPayout.mockRejectedValueOnce(new Error('Wallet unavailable'));
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [{ id: 1, payment_mode: 'direct' }] });
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // game update
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // no existing payout
|
|
mockDb.query.mockResolvedValueOnce({
|
|
rows: [{ id: 1, payout_address: 'wow1testaddress' }]
|
|
});
|
|
|
|
const result = await gmm.completeGame('socket1', 'game123', true, false, {});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.payout).toBeNull();
|
|
expect(result.payoutError).toBeDefined();
|
|
});
|
|
|
|
test('handles database error during credits deduction', async () => {
|
|
const user = { id: 1, credits: 5 };
|
|
|
|
mockDb.query.mockRejectedValueOnce(new Error('Database connection lost'));
|
|
|
|
await expect(gmm._processGameStartWithCredits(user, 'socket1', 'game123'))
|
|
.rejects.toThrow('Database connection lost');
|
|
});
|
|
|
|
test('handles payment creation when wallet is unavailable', async () => {
|
|
const user = { id: 1 };
|
|
|
|
mockDb.query.mockResolvedValueOnce({ rows: [user] }); // getOrCreateUser
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // update last_active
|
|
mockDb.query.mockResolvedValueOnce({ rows: [] }); // no existing payment
|
|
|
|
mockWalletService.createPaymentRequest.mockRejectedValueOnce(
|
|
new Error('RPC connection refused')
|
|
);
|
|
|
|
await expect(gmm.createPaymentRequest('socket1', 'single_game'))
|
|
.rejects.toThrow();
|
|
});
|
|
});
|