From 4d9b6c74e945b32a954df84305fe0661008f8964 Mon Sep 17 00:00:00 2001 From: ONEONUORA Date: Thu, 26 Feb 2026 12:38:57 +0100 Subject: [PATCH] feat: Backend unit tests for Prediction service --- backend/src/services/blockchain/market.ts | 33 + backend/src/services/cron.service.ts | 36 +- backend/src/services/prediction.service.ts | 52 +- backend/tests/auth.integration.test.ts | 63 +- .../database/transaction.integration.test.ts | 13 +- ...rket-lifecycle.service.integration.test.ts | 395 +++++---- .../integration/markets.integration.test.ts | 834 +++++++++--------- .../integration/oracle.integration.test.ts | 78 +- .../integration/trading.integration.test.ts | 39 +- .../integration/treasury.integration.test.ts | 43 +- .../tests/middleware/error.middleware.test.ts | 64 +- backend/tests/middleware/integration.test.ts | 78 +- .../middleware/validation.middleware.test.ts | 164 ++-- .../middleware/validation.schemas.test.ts | 55 +- .../market.repository.integration.test.ts | 14 +- .../user.repository.integration.test.ts | 9 +- backend/tests/services/auth.service.test.ts | 17 +- .../tests/services/blockchain/base.test.ts | 20 +- backend/tests/services/cron.service.test.ts | 36 +- .../services/market.service.unit.test.ts | 228 ++--- .../tests/services/prediction.service.test.ts | 540 ++++++++++++ .../services/user.service.integration.test.ts | 12 +- backend/tests/setup.ts | 17 +- 23 files changed, 1817 insertions(+), 1023 deletions(-) create mode 100644 backend/tests/services/prediction.service.test.ts diff --git a/backend/src/services/blockchain/market.ts b/backend/src/services/blockchain/market.ts index 2e91ae9e..83166b9b 100644 --- a/backend/src/services/blockchain/market.ts +++ b/backend/src/services/blockchain/market.ts @@ -127,6 +127,39 @@ export class MarketBlockchainService extends BaseBlockchainService { ); } } + + /** + * Commit a prediction on the blockchain + */ + async commitPrediction( + marketContractAddress: string, + commitmentHash: string, + amountUsdc: number + ): Promise { + // TODO: Implement actual Stellar contract call + logger.info('Blockchain: commiting prediction', { + marketContractAddress, + commitmentHash, + amountUsdc, + }); + return { txHash: 'mock-commit-tx-' + Date.now() }; + } + + /** + * Reveal a prediction on the blockchain + */ + async revealPrediction( + marketContractAddress: string, + predictedOutcome: number, + salt: string + ): Promise { + // TODO: Implement actual Stellar contract call + logger.info('Blockchain: revealing prediction', { + marketContractAddress, + predictedOutcome, + }); + return { txHash: 'mock-reveal-tx-' + Date.now() }; + } } export const marketBlockchainService = new MarketBlockchainService(); diff --git a/backend/src/services/cron.service.ts b/backend/src/services/cron.service.ts index 79236dcd..e3c22920 100644 --- a/backend/src/services/cron.service.ts +++ b/backend/src/services/cron.service.ts @@ -10,10 +10,7 @@ export class CronService { private marketRepository: MarketRepository; private marketService: MarketService; - constructor( - marketRepo?: MarketRepository, - marketSvc?: MarketService - ) { + constructor(marketRepo?: MarketRepository, marketSvc?: MarketService) { this.marketRepository = marketRepo || new MarketRepository(); this.marketService = marketSvc || new MarketService(); } @@ -53,7 +50,8 @@ export class CronService { let markets; try { - markets = await this.marketRepository.getClosedMarketsAwaitingResolution(); + markets = + await this.marketRepository.getClosedMarketsAwaitingResolution(); } catch (error) { logger.error('Oracle polling: failed to fetch closed markets', { error }); return; @@ -64,20 +62,27 @@ export class CronService { return; } - logger.info(`Oracle polling: checking consensus for ${markets.length} market(s)`); + logger.info( + `Oracle polling: checking consensus for ${markets.length} market(s)` + ); for (const market of markets) { try { const winningOutcome = await oracleService.checkConsensus(market.id); if (winningOutcome === null) { - logger.info(`Oracle polling: no consensus yet for market ${market.id}`); + logger.info( + `Oracle polling: no consensus yet for market ${market.id}` + ); continue; } - logger.info(`Oracle polling: consensus reached for market ${market.id}`, { - winningOutcome, - }); + logger.info( + `Oracle polling: consensus reached for market ${market.id}`, + { + winningOutcome, + } + ); const resolved = await this.marketService.resolveMarket( market.id, @@ -85,10 +90,13 @@ export class CronService { 'oracle-consensus' ); - logger.info(`Oracle polling: market ${market.id} resolved successfully`, { - winningOutcome, - resolvedAt: resolved.resolvedAt, - }); + logger.info( + `Oracle polling: market ${market.id} resolved successfully`, + { + winningOutcome, + resolvedAt: resolved.resolvedAt, + } + ); } catch (error) { logger.error(`Oracle polling: failed to process market ${market.id}`, { error, diff --git a/backend/src/services/prediction.service.ts b/backend/src/services/prediction.service.ts index 4cda04e2..9596edcb 100644 --- a/backend/src/services/prediction.service.ts +++ b/backend/src/services/prediction.service.ts @@ -10,16 +10,27 @@ import { encrypt, decrypt, } from '../utils/crypto.js'; +import { + marketBlockchainService, + MarketBlockchainService, +} from './blockchain/market.js'; export class PredictionService { private predictionRepository: PredictionRepository; private marketRepository: MarketRepository; private userRepository: UserRepository; + private blockchainService: MarketBlockchainService; - constructor() { - this.predictionRepository = new PredictionRepository(); - this.marketRepository = new MarketRepository(); - this.userRepository = new UserRepository(); + constructor( + predictionRepo?: PredictionRepository, + marketRepo?: MarketRepository, + userRepo?: UserRepository, + blockchainSvc?: MarketBlockchainService + ) { + this.predictionRepository = predictionRepo || new PredictionRepository(); + this.marketRepository = marketRepo || new MarketRepository(); + this.userRepository = userRepo || new UserRepository(); + this.blockchainService = blockchainSvc || marketBlockchainService; } /** @@ -87,13 +98,12 @@ export class PredictionService { // Encrypt salt for secure storage const { encrypted: encryptedSalt, iv: saltIv } = encrypt(salt); - // TODO: Call blockchain contract - Market.commit_prediction() - // const txHash = await blockchainService.commitPrediction( - // marketId, - // commitmentHash, - // amountUsdc - // ); - const txHash = 'mock-tx-hash-' + Date.now(); // Mock for now + // Call blockchain contract - Market.commit_prediction() + const { txHash } = await this.blockchainService.commitPrediction( + market.contractAddress, + commitmentHash, + amountUsdc + ); // Create prediction and update balances in transaction return await executeTransaction(async (tx) => { @@ -170,15 +180,10 @@ export class PredictionService { // Decrypt the stored salt const salt = decrypt(prediction.encryptedSalt, prediction.saltIv); - // TODO: Call blockchain contract - Market.reveal_prediction() - // const revealTxHash = await blockchainService.revealPrediction( - // marketId, - // predictedOutcome, - // salt - // ); - const revealTxHash = 'mock-reveal-tx-' + Date.now(); // Mock for now - - // Calculate the original predicted outcome from commitment hash + // Call blockchain contract - Market.reveal_prediction() + // We reveal ONLY after finding the correct outcome below + // (Actually the reveal call on chain needs the outcome and salt) + // First, calculate the original predicted outcome from commitment hash // We need to try both outcomes to verify which one matches let predictedOutcome: number | null = null; for (const outcome of [0, 1]) { @@ -195,6 +200,13 @@ export class PredictionService { ); } + const { txHash: revealTxHash } = + await this.blockchainService.revealPrediction( + market.contractAddress, + predictedOutcome, + salt + ); + // Update prediction to revealed status return await this.predictionRepository.revealPrediction( predictionId, diff --git a/backend/tests/auth.integration.test.ts b/backend/tests/auth.integration.test.ts index 86a776de..e7394985 100644 --- a/backend/tests/auth.integration.test.ts +++ b/backend/tests/auth.integration.test.ts @@ -1,4 +1,12 @@ -import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest'; +import { + describe, + it, + expect, + beforeAll, + afterAll, + beforeEach, + vi, +} from 'vitest'; import { Keypair } from '@stellar/stellar-sdk'; import Redis from 'ioredis'; import jwt from 'jsonwebtoken'; @@ -9,7 +17,14 @@ const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379'); // Import services import { SessionService } from '../src/services/session.service.js'; import { StellarService } from '../src/services/stellar.service.js'; -import { signAccessToken, signRefreshToken, verifyAccessToken, verifyRefreshToken, getAccessTokenTTLSeconds, getRefreshTokenTTLSeconds } from '../src/utils/jwt.js'; +import { + signAccessToken, + signRefreshToken, + verifyAccessToken, + verifyRefreshToken, + getAccessTokenTTLSeconds, + getRefreshTokenTTLSeconds, +} from '../src/utils/jwt.js'; import { generateNonce } from '../src/utils/crypto.js'; describe('Auth Integration Tests', () => { @@ -53,7 +68,10 @@ describe('Auth Integration Tests', () => { expect(isValid).toBe(true); // Step 4: Consume nonce (simulating login) - const consumed = await sessionService.consumeNonce(publicKey, nonceData.nonce); + const consumed = await sessionService.consumeNonce( + publicKey, + nonceData.nonce + ); expect(consumed).not.toBeNull(); // Step 5: Generate tokens @@ -109,7 +127,10 @@ describe('Auth Integration Tests', () => { await sessionService.consumeNonce(publicKey, nonceData.nonce); // Try to use same nonce again (replay attack) - const consumed = await sessionService.consumeNonce(publicKey, nonceData.nonce); + const consumed = await sessionService.consumeNonce( + publicKey, + nonceData.nonce + ); expect(consumed).toBeNull(); }); }); @@ -260,7 +281,10 @@ describe('Auth Integration Tests', () => { }) ); - const consumed = await sessionService.consumeNonce(publicKey, expiredNonce); + const consumed = await sessionService.consumeNonce( + publicKey, + expiredNonce + ); expect(consumed).toBeNull(); }); @@ -302,9 +326,15 @@ describe('Auth Integration Tests', () => { const publicKey = keypair.publicKey(); const nonceData = await sessionService.createNonce(publicKey); - const signature = keypair.sign(Buffer.from(nonceData.message)).toString('base64'); + const signature = keypair + .sign(Buffer.from(nonceData.message)) + .toString('base64'); - const isValid = stellarService.verifySignature(publicKey, nonceData.message, signature); + const isValid = stellarService.verifySignature( + publicKey, + nonceData.message, + signature + ); expect(isValid).toBe(true); }); @@ -313,7 +343,9 @@ describe('Auth Integration Tests', () => { const keypair2 = Keypair.random(); const nonceData = await sessionService.createNonce(keypair1.publicKey()); - const signature = keypair2.sign(Buffer.from(nonceData.message)).toString('base64'); + const signature = keypair2 + .sign(Buffer.from(nonceData.message)) + .toString('base64'); const isValid = stellarService.verifySignature( keypair1.publicKey(), @@ -345,10 +377,16 @@ describe('Auth Integration Tests', () => { const publicKey = keypair.publicKey(); const nonceData = await sessionService.createNonce(publicKey); - const signature = keypair.sign(Buffer.from(nonceData.message)).toString('base64'); + const signature = keypair + .sign(Buffer.from(nonceData.message)) + .toString('base64'); const tamperedMessage = nonceData.message + ' TAMPERED'; - const isValid = stellarService.verifySignature(publicKey, tamperedMessage, signature); + const isValid = stellarService.verifySignature( + publicKey, + tamperedMessage, + signature + ); expect(isValid).toBe(false); }); }); @@ -416,7 +454,10 @@ describe('Auth Integration Tests', () => { }); it('should reject refresh token used as access token', () => { - const refreshToken = signRefreshToken({ userId: 'test', tokenId: 'test' }); + const refreshToken = signRefreshToken({ + userId: 'test', + tokenId: 'test', + }); expect(() => verifyAccessToken(refreshToken)).toThrow(); }); diff --git a/backend/tests/database/transaction.integration.test.ts b/backend/tests/database/transaction.integration.test.ts index 4b5b5783..ce59ab68 100644 --- a/backend/tests/database/transaction.integration.test.ts +++ b/backend/tests/database/transaction.integration.test.ts @@ -1,6 +1,9 @@ // Integration tests for transaction utilities import { describe, it, expect, beforeEach } from 'vitest'; -import { executeTransaction, executeTransactionWithRetry } from '../../src/database/transaction.js'; +import { + executeTransaction, + executeTransactionWithRetry, +} from '../../src/database/transaction.js'; import { UserRepository } from '../../src/repositories/user.repository.js'; import { MarketRepository } from '../../src/repositories/market.repository.js'; import { MarketCategory } from '@prisma/client'; @@ -13,7 +16,7 @@ describe('Transaction Utilities Integration Tests', () => { it('should commit transaction on success', async () => { const result = await executeTransaction(async (tx) => { const userRepoTx = new UserRepository(tx); - + const user = await userRepoTx.createUser({ email: `tx-success-${Date.now()}@example.com`, username: `txsuccess-${Date.now()}`, @@ -36,7 +39,7 @@ describe('Transaction Utilities Integration Tests', () => { try { await executeTransaction(async (tx) => { const userRepoTx = new UserRepository(tx); - + await userRepoTx.createUser({ email, username: `txrollback-${Date.now()}`, @@ -65,7 +68,7 @@ describe('Transaction Utilities Integration Tests', () => { }); const contractAddress = `CONTRACT_PARTIAL_${Date.now()}`; - + try { await executeTransaction(async (tx) => { const userRepoTx = new UserRepository(tx); @@ -109,7 +112,7 @@ describe('Transaction Utilities Integration Tests', () => { const result = await executeTransactionWithRetry(async (tx) => { attemptCount++; - + if (attemptCount < 2) { throw new Error('Simulated transient failure'); } diff --git a/backend/tests/integration/market-lifecycle.service.integration.test.ts b/backend/tests/integration/market-lifecycle.service.integration.test.ts index 28d13f7b..ee7a1555 100644 --- a/backend/tests/integration/market-lifecycle.service.integration.test.ts +++ b/backend/tests/integration/market-lifecycle.service.integration.test.ts @@ -1,6 +1,20 @@ // backend/tests/integration/market-lifecycle.service.integration.test.ts -import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest'; -import { PrismaClient, MarketStatus, TradeType, TradeStatus, PredictionStatus } from '@prisma/client'; +import { + describe, + it, + expect, + beforeAll, + afterAll, + beforeEach, + vi, +} from 'vitest'; +import { + PrismaClient, + MarketStatus, + TradeType, + TradeStatus, + PredictionStatus, +} from '@prisma/client'; import { MarketService } from '../../src/services/market.service.js'; import { TradingService } from '../../src/services/trading.service.js'; import { PredictionService } from '../../src/services/prediction.service.js'; @@ -10,190 +24,217 @@ import { prisma } from '../../src/database/prisma.js'; // Mock blockchain services vi.mock('../../src/services/blockchain/amm.js', () => ({ - ammService: { - createPool: vi.fn(), - buyShares: vi.fn(), - sellShares: vi.fn(), - getOdds: vi.fn(), - getPoolState: vi.fn(), - }, + ammService: { + createPool: vi.fn(), + buyShares: vi.fn(), + sellShares: vi.fn(), + getOdds: vi.fn(), + getPoolState: vi.fn(), + }, })); vi.mock('../../src/services/blockchain/factory.js', () => ({ - factoryService: { - createMarket: vi.fn(), - getMarketCount: vi.fn(), - }, + factoryService: { + createMarket: vi.fn(), + getMarketCount: vi.fn(), + }, })); describe('Market Lifecycle Service Integration', () => { - let marketService: MarketService; - let tradingService: TradingService; - let predictionService: PredictionService; - - let testUser: any; - let testMarket: any; - - beforeAll(async () => { - marketService = new MarketService(); - tradingService = new TradingService(); - predictionService = new PredictionService(); - - // Create test user - testUser = await prisma.user.create({ - data: { - email: `lifecycle-${Date.now()}@test.com`, - username: `lifecycle_user_${Date.now()}`, - passwordHash: 'hash', - walletAddress: 'GTEST' + Math.random().toString(36).substring(2, 15).toUpperCase() + 'X'.repeat(40), - usdcBalance: 10000, - xlmBalance: 1000 - } - }); + let marketService: MarketService; + let tradingService: TradingService; + let predictionService: PredictionService; + + let testUser: any; + let testMarket: any; + + beforeAll(async () => { + marketService = new MarketService(); + tradingService = new TradingService(); + predictionService = new PredictionService(); + + // Create test user + testUser = await prisma.user.create({ + data: { + email: `lifecycle-${Date.now()}@test.com`, + username: `lifecycle_user_${Date.now()}`, + passwordHash: 'hash', + walletAddress: + 'GTEST' + + Math.random().toString(36).substring(2, 15).toUpperCase() + + 'X'.repeat(40), + usdcBalance: 10000, + xlmBalance: 1000, + }, }); + }); + + afterAll(async () => { + if (testMarket) { + // Need to delete related entities first + await prisma.trade.deleteMany({ where: { marketId: testMarket.id } }); + await prisma.prediction.deleteMany({ + where: { marketId: testMarket.id }, + }); + await prisma.share.deleteMany({ where: { marketId: testMarket.id } }); + await prisma.market.delete({ where: { id: testMarket.id } }); + } + if (testUser) { + await prisma.user.delete({ where: { id: testUser.id } }); + } + }); + + it('should complete full market lifecycle using services', async () => { + // 1. Create Market + const futureDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + const mockMarketId = '0x' + '1'.repeat(64); + + vi.mocked(factoryService.createMarket).mockResolvedValue({ + marketId: mockMarketId, + txHash: 'factory-tx-hash', + contractAddress: 'factory-address', + }); + + testMarket = await marketService.createMarket({ + title: 'Integration Test Market', + description: 'Full lifecycle test market', + category: 'WRESTLING', + creatorId: testUser.id, + creatorPublicKey: testUser.walletAddress, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: futureDate, + }); + + expect(testMarket.status).toBe(MarketStatus.OPEN); + expect(testMarket.contractAddress).toBe(mockMarketId); + + // 2. Initialize Pool + const poolLiquidity = 1000000000n; // 1000 USDC in stroops (assuming 6 decimals) + + vi.mocked(ammService.createPool).mockResolvedValue({ + txHash: 'pool-tx-hash', + reserves: { yes: 500000000n, no: 500000000n }, + odds: { yes: 0.5, no: 0.5 }, + }); + + const poolResult = await marketService.createPool( + testMarket.id, + poolLiquidity + ); + + expect(poolResult.txHash).toBe('pool-tx-hash'); + expect(poolResult.reserves.yes).toBe(500); // 500.000000 + expect(poolResult.reserves.no).toBe(500); - afterAll(async () => { - if (testMarket) { - // Need to delete related entities first - await prisma.trade.deleteMany({ where: { marketId: testMarket.id } }); - await prisma.prediction.deleteMany({ where: { marketId: testMarket.id } }); - await prisma.share.deleteMany({ where: { marketId: testMarket.id } }); - await prisma.market.delete({ where: { id: testMarket.id } }); - } - if (testUser) { - await prisma.user.delete({ where: { id: testUser.id } }); - } + const marketAfterPool = await prisma.market.findUnique({ + where: { id: testMarket.id }, + }); + expect(Number(marketAfterPool?.yesLiquidity)).toBe(500); + + // 3. Trade (Buy YES shares) + const buyAmount = 100; + vi.mocked(ammService.buyShares).mockResolvedValue({ + sharesReceived: 100, + pricePerUnit: 1.0, + totalCost: 100, + feeAmount: 1, + txHash: 'buy-tx-hash', + }); + + const buyResult = await tradingService.buyShares({ + userId: testUser.id, + marketId: testMarket.id, + outcome: 1, // YES + amount: buyAmount, }); - it('should complete full market lifecycle using services', async () => { - // 1. Create Market - const futureDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); - const mockMarketId = '0x' + '1'.repeat(64); - - vi.mocked(factoryService.createMarket).mockResolvedValue({ - marketId: mockMarketId, - txHash: 'factory-tx-hash', - contractAddress: 'factory-address' - }); - - testMarket = await marketService.createMarket({ - title: 'Integration Test Market', - description: 'Full lifecycle test market', - category: 'WRESTLING', - creatorId: testUser.id, - creatorPublicKey: testUser.walletAddress, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: futureDate, - }); - - expect(testMarket.status).toBe(MarketStatus.OPEN); - expect(testMarket.contractAddress).toBe(mockMarketId); - - // 2. Initialize Pool - const poolLiquidity = 1000000000n; // 1000 USDC in stroops (assuming 6 decimals) - - vi.mocked(ammService.createPool).mockResolvedValue({ - txHash: 'pool-tx-hash', - reserves: { yes: 500000000n, no: 500000000n }, - odds: { yes: 0.5, no: 0.5 } - }); - - const poolResult = await marketService.createPool(testMarket.id, poolLiquidity); - - expect(poolResult.txHash).toBe('pool-tx-hash'); - expect(poolResult.reserves.yes).toBe(500); // 500.000000 - expect(poolResult.reserves.no).toBe(500); - - const marketAfterPool = await prisma.market.findUnique({ where: { id: testMarket.id } }); - expect(Number(marketAfterPool?.yesLiquidity)).toBe(500); - - // 3. Trade (Buy YES shares) - const buyAmount = 100; - vi.mocked(ammService.buyShares).mockResolvedValue({ - sharesReceived: 100, - pricePerUnit: 1.0, - totalCost: 100, - feeAmount: 1, - txHash: 'buy-tx-hash' - }); - - const buyResult = await tradingService.buyShares({ - userId: testUser.id, - marketId: testMarket.id, - outcome: 1, // YES - amount: buyAmount, - }); - - expect(buyResult.sharesBought).toBe(100); - expect(buyResult.txHash).toBe('buy-tx-hash'); - - const userAfterBuy = await prisma.user.findUnique({ where: { id: testUser.id } }); - expect(Number(userAfterBuy?.usdcBalance)).toBe(10000 - 100); // Only totalCost is deducted in current implementation - - // 4. Prediction (Commit and Reveal) - const predictionAmount = 50; - const prediction = await predictionService.commitPrediction( - testUser.id, - testMarket.id, - 1, // YES - predictionAmount - ); - - expect(prediction.status).toBe(PredictionStatus.COMMITTED); - expect(Number(prediction.amountUsdc)).toBe(50); - - const userAfterCommit = await prisma.user.findUnique({ where: { id: testUser.id } }); - expect(Number(userAfterCommit?.usdcBalance)).toBe(Number(userAfterBuy?.usdcBalance) - predictionAmount); - - // Reveal prediction (usually happens before closing, but let's test the flow) - const revealedPrediction = await predictionService.revealPrediction( - testUser.id, - prediction.id, - testMarket.id - ); - expect(revealedPrediction.status).toBe(PredictionStatus.REVEALED); - - // 5. Close Market - const closedMarket = await marketService.closeMarket(testMarket.id); - expect(closedMarket.status).toBe(MarketStatus.CLOSED); - - // 6. Resolve Market (YES wins) - const resolvedMarket = await marketService.resolveMarket( - testMarket.id, - 1, // YES wins - 'manual-integration-test' - ); - - expect(resolvedMarket.status).toBe(MarketStatus.RESOLVED); - expect(resolvedMarket.winningOutcome).toBe(1); - - // 7. Settle Predictions (already handled by resolveMarket internally) - const settledPrediction = await prisma.prediction.findUnique({ where: { id: prediction.id } }); - expect(settledPrediction?.status).toBe(PredictionStatus.SETTLED); - expect(settledPrediction?.isWinner).toBe(true); - expect(Number(settledPrediction?.pnlUsd)).toBeGreaterThan(0); - - // 8. Claim Winnings - const balanceBeforeClaim = (await prisma.user.findUnique({ where: { id: testUser.id } }))?.usdcBalance; - - const claimResult = await predictionService.claimWinnings(testUser.id, prediction.id); - expect(claimResult.winnings).toBeGreaterThan(0); - - const finalUser = await prisma.user.findUnique({ where: { id: testUser.id } }); - expect(Number(finalUser?.usdcBalance)).toBe(Number(balanceBeforeClaim) + claimResult.winnings); - - // Final state checks - const finalMarket = await prisma.market.findUnique({ - where: { id: testMarket.id }, - include: { - trades: true, - predictions: true - } - }); - - expect(finalMarket?.trades.length).toBe(1); - expect(finalMarket?.predictions.length).toBe(1); - expect(finalMarket?.status).toBe(MarketStatus.RESOLVED); + expect(buyResult.sharesBought).toBe(100); + expect(buyResult.txHash).toBe('buy-tx-hash'); + + const userAfterBuy = await prisma.user.findUnique({ + where: { id: testUser.id }, + }); + expect(Number(userAfterBuy?.usdcBalance)).toBe(10000 - 100); // Only totalCost is deducted in current implementation + + // 4. Prediction (Commit and Reveal) + const predictionAmount = 50; + const prediction = await predictionService.commitPrediction( + testUser.id, + testMarket.id, + 1, // YES + predictionAmount + ); + + expect(prediction.status).toBe(PredictionStatus.COMMITTED); + expect(Number(prediction.amountUsdc)).toBe(50); + + const userAfterCommit = await prisma.user.findUnique({ + where: { id: testUser.id }, }); + expect(Number(userAfterCommit?.usdcBalance)).toBe( + Number(userAfterBuy?.usdcBalance) - predictionAmount + ); + + // Reveal prediction (usually happens before closing, but let's test the flow) + const revealedPrediction = await predictionService.revealPrediction( + testUser.id, + prediction.id, + testMarket.id + ); + expect(revealedPrediction.status).toBe(PredictionStatus.REVEALED); + + // 5. Close Market + const closedMarket = await marketService.closeMarket(testMarket.id); + expect(closedMarket.status).toBe(MarketStatus.CLOSED); + + // 6. Resolve Market (YES wins) + const resolvedMarket = await marketService.resolveMarket( + testMarket.id, + 1, // YES wins + 'manual-integration-test' + ); + + expect(resolvedMarket.status).toBe(MarketStatus.RESOLVED); + expect(resolvedMarket.winningOutcome).toBe(1); + + // 7. Settle Predictions (already handled by resolveMarket internally) + const settledPrediction = await prisma.prediction.findUnique({ + where: { id: prediction.id }, + }); + expect(settledPrediction?.status).toBe(PredictionStatus.SETTLED); + expect(settledPrediction?.isWinner).toBe(true); + expect(Number(settledPrediction?.pnlUsd)).toBeGreaterThan(0); + + // 8. Claim Winnings + const balanceBeforeClaim = ( + await prisma.user.findUnique({ where: { id: testUser.id } }) + )?.usdcBalance; + + const claimResult = await predictionService.claimWinnings( + testUser.id, + prediction.id + ); + expect(claimResult.winnings).toBeGreaterThan(0); + + const finalUser = await prisma.user.findUnique({ + where: { id: testUser.id }, + }); + expect(Number(finalUser?.usdcBalance)).toBe( + Number(balanceBeforeClaim) + claimResult.winnings + ); + + // Final state checks + const finalMarket = await prisma.market.findUnique({ + where: { id: testMarket.id }, + include: { + trades: true, + predictions: true, + }, + }); + + expect(finalMarket?.trades.length).toBe(1); + expect(finalMarket?.predictions.length).toBe(1); + expect(finalMarket?.status).toBe(MarketStatus.RESOLVED); + }); }); diff --git a/backend/tests/integration/markets.integration.test.ts b/backend/tests/integration/markets.integration.test.ts index 76567343..573384cb 100644 --- a/backend/tests/integration/markets.integration.test.ts +++ b/backend/tests/integration/markets.integration.test.ts @@ -1,7 +1,15 @@ // backend/tests/integration/markets.integration.test.ts // Integration tests for POST /api/markets endpoint -import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest'; +import { + describe, + it, + expect, + beforeAll, + afterAll, + beforeEach, + vi, +} from 'vitest'; import request from 'supertest'; import app from '../../src/index.js'; import { MarketCategory } from '@prisma/client'; @@ -9,453 +17,465 @@ import { factoryService } from '../../src/services/blockchain/factory.js'; // Mock JWT verification vi.mock('../../src/utils/jwt.js', () => ({ - verifyAccessToken: vi.fn().mockReturnValue({ - userId: 'test-user-id', - publicKey: 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - tier: 'BEGINNER', - type: 'access', - }), + verifyAccessToken: vi.fn().mockReturnValue({ + userId: 'test-user-id', + publicKey: 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + tier: 'BEGINNER', + type: 'access', + }), })); // Mock admin middleware vi.mock('../../src/middleware/admin.middleware.js', () => ({ - requireAdmin: vi.fn((req, res, next) => { - // By default, let's say test user is admin unless overridden - if (req.user && req.user.publicKey === 'GYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY') { - return res.status(403).json({ - success: false, - error: { code: 'FORBIDDEN', message: 'Admin access required' }, - }); - } - next(); - }), + requireAdmin: vi.fn((req, res, next) => { + // By default, let's say test user is admin unless overridden + if ( + req.user && + req.user.publicKey === 'GYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY' + ) { + return res.status(403).json({ + success: false, + error: { code: 'FORBIDDEN', message: 'Admin access required' }, + }); + } + next(); + }), })); // Mock the factory service to avoid actual blockchain calls in tests vi.mock('../../src/services/blockchain/factory.js', () => ({ - factoryService: { - createMarket: vi.fn().mockResolvedValue({ - marketId: 'mock-market-id-123456', - txHash: 'mock-tx-hash-abc123', - contractAddress: 'mock-contract-address', - }), - deactivateMarket: vi.fn().mockResolvedValue({ - txHash: 'mock-tx-hash-deactivate456', - }), - getMarketCount: vi.fn().mockResolvedValue(10), - }, + factoryService: { + createMarket: vi.fn().mockResolvedValue({ + marketId: 'mock-market-id-123456', + txHash: 'mock-tx-hash-abc123', + contractAddress: 'mock-contract-address', + }), + deactivateMarket: vi.fn().mockResolvedValue({ + txHash: 'mock-tx-hash-deactivate456', + }), + getMarketCount: vi.fn().mockResolvedValue(10), + }, })); // Mock database to avoid connection errors vi.mock('../../src/database/prisma.js', () => ({ - prisma: { - market: { - create: vi.fn((args) => Promise.resolve({ - id: 'test-market-uuid', - contractAddress: args.data.contractAddress, - title: args.data.title, - description: args.data.description, - category: args.data.category, - status: 'OPEN', - outcomeA: args.data.outcomeA, - outcomeB: args.data.outcomeB, - closingAt: args.data.closingAt, - createdAt: new Date(), - txHash: 'mock-tx-hash-abc123', // From factory service mock - creator: { - id: args.data.creatorId, - username: 'testcreator', - displayName: 'Test Creator', - }, - })), - findMany: vi.fn().mockResolvedValue([]), - findUnique: vi.fn(), - findById: vi.fn(), // We might need this for the service - update: vi.fn(), - findByContractAddress: vi.fn(), - }, + prisma: { + market: { + create: vi.fn((args) => + Promise.resolve({ + id: 'test-market-uuid', + contractAddress: args.data.contractAddress, + title: args.data.title, + description: args.data.description, + category: args.data.category, + status: 'OPEN', + outcomeA: args.data.outcomeA, + outcomeB: args.data.outcomeB, + closingAt: args.data.closingAt, + createdAt: new Date(), + txHash: 'mock-tx-hash-abc123', // From factory service mock + creator: { + id: args.data.creatorId, + username: 'testcreator', + displayName: 'Test Creator', + }, + }) + ), + findMany: vi.fn().mockResolvedValue([]), + findUnique: vi.fn(), + findById: vi.fn(), // We might need this for the service + update: vi.fn(), + findByContractAddress: vi.fn(), }, + }, })); - describe('POST /api/markets - Create Market', () => { - let authToken: string; - const testUser = { - email: 'testcreator@example.com', - username: 'testcreator', - password: 'SecurePass123!', - walletAddress: 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + let authToken: string; + const testUser = { + email: 'testcreator@example.com', + username: 'testcreator', + password: 'SecurePass123!', + walletAddress: 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + }; + + beforeAll(async () => { + // Setup: Create test user and get auth token + // This assumes you have auth endpoints working + // For now, we'll mock the token + authToken = 'mock-jwt-token'; + }); + + afterAll(async () => { + // Cleanup: Remove test data + }); + + beforeEach(() => { + // Reset mocks before each test + vi.clearAllMocks(); + }); + + it('should create a market successfully with valid data', async () => { + const marketData = { + title: 'Will Bitcoin reach $100k in 2026?', + description: + 'This market predicts whether Bitcoin will reach $100,000 USD by December 31, 2026.', + category: MarketCategory.CRYPTO, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: new Date('2026-12-15T00:00:00Z').toISOString(), + resolutionTime: new Date('2026-12-31T23:59:59Z').toISOString(), }; - beforeAll(async () => { - // Setup: Create test user and get auth token - // This assumes you have auth endpoints working - // For now, we'll mock the token - authToken = 'mock-jwt-token'; - }); - - afterAll(async () => { - // Cleanup: Remove test data - }); - - beforeEach(() => { - // Reset mocks before each test - vi.clearAllMocks(); - }); + const response = await request(app) + .post('/api/markets') + .set('Authorization', `Bearer ${authToken}`) + .send(marketData) + .expect(201); + + expect(response.body.success).toBe(true); + expect(response.body.data).toHaveProperty('id'); + expect(response.body.data).toHaveProperty('contractAddress'); + expect(response.body.data).toHaveProperty('txHash'); + expect(response.body.data.title).toBe(marketData.title); + expect(response.body.data.category).toBe(marketData.category); + expect(response.body.data.status).toBe('OPEN'); + + // Verify blockchain service was called + expect(factoryService.createMarket).toHaveBeenCalledTimes(1); + expect(factoryService.createMarket).toHaveBeenCalledWith( + expect.objectContaining({ + title: marketData.title, + description: marketData.description, + category: marketData.category, + }) + ); + }); + + it('should reject market creation with invalid timestamps', async () => { + const marketData = { + title: 'Test Market with Invalid Time', + description: 'This market has invalid closing time in the past.', + category: MarketCategory.SPORTS, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: new Date('2020-01-01T00:00:00Z').toISOString(), // Past date + resolutionTime: new Date('2020-01-02T00:00:00Z').toISOString(), + }; - it('should create a market successfully with valid data', async () => { - const marketData = { - title: 'Will Bitcoin reach $100k in 2026?', - description: 'This market predicts whether Bitcoin will reach $100,000 USD by December 31, 2026.', - category: MarketCategory.CRYPTO, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: new Date('2026-12-15T00:00:00Z').toISOString(), - resolutionTime: new Date('2026-12-31T23:59:59Z').toISOString(), - }; - - const response = await request(app) - .post('/api/markets') - .set('Authorization', `Bearer ${authToken}`) - .send(marketData) - .expect(201); - - expect(response.body.success).toBe(true); - expect(response.body.data).toHaveProperty('id'); - expect(response.body.data).toHaveProperty('contractAddress'); - expect(response.body.data).toHaveProperty('txHash'); - expect(response.body.data.title).toBe(marketData.title); - expect(response.body.data.category).toBe(marketData.category); - expect(response.body.data.status).toBe('OPEN'); - - // Verify blockchain service was called - expect(factoryService.createMarket).toHaveBeenCalledTimes(1); - expect(factoryService.createMarket).toHaveBeenCalledWith( - expect.objectContaining({ - title: marketData.title, - description: marketData.description, - category: marketData.category, - }) - ); - }); + const response = await request(app) + .post('/api/markets') + .set('Authorization', `Bearer ${authToken}`) + .send(marketData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.error.code).toBe('VALIDATION_ERROR'); + expect(response.body.error.message).toContain('Validation failed'); + + // Blockchain service should not be called + expect(factoryService.createMarket).not.toHaveBeenCalled(); + }); + + it('should reject market with resolution time before closing time', async () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); + + const marketData = { + title: 'Test Market', + description: 'Market with invalid resolution time.', + category: MarketCategory.POLITICAL, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: futureDate.toISOString(), + resolutionTime: new Date(futureDate.getTime() - 1000).toISOString(), // Before closing + }; - it('should reject market creation with invalid timestamps', async () => { - const marketData = { - title: 'Test Market with Invalid Time', - description: 'This market has invalid closing time in the past.', - category: MarketCategory.SPORTS, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: new Date('2020-01-01T00:00:00Z').toISOString(), // Past date - resolutionTime: new Date('2020-01-02T00:00:00Z').toISOString(), - }; - - const response = await request(app) - .post('/api/markets') - .set('Authorization', `Bearer ${authToken}`) - .send(marketData) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.error.code).toBe('VALIDATION_ERROR'); - expect(response.body.error.message).toContain('Validation failed'); - - // Blockchain service should not be called - expect(factoryService.createMarket).not.toHaveBeenCalled(); - }); + const response = await request(app) + .post('/api/markets') + .set('Authorization', `Bearer ${authToken}`) + .send(marketData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.error.code).toBe('VALIDATION_ERROR'); + expect(factoryService.createMarket).not.toHaveBeenCalled(); + }); + + it('should reject market with title too short', async () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); + + const marketData = { + title: 'Test', // Too short (< 5 characters) + description: 'This is a valid description that is long enough.', + category: MarketCategory.ENTERTAINMENT, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: futureDate.toISOString(), + }; - it('should reject market with resolution time before closing time', async () => { - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + 30); - - const marketData = { - title: 'Test Market', - description: 'Market with invalid resolution time.', - category: MarketCategory.POLITICAL, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: futureDate.toISOString(), - resolutionTime: new Date(futureDate.getTime() - 1000).toISOString(), // Before closing - }; - - const response = await request(app) - .post('/api/markets') - .set('Authorization', `Bearer ${authToken}`) - .send(marketData) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.error.code).toBe('VALIDATION_ERROR'); - expect(factoryService.createMarket).not.toHaveBeenCalled(); - }); + const response = await request(app) + .post('/api/markets') + .set('Authorization', `Bearer ${authToken}`) + .send(marketData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.error.code).toBe('VALIDATION_ERROR'); + expect(factoryService.createMarket).not.toHaveBeenCalled(); + }); + + it('should reject market with description too short', async () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); + + const marketData = { + title: 'Valid Title Here', + description: 'Short', // Too short (< 10 characters) + category: MarketCategory.MMA, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: futureDate.toISOString(), + }; - it('should reject market with title too short', async () => { - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + 30); - - const marketData = { - title: 'Test', // Too short (< 5 characters) - description: 'This is a valid description that is long enough.', - category: MarketCategory.ENTERTAINMENT, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: futureDate.toISOString(), - }; - - const response = await request(app) - .post('/api/markets') - .set('Authorization', `Bearer ${authToken}`) - .send(marketData) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.error.code).toBe('VALIDATION_ERROR'); - expect(factoryService.createMarket).not.toHaveBeenCalled(); - }); + const response = await request(app) + .post('/api/markets') + .set('Authorization', `Bearer ${authToken}`) + .send(marketData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.error.code).toBe('VALIDATION_ERROR'); + expect(factoryService.createMarket).not.toHaveBeenCalled(); + }); + + it('should reject market creation without authentication', async () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); + + const marketData = { + title: 'Test Market', + description: 'This is a test market description.', + category: MarketCategory.CRYPTO, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: futureDate.toISOString(), + }; - it('should reject market with description too short', async () => { - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + 30); - - const marketData = { - title: 'Valid Title Here', - description: 'Short', // Too short (< 10 characters) - category: MarketCategory.MMA, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: futureDate.toISOString(), - }; - - const response = await request(app) - .post('/api/markets') - .set('Authorization', `Bearer ${authToken}`) - .send(marketData) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.error.code).toBe('VALIDATION_ERROR'); - expect(factoryService.createMarket).not.toHaveBeenCalled(); - }); + const response = await request(app) + .post('/api/markets') + .send(marketData) + .expect(401); + + expect(response.body.success).toBe(false); + expect(factoryService.createMarket).not.toHaveBeenCalled(); + }); + + it('should handle blockchain contract call failure gracefully', async () => { + // Mock contract failure + vi.mocked(factoryService.createMarket).mockRejectedValueOnce( + new Error('Failed to create market on blockchain: Network timeout') + ); + + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); + + const marketData = { + title: 'Test Market for Failure', + description: 'This market will trigger a blockchain failure.', + category: MarketCategory.BOXING, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: futureDate.toISOString(), + }; - it('should reject market creation without authentication', async () => { - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + 30); - - const marketData = { - title: 'Test Market', - description: 'This is a test market description.', - category: MarketCategory.CRYPTO, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: futureDate.toISOString(), - }; - - const response = await request(app) - .post('/api/markets') - .send(marketData) - .expect(401); - - expect(response.body.success).toBe(false); - expect(factoryService.createMarket).not.toHaveBeenCalled(); + const response = await request(app) + .post('/api/markets') + .set('Authorization', `Bearer ${authToken}`) + .send(marketData) + .expect(503); + + expect(response.body.success).toBe(false); + expect(response.body.error.code).toBe('BLOCKCHAIN_ERROR'); + expect(response.body.error.message).toContain('blockchain'); + }); + + it('should store transaction hash correctly in database', async () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 30); + + const mockTxHash = 'unique-tx-hash-xyz789'; + vi.mocked(factoryService.createMarket).mockResolvedValueOnce({ + marketId: 'test-market-id', + txHash: mockTxHash, + contractAddress: 'test-contract', }); - it('should handle blockchain contract call failure gracefully', async () => { - // Mock contract failure - vi.mocked(factoryService.createMarket).mockRejectedValueOnce( - new Error('Failed to create market on blockchain: Network timeout') - ); - - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + 30); - - const marketData = { - title: 'Test Market for Failure', - description: 'This market will trigger a blockchain failure.', - category: MarketCategory.BOXING, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: futureDate.toISOString(), - }; - - const response = await request(app) - .post('/api/markets') - .set('Authorization', `Bearer ${authToken}`) - .send(marketData) - .expect(503); - - expect(response.body.success).toBe(false); - expect(response.body.error.code).toBe('BLOCKCHAIN_ERROR'); - expect(response.body.error.message).toContain('blockchain'); - }); + const marketData = { + title: 'Transaction Hash Test Market', + description: 'Testing that transaction hash is stored correctly.', + category: MarketCategory.WRESTLING, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: futureDate.toISOString(), + }; - it('should store transaction hash correctly in database', async () => { - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + 30); - - const mockTxHash = 'unique-tx-hash-xyz789'; - vi.mocked(factoryService.createMarket).mockResolvedValueOnce({ - marketId: 'test-market-id', - txHash: mockTxHash, - contractAddress: 'test-contract', - }); - - const marketData = { - title: 'Transaction Hash Test Market', - description: 'Testing that transaction hash is stored correctly.', - category: MarketCategory.WRESTLING, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: futureDate.toISOString(), - }; - - const response = await request(app) - .post('/api/markets') - .set('Authorization', `Bearer ${authToken}`) - .send(marketData) - .expect(201); - - expect(response.body.data.txHash).toBe(mockTxHash); - }); + const response = await request(app) + .post('/api/markets') + .set('Authorization', `Bearer ${authToken}`) + .send(marketData) + .expect(201); + + expect(response.body.data.txHash).toBe(mockTxHash); + }); + + it('should use default resolution time if not provided', async () => { + const closingDate = new Date(); + closingDate.setDate(closingDate.getDate() + 30); + + const marketData = { + title: 'Market Without Resolution Time', + description: 'This market does not specify a resolution time.', + category: MarketCategory.SPORTS, + outcomeA: 'YES', + outcomeB: 'NO', + closingAt: closingDate.toISOString(), + // No resolutionTime provided + }; - it('should use default resolution time if not provided', async () => { - const closingDate = new Date(); - closingDate.setDate(closingDate.getDate() + 30); - - const marketData = { - title: 'Market Without Resolution Time', - description: 'This market does not specify a resolution time.', - category: MarketCategory.SPORTS, - outcomeA: 'YES', - outcomeB: 'NO', - closingAt: closingDate.toISOString(), - // No resolutionTime provided - }; - - const response = await request(app) - .post('/api/markets') - .set('Authorization', `Bearer ${authToken}`) - .send(marketData) - .expect(201); - - expect(response.body.success).toBe(true); - - // Verify default resolution time is 24 hours after closing - const expectedResolutionTime = new Date(closingDate.getTime() + 24 * 60 * 60 * 1000); - expect(factoryService.createMarket).toHaveBeenCalledWith( - expect.objectContaining({ - resolutionTime: expect.any(Date), - }) - ); - }); + const response = await request(app) + .post('/api/markets') + .set('Authorization', `Bearer ${authToken}`) + .send(marketData) + .expect(201); + + expect(response.body.success).toBe(true); + + // Verify default resolution time is 24 hours after closing + const expectedResolutionTime = new Date( + closingDate.getTime() + 24 * 60 * 60 * 1000 + ); + expect(factoryService.createMarket).toHaveBeenCalledWith( + expect.objectContaining({ + resolutionTime: expect.any(Date), + }) + ); + }); }); describe('GET /api/markets - List Markets', () => { - it('should list markets successfully', async () => { - const response = await request(app) - .get('/api/markets') - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data).toBeInstanceOf(Array); - expect(response.body.pagination).toBeDefined(); - }); - - it('should filter markets by category', async () => { - const response = await request(app) - .get('/api/markets?category=CRYPTO') - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data).toBeInstanceOf(Array); - }); - - it('should paginate results correctly', async () => { - const response = await request(app) - .get('/api/markets?skip=0&take=10') - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.pagination.skip).toBe(0); - expect(response.body.pagination.take).toBe(10); - }); + it('should list markets successfully', async () => { + const response = await request(app).get('/api/markets').expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeInstanceOf(Array); + expect(response.body.pagination).toBeDefined(); + }); + + it('should filter markets by category', async () => { + const response = await request(app) + .get('/api/markets?category=CRYPTO') + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeInstanceOf(Array); + }); + + it('should paginate results correctly', async () => { + const response = await request(app) + .get('/api/markets?skip=0&take=10') + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.pagination.skip).toBe(0); + expect(response.body.pagination.take).toBe(10); + }); }); describe('PATCH /api/markets/:id/deactivate - Deactivate a market', () => { - let authToken: string; - - beforeAll(() => { - authToken = 'mock-jwt-token'; - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should deactivate a market successfully when admin', async () => { - const marketId = '12345678-1234-1234-1234-123456789012'; - - const { MarketRepository } = await import('../../src/repositories/market.repository.js'); - vi.spyOn(MarketRepository.prototype, 'findById').mockResolvedValue({ - id: marketId, - contractAddress: 'mock-contract-address', - status: 'OPEN', - // other fields are not strictly necessary for this test given our service impl - } as any); - - vi.spyOn(MarketRepository.prototype, 'updateMarketStatus').mockResolvedValue({ - id: marketId, - status: 'CANCELLED', - } as any); - - const response = await request(app) - .patch(`/api/markets/${marketId}/deactivate`) - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data.status).toBe('CANCELLED'); - - expect(factoryService.deactivateMarket).toHaveBeenCalledTimes(1); - expect(factoryService.deactivateMarket).toHaveBeenCalledWith('mock-contract-address'); + let authToken: string; + + beforeAll(() => { + authToken = 'mock-jwt-token'; + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should deactivate a market successfully when admin', async () => { + const marketId = '12345678-1234-1234-1234-123456789012'; + + const { MarketRepository } = + await import('../../src/repositories/market.repository.js'); + vi.spyOn(MarketRepository.prototype, 'findById').mockResolvedValue({ + id: marketId, + contractAddress: 'mock-contract-address', + status: 'OPEN', + // other fields are not strictly necessary for this test given our service impl + } as any); + + vi.spyOn( + MarketRepository.prototype, + 'updateMarketStatus' + ).mockResolvedValue({ + id: marketId, + status: 'CANCELLED', + } as any); + + const response = await request(app) + .patch(`/api/markets/${marketId}/deactivate`) + .set('Authorization', `Bearer ${authToken}`) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.status).toBe('CANCELLED'); + + expect(factoryService.deactivateMarket).toHaveBeenCalledTimes(1); + expect(factoryService.deactivateMarket).toHaveBeenCalledWith( + 'mock-contract-address' + ); + }); + + it('should return 403 when not an admin user', async () => { + const marketId = '12345678-1234-1234-1234-123456789012'; + + // Mock verifyAccessToken to return a non-admin public key for this test + const { verifyAccessToken } = await import('../../src/utils/jwt.js'); + vi.mocked(verifyAccessToken).mockReturnValueOnce({ + userId: 'non-admin-user', + publicKey: 'GYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY', + tier: 'BEGINNER', + type: 'access', }); - it('should return 403 when not an admin user', async () => { - const marketId = '12345678-1234-1234-1234-123456789012'; - - // Mock verifyAccessToken to return a non-admin public key for this test - const { verifyAccessToken } = await import('../../src/utils/jwt.js'); - vi.mocked(verifyAccessToken).mockReturnValueOnce({ - userId: 'non-admin-user', - publicKey: 'GYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY', - tier: 'BEGINNER', - type: 'access', - }); + const response = await request(app) + .patch(`/api/markets/${marketId}/deactivate`) + .set('Authorization', `Bearer ${authToken}`) + .expect(403); - const response = await request(app) - .patch(`/api/markets/${marketId}/deactivate`) - .set('Authorization', `Bearer ${authToken}`) - .expect(403); + expect(response.body.success).toBe(false); + expect(response.body.error.code).toBe('FORBIDDEN'); - expect(response.body.success).toBe(false); - expect(response.body.error.code).toBe('FORBIDDEN'); + expect(factoryService.deactivateMarket).not.toHaveBeenCalled(); + }); - expect(factoryService.deactivateMarket).not.toHaveBeenCalled(); - }); - - it('should return 404 when market not found', async () => { - const marketId = '12345678-1234-1234-1234-123456789012'; + it('should return 404 when market not found', async () => { + const marketId = '12345678-1234-1234-1234-123456789012'; - const { MarketRepository } = await import('../../src/repositories/market.repository.js'); - vi.spyOn(MarketRepository.prototype, 'findById').mockResolvedValue(null); + const { MarketRepository } = + await import('../../src/repositories/market.repository.js'); + vi.spyOn(MarketRepository.prototype, 'findById').mockResolvedValue(null); - const response = await request(app) - .patch(`/api/markets/${marketId}/deactivate`) - .set('Authorization', `Bearer ${authToken}`) - .expect(404); + const response = await request(app) + .patch(`/api/markets/${marketId}/deactivate`) + .set('Authorization', `Bearer ${authToken}`) + .expect(404); - expect(response.body.success).toBe(false); - expect(response.body.error.code).toBe('NOT_FOUND'); + expect(response.body.success).toBe(false); + expect(response.body.error.code).toBe('NOT_FOUND'); - expect(factoryService.deactivateMarket).not.toHaveBeenCalled(); - }); + expect(factoryService.deactivateMarket).not.toHaveBeenCalled(); + }); }); diff --git a/backend/tests/integration/oracle.integration.test.ts b/backend/tests/integration/oracle.integration.test.ts index b7d9d2e6..e1e74ad9 100644 --- a/backend/tests/integration/oracle.integration.test.ts +++ b/backend/tests/integration/oracle.integration.test.ts @@ -7,61 +7,65 @@ import { marketBlockchainService } from '../../src/services/blockchain/market.js import { MarketService } from '../../src/services/market.service.js'; vi.mock('../../src/services/blockchain/oracle.js', () => ({ - oracleService: { - submitAttestation: vi.fn(), - checkConsensus: vi.fn(), - }, + oracleService: { + submitAttestation: vi.fn(), + checkConsensus: vi.fn(), + }, })); vi.mock('../../src/services/blockchain/market.js', () => ({ - marketBlockchainService: { - resolveMarket: vi.fn(), - claimWinnings: vi.fn(), - }, + marketBlockchainService: { + resolveMarket: vi.fn(), + claimWinnings: vi.fn(), + }, })); -// Mock MarketService to avoid actual DB calls if needed, -// but integration tests usually hit a test DB. +// Mock MarketService to avoid actual DB calls if needed, +// but integration tests usually hit a test DB. // However, since we need to mock auth, we'll focus on the API flow. describe('Oracle & Resolution API', () => { - const marketId = 'test-market-id'; - const authToken = 'mock-admin-token'; // In a real test, we'd get this from login + const marketId = 'test-market-id'; + const authToken = 'mock-admin-token'; // In a real test, we'd get this from login - describe('POST /api/markets/:id/attest', () => { - it('should require authentication', async () => { - const response = await request(app) - .post(`/api/markets/${marketId}/attest`) - .send({ outcome: 1 }); + describe('POST /api/markets/:id/attest', () => { + it('should require authentication', async () => { + const response = await request(app) + .post(`/api/markets/${marketId}/attest`) + .send({ outcome: 1 }); - expect(response.status).toBe(401); - }); + expect(response.status).toBe(401); + }); - it('should submit attestation when authenticated as admin', async () => { - // Mocking oracle service - (oracleService.submitAttestation as any).mockResolvedValue({ txHash: '0x123' }); + it('should submit attestation when authenticated as admin', async () => { + // Mocking oracle service + (oracleService.submitAttestation as any).mockResolvedValue({ + txHash: '0x123', + }); - // Note: In actual integration tests, you'd handle the auth middleware properly - // Here we assume requireAuth is mocked or handled via test setup + // Note: In actual integration tests, you'd handle the auth middleware properly + // Here we assume requireAuth is mocked or handled via test setup - // For this demonstration, we'll assume the mock works - // ... actual test logic would go here - }); + // For this demonstration, we'll assume the mock works + // ... actual test logic would go here }); + }); - describe('POST /api/markets/:id/resolve', () => { - it('should fail if consensus is not reached', async () => { - (oracleService.checkConsensus as any).mockResolvedValue(null); + describe('POST /api/markets/:id/resolve', () => { + it('should fail if consensus is not reached', async () => { + (oracleService.checkConsensus as any).mockResolvedValue(null); - // ... test logic - }); + // ... test logic }); + }); - describe('POST /api/markets/:id/claim', () => { - it('should call claim winnings on the blockchain', async () => { - (marketBlockchainService.claimWinnings as any).mockResolvedValue({ txHash: '0x456' }); + describe('POST /api/markets/:id/claim', () => { + it('should call claim winnings on the blockchain', async () => { + (marketBlockchainService.claimWinnings as any).mockResolvedValue({ + txHash: '0x456', + }); - // ... test logic - }); + // ... test logic }); + }); }); diff --git a/backend/tests/integration/trading.integration.test.ts b/backend/tests/integration/trading.integration.test.ts index 780a4bf9..13a9cf19 100644 --- a/backend/tests/integration/trading.integration.test.ts +++ b/backend/tests/integration/trading.integration.test.ts @@ -1,7 +1,15 @@ // backend/tests/integration/trading.integration.test.ts // Integration tests for Trading API endpoints (both direct and user-signed flow) -import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest'; +import { + describe, + it, + expect, + beforeAll, + afterAll, + beforeEach, + vi, +} from 'vitest'; import request from 'supertest'; import app from '../../src/index.js'; import { MarketStatus, TradeType, TradeStatus } from '@prisma/client'; @@ -52,14 +60,18 @@ vi.mock('../../src/database/prisma.js', () => ({ update: vi.fn(), findFirst: vi.fn(), }, - $transaction: vi.fn((callback) => callback({ - user: { - update: vi.fn().mockResolvedValue({ id: 'test-user-id', usdcBalance: 900 }), - }, - market: { - update: vi.fn().mockResolvedValue({ id: 'test-market-id' }), - }, - })), + $transaction: vi.fn((callback) => + callback({ + user: { + update: vi + .fn() + .mockResolvedValue({ id: 'test-user-id', usdcBalance: 900 }), + }, + market: { + update: vi.fn().mockResolvedValue({ id: 'test-market-id' }), + }, + }) + ), }, })); @@ -82,7 +94,9 @@ describe('Trading API - User-Signed Transaction Flow', () => { } as any); // Mock AMM response - vi.mocked(ammService.buildBuySharesTx).mockResolvedValue('AAAA-UNSIGNED-XDR'); + vi.mocked(ammService.buildBuySharesTx).mockResolvedValue( + 'AAAA-UNSIGNED-XDR' + ); const response = await request(app) .post('/api/markets/market-1/build-tx/buy') @@ -431,7 +445,10 @@ describe('Trading API - Odds & Liquidity', () => { expect(response.body.success).toBe(true); expect(response.body.data).toHaveProperty('lpTokensMinted', '500'); - expect(response.body.data).toHaveProperty('txHash', 'mock-tx-hash-add-liquidity'); + expect(response.body.data).toHaveProperty( + 'txHash', + 'mock-tx-hash-add-liquidity' + ); }); it('should remove liquidity successfully', async () => { diff --git a/backend/tests/integration/treasury.integration.test.ts b/backend/tests/integration/treasury.integration.test.ts index b0f785ca..63e162f3 100644 --- a/backend/tests/integration/treasury.integration.test.ts +++ b/backend/tests/integration/treasury.integration.test.ts @@ -4,15 +4,21 @@ import express from 'express'; import { signAccessToken } from '../../src/utils/jwt.js'; import { prisma } from '../../src/database/prisma.js'; -const ADMIN_PUBLIC_KEY = 'GADMINTEST1234567890123456789012345678901234567890123456'; -const USER_PUBLIC_KEY = 'GUSERTEST12345678901234567890123456789012345678901234567'; +const ADMIN_PUBLIC_KEY = + 'GADMINTEST1234567890123456789012345678901234567890123456'; +const USER_PUBLIC_KEY = + 'GUSERTEST12345678901234567890123456789012345678901234567'; process.env.ADMIN_WALLET_ADDRESSES = ADMIN_PUBLIC_KEY; -process.env.JWT_ACCESS_SECRET = 'test-jwt-access-secret-min-32-chars-here-for-testing'; -process.env.JWT_REFRESH_SECRET = 'test-jwt-refresh-secret-min-32-chars-here-for-testing'; - -const { treasuryService: blockchainTreasuryService } = await import('../../src/services/blockchain/treasury.js'); -const treasuryRoutesModule = await import('../../src/routes/treasury.routes.js'); +process.env.JWT_ACCESS_SECRET = + 'test-jwt-access-secret-min-32-chars-here-for-testing'; +process.env.JWT_REFRESH_SECRET = + 'test-jwt-refresh-secret-min-32-chars-here-for-testing'; + +const { treasuryService: blockchainTreasuryService } = + await import('../../src/services/blockchain/treasury.js'); +const treasuryRoutesModule = + await import('../../src/routes/treasury.routes.js'); const treasuryRoutes = treasuryRoutesModule.default; vi.mock('../../src/services/blockchain/treasury.js', async () => { @@ -25,7 +31,8 @@ vi.mock('../../src/services/blockchain/treasury.js', async () => { }; }); -const { errorHandler } = await import('../../src/middleware/error.middleware.js'); +const { errorHandler } = + await import('../../src/middleware/error.middleware.js'); const app = express(); app.use(express.json()); @@ -37,7 +44,6 @@ describe('Treasury API Integration Tests', () => { let userToken: string; beforeAll(async () => { - adminToken = signAccessToken({ userId: 'admin-user-id', publicKey: ADMIN_PUBLIC_KEY, @@ -68,7 +74,9 @@ describe('Treasury API Integration Tests', () => { platformFees: '500000', }; - vi.mocked(blockchainTreasuryService.getBalances).mockResolvedValue(mockBalances); + vi.mocked(blockchainTreasuryService.getBalances).mockResolvedValue( + mockBalances + ); const response = await request(app) .get('/api/treasury/balances') @@ -92,7 +100,10 @@ describe('Treasury API Integration Tests', () => { it('should return 403 when non-admin tries to distribute', async () => { const recipients = [ - { address: 'GUSER1TEST12345678901234567890123456789012345678901', amount: '1000' }, + { + address: 'GUSER1TEST12345678901234567890123456789012345678901', + amount: '1000', + }, ]; const response = await request(app) @@ -123,10 +134,13 @@ describe('Treasury API Integration Tests', () => { totalDistributed: '2000', }; - vi.mocked(blockchainTreasuryService.distributeCreator).mockResolvedValue(mockResult); + vi.mocked(blockchainTreasuryService.distributeCreator).mockResolvedValue( + mockResult + ); const marketId = '123e4567-e89b-12d3-a456-426614174000'; - const creatorAddress = 'GCREATORTEST12345678901234567890123456789012345678901234'; // 56 chars + const creatorAddress = + 'GCREATORTEST12345678901234567890123456789012345678901234'; // 56 chars const response = await request(app) .post('/api/treasury/distribute-creator') @@ -157,7 +171,8 @@ describe('Treasury API Integration Tests', () => { .set('Authorization', `Bearer ${userToken}`) .send({ marketId: 'market-123', - creatorAddress: 'GCREATORTEST12345678901234567890123456789012345678901234', + creatorAddress: + 'GCREATORTEST12345678901234567890123456789012345678901234', amount: '2000', }); diff --git a/backend/tests/middleware/error.middleware.test.ts b/backend/tests/middleware/error.middleware.test.ts index 0218daaf..6709ecdd 100644 --- a/backend/tests/middleware/error.middleware.test.ts +++ b/backend/tests/middleware/error.middleware.test.ts @@ -1,7 +1,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import request from 'supertest'; import express from 'express'; -import { errorHandler, notFoundHandler, ApiError } from '../../src/middleware/error.middleware'; +import { + errorHandler, + notFoundHandler, + ApiError, +} from '../../src/middleware/error.middleware'; import { logger } from '../../src/utils/logger'; describe('Error Handler Middleware', () => { @@ -15,7 +19,7 @@ describe('Error Handler Middleware', () => { describe('ApiError class', () => { it('should create an instance with correct properties', () => { const error = new ApiError(400, 'VALIDATION_ERROR', 'Validation failed', [ - { field: 'email', message: 'Invalid email' } + { field: 'email', message: 'Invalid email' }, ]); expect(error).toBeInstanceOf(Error); @@ -23,7 +27,9 @@ describe('Error Handler Middleware', () => { expect(error.statusCode).toBe(400); expect(error.code).toBe('VALIDATION_ERROR'); expect(error.message).toBe('Validation failed'); - expect(error.details).toEqual([{ field: 'email', message: 'Invalid email' }]); + expect(error.details).toEqual([ + { field: 'email', message: 'Invalid email' }, + ]); expect(error.name).toBe('ApiError'); }); @@ -40,9 +46,11 @@ describe('Error Handler Middleware', () => { describe('errorHandler', () => { it('should handle ApiError with correct response format', async () => { app.get('/test', (req, res, next) => { - next(new ApiError(400, 'TEST_ERROR', 'Test error', [ - { field: 'test', message: 'Test detail' } - ])); + next( + new ApiError(400, 'TEST_ERROR', 'Test error', [ + { field: 'test', message: 'Test detail' }, + ]) + ); }); app.use(errorHandler); @@ -54,11 +62,11 @@ describe('Error Handler Middleware', () => { error: { code: 'TEST_ERROR', message: 'Test error', - details: [{ field: 'test', message: 'Test detail' }] + details: [{ field: 'test', message: 'Test detail' }], }, meta: { - timestamp: expect.any(String) - } + timestamp: expect.any(String), + }, }); }); @@ -92,8 +100,8 @@ describe('Error Handler Middleware', () => { code: 'invalid_string', validation: 'email', message: 'Invalid email', - path: ['email'] - } + path: ['email'], + }, ]); next(error); }); @@ -126,7 +134,9 @@ describe('Error Handler Middleware', () => { expect(errorMessage).toContain('Invalid email'); } else { // Or it might be "Validation failed" - expect(['Validation failed', 'Invalid email']).toContain(errorMessage); + expect(['Validation failed', 'Invalid email']).toContain( + errorMessage + ); } } }); @@ -169,16 +179,28 @@ describe('Error Handler Middleware', () => { error: 'Test error', path: '/test', method: 'GET', - ip: expect.any(String) + ip: expect.any(String), }) ); }); it('should handle different error types by message', async () => { const testCases = [ - { message: 'Unauthorized access', expectedCode: 'UNAUTHORIZED', expectedStatus: 401 }, - { message: 'Insufficient permissions', expectedCode: 'FORBIDDEN', expectedStatus: 403 }, - { message: 'Resource not found', expectedCode: 'NOT_FOUND', expectedStatus: 404 } + { + message: 'Unauthorized access', + expectedCode: 'UNAUTHORIZED', + expectedStatus: 401, + }, + { + message: 'Insufficient permissions', + expectedCode: 'FORBIDDEN', + expectedStatus: 403, + }, + { + message: 'Resource not found', + expectedCode: 'NOT_FOUND', + expectedStatus: 404, + }, ]; for (const testCase of testCases) { @@ -193,7 +215,9 @@ describe('Error Handler Middleware', () => { }); app.use(errorHandler); - const response = await request(app).get(`/test-${testCase.expectedCode}`); + const response = await request(app).get( + `/test-${testCase.expectedCode}` + ); expect(response.status).toBe(testCase.expectedStatus); expect(response.body.error.code).toBe(testCase.expectedCode); @@ -215,7 +239,9 @@ describe('Error Handler Middleware', () => { const notFoundResponse = await request(app).get('/does-not-exist'); expect(notFoundResponse.status).toBe(404); expect(notFoundResponse.body.error.code).toBe('NOT_FOUND'); - expect(notFoundResponse.body.error.message).toBe('Cannot GET /does-not-exist'); + expect(notFoundResponse.body.error.message).toBe( + 'Cannot GET /does-not-exist' + ); }); it('should include correct HTTP method in error message', async () => { @@ -231,4 +257,4 @@ describe('Error Handler Middleware', () => { expect(response.body.error.message).toBe('Cannot PUT /test'); }); }); -}); \ No newline at end of file +}); diff --git a/backend/tests/middleware/integration.test.ts b/backend/tests/middleware/integration.test.ts index b1b5b63b..7a49915d 100644 --- a/backend/tests/middleware/integration.test.ts +++ b/backend/tests/middleware/integration.test.ts @@ -17,37 +17,41 @@ describe('Validation and Error Handling Integration', () => { }); it('should process valid request through complete middleware chain', async () => { - app.post('/api/test', - validate({ body: challengeBody }), - (req, res) => { - res.json({ - success: true, - data: { - publicKey: req.body.publicKey, - } - }); - } - ); + app.post('/api/test', validate({ body: challengeBody }), (req, res) => { + res.json({ + success: true, + data: { + publicKey: req.body.publicKey, + }, + }); + }); app.use(errorHandler); - const response = await request(app) - .post('/api/test') - .send({ - publicKey: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ' - }); + const response = await request(app).post('/api/test').send({ + publicKey: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ', + }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); - expect(response.body.data.publicKey).toBe('GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ'); + expect(response.body.data.publicKey).toBe( + 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ' + ); }); it('should handle validation error with custom business logic', async () => { - app.post('/api/market', + app.post( + '/api/market', validate({ body: createMarketBody }), (req, res, next) => { // Business logic after validation if (req.body.title.toLowerCase().includes('spam')) { - return next(new ApiError(422, 'SPAM_DETECTED', 'Market title contains spam content')); + return next( + new ApiError( + 422, + 'SPAM_DETECTED', + 'Market title contains spam content' + ) + ); } res.json({ success: true, data: req.body }); } @@ -60,32 +64,28 @@ describe('Validation and Error Handling Integration', () => { const closingAt = futureDate.toISOString(); // Use a title that passes validation but contains "spam" - const spamResponse = await request(app) - .post('/api/market') - .send({ - title: 'Is this product considered spam?', - description: 'A valid description that passes all validation checks', - category: 'CRYPTO', - outcomeA: 'Yes', - outcomeB: 'No', - closingAt, - }); + const spamResponse = await request(app).post('/api/market').send({ + title: 'Is this product considered spam?', + description: 'A valid description that passes all validation checks', + category: 'CRYPTO', + outcomeA: 'Yes', + outcomeB: 'No', + closingAt, + }); // Should be 422 (business logic rejection) not 400 (validation error) expect(spamResponse.status).toBe(422); expect(spamResponse.body.error.code).toBe('SPAM_DETECTED'); // Valid request that passes all checks - const validResponse = await request(app) - .post('/api/market') - .send({ - title: 'Legitimate market question here', - description: 'A valid description that passes all validation checks', - category: 'CRYPTO', - outcomeA: 'Yes', - outcomeB: 'No', - closingAt, - }); + const validResponse = await request(app).post('/api/market').send({ + title: 'Legitimate market question here', + description: 'A valid description that passes all validation checks', + category: 'CRYPTO', + outcomeA: 'Yes', + outcomeB: 'No', + closingAt, + }); expect(validResponse.status).toBe(200); expect(validResponse.body.success).toBe(true); diff --git a/backend/tests/middleware/validation.middleware.test.ts b/backend/tests/middleware/validation.middleware.test.ts index 8107302a..4aba3731 100644 --- a/backend/tests/middleware/validation.middleware.test.ts +++ b/backend/tests/middleware/validation.middleware.test.ts @@ -22,37 +22,31 @@ describe('Validation Middleware', () => { describe('validate() - Body Validation', () => { it('should accept valid challenge data', async () => { - app.post('/challenge', - validate({ body: challengeBody }), - (req, res) => { - res.json({ success: true, data: req.body }); - } - ); + app.post('/challenge', validate({ body: challengeBody }), (req, res) => { + res.json({ success: true, data: req.body }); + }); app.use(errorHandler); - const response = await request(app) - .post('/challenge') - .send({ - publicKey: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ' - }); + const response = await request(app).post('/challenge').send({ + publicKey: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ', + }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); - expect(response.body.data.publicKey).toBe('GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ'); + expect(response.body.data.publicKey).toBe( + 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ' + ); }); it('should reject invalid Stellar public key', async () => { - app.post('/challenge', - validate({ body: challengeBody }), - (req, res) => res.json({ success: true }) + app.post('/challenge', validate({ body: challengeBody }), (req, res) => + res.json({ success: true }) ); app.use(errorHandler); - const response = await request(app) - .post('/challenge') - .send({ - publicKey: 'invalid-key' - }); + const response = await request(app).post('/challenge').send({ + publicKey: 'invalid-key', + }); expect(response.status).toBe(400); expect(response.body.success).toBe(false); @@ -61,15 +55,12 @@ describe('Validation Middleware', () => { }); it('should reject missing required fields', async () => { - app.post('/challenge', - validate({ body: challengeBody }), - (req, res) => res.json({ success: true }) + app.post('/challenge', validate({ body: challengeBody }), (req, res) => + res.json({ success: true }) ); app.use(errorHandler); - const response = await request(app) - .post('/challenge') - .send({}); + const response = await request(app).post('/challenge').send({}); expect(response.status).toBe(400); expect(response.body.success).toBe(false); @@ -77,40 +68,32 @@ describe('Validation Middleware', () => { }); it('should accept valid login data', async () => { - app.post('/login', - validate({ body: loginBody }), - (req, res) => { - res.json({ success: true, data: req.body }); - } - ); + app.post('/login', validate({ body: loginBody }), (req, res) => { + res.json({ success: true, data: req.body }); + }); app.use(errorHandler); - const response = await request(app) - .post('/login') - .send({ - publicKey: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ', - signature: 'test-signature', - nonce: 'test-nonce' - }); + const response = await request(app).post('/login').send({ + publicKey: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ', + signature: 'test-signature', + nonce: 'test-nonce', + }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); }); it('should reject empty signature in login', async () => { - app.post('/login', - validate({ body: loginBody }), - (req, res) => res.json({ success: true }) + app.post('/login', validate({ body: loginBody }), (req, res) => + res.json({ success: true }) ); app.use(errorHandler); - const response = await request(app) - .post('/login') - .send({ - publicKey: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ', - signature: '', - nonce: 'test-nonce' - }); + const response = await request(app).post('/login').send({ + publicKey: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ', + signature: '', + nonce: 'test-nonce', + }); expect(response.status).toBe(400); expect(response.body.error.details[0].field).toBe('signature'); @@ -119,32 +102,24 @@ describe('Validation Middleware', () => { describe('validate() - Body Validation with attestBody', () => { it('should accept valid attest data', async () => { - app.post('/attest', - validate({ body: attestBody }), - (req, res) => { - res.json({ success: true, data: req.body }); - } - ); + app.post('/attest', validate({ body: attestBody }), (req, res) => { + res.json({ success: true, data: req.body }); + }); app.use(errorHandler); - const response = await request(app) - .post('/attest') - .send({ outcome: 1 }); + const response = await request(app).post('/attest').send({ outcome: 1 }); expect(response.status).toBe(200); expect(response.body.data.outcome).toBe(1); }); it('should reject invalid outcome value', async () => { - app.post('/attest', - validate({ body: attestBody }), - (req, res) => res.json({ success: true }) + app.post('/attest', validate({ body: attestBody }), (req, res) => + res.json({ success: true }) ); app.use(errorHandler); - const response = await request(app) - .post('/attest') - .send({ outcome: 5 }); + const response = await request(app).post('/attest').send({ outcome: 5 }); expect(response.status).toBe(400); expect(response.body.error.code).toBe('VALIDATION_ERROR'); @@ -153,26 +128,25 @@ describe('Validation Middleware', () => { describe('validate() - Params Validation', () => { it('should validate UUID in URL parameters', async () => { - app.get('/users/:id', - validate({ params: uuidParam }), - (req, res) => { - res.json({ success: true, data: req.params }); - } - ); + app.get('/users/:id', validate({ params: uuidParam }), (req, res) => { + res.json({ success: true, data: req.params }); + }); app.use(errorHandler); - const response = await request(app) - .get('/users/123e4567-e89b-12d3-a456-426614174000'); + const response = await request(app).get( + '/users/123e4567-e89b-12d3-a456-426614174000' + ); expect(response.status).toBe(200); expect(response.body.success).toBe(true); - expect(response.body.data.id).toBe('123e4567-e89b-12d3-a456-426614174000'); + expect(response.body.data.id).toBe( + '123e4567-e89b-12d3-a456-426614174000' + ); }); it('should reject invalid UUID', async () => { - app.get('/users/:id', - validate({ params: uuidParam }), - (req, res) => res.json({ success: true }) + app.get('/users/:id', validate({ params: uuidParam }), (req, res) => + res.json({ success: true }) ); app.use(errorHandler); @@ -186,7 +160,8 @@ describe('Validation Middleware', () => { describe('validate() - Combined Body + Params', () => { it('should validate both params and body simultaneously', async () => { - app.post('/markets/:id/attest', + app.post( + '/markets/:id/attest', validate({ params: uuidParam, body: attestBody }), (req, res) => { res.json({ success: true, params: req.params, body: req.body }); @@ -199,12 +174,15 @@ describe('Validation Middleware', () => { .send({ outcome: 0 }); expect(response.status).toBe(200); - expect(response.body.params.id).toBe('123e4567-e89b-12d3-a456-426614174000'); + expect(response.body.params.id).toBe( + '123e4567-e89b-12d3-a456-426614174000' + ); expect(response.body.body.outcome).toBe(0); }); it('should reject if params are invalid even when body is valid', async () => { - app.post('/markets/:id/attest', + app.post( + '/markets/:id/attest', validate({ params: uuidParam, body: attestBody }), (req, res) => res.json({ success: true }) ); @@ -222,7 +200,8 @@ describe('Validation Middleware', () => { describe('validate() - Stellar address', () => { it('should accept valid Stellar address in body', async () => { const stellarAddressBody = z.object({ address: stellarAddress }); - app.post('/verify', + app.post( + '/verify', validate({ body: stellarAddressBody }), (req, res) => { res.json({ success: true, data: req.body }); @@ -230,30 +209,27 @@ describe('Validation Middleware', () => { ); app.use(errorHandler); - const response = await request(app) - .post('/verify') - .send({ - address: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ' - }); + const response = await request(app).post('/verify').send({ + address: 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ', + }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); - expect(response.body.data.address).toBe('GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ'); + expect(response.body.data.address).toBe( + 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ' + ); }); it('should reject invalid Stellar address', async () => { const stellarAddressBody = z.object({ address: stellarAddress }); - app.post('/verify', - validate({ body: stellarAddressBody }), - (req, res) => res.json({ success: true }) + app.post('/verify', validate({ body: stellarAddressBody }), (req, res) => + res.json({ success: true }) ); app.use(errorHandler); - const response = await request(app) - .post('/verify') - .send({ - address: 'not-a-stellar-address' - }); + const response = await request(app).post('/verify').send({ + address: 'not-a-stellar-address', + }); expect(response.status).toBe(400); expect(response.body.error.code).toBe('VALIDATION_ERROR'); diff --git a/backend/tests/middleware/validation.schemas.test.ts b/backend/tests/middleware/validation.schemas.test.ts index d785c57b..88a5ad73 100644 --- a/backend/tests/middleware/validation.schemas.test.ts +++ b/backend/tests/middleware/validation.schemas.test.ts @@ -76,9 +76,7 @@ describe('sanitizedString', () => { }); it('should strip XSS payloads', () => { - const result = schema.parse( - 'helloworld' - ); + const result = schema.parse('helloworld'); expect(result).toBe('helloworld'); }); @@ -118,9 +116,7 @@ describe('Auth schemas', () => { }); it('should reject invalid Stellar key - wrong length', () => { - expect(() => - challengeBody.parse({ publicKey: 'GABCDEF' }) - ).toThrow(); + expect(() => challengeBody.parse({ publicKey: 'GABCDEF' })).toThrow(); }); it('should reject lowercase Stellar key', () => { @@ -318,9 +314,7 @@ describe('Market schemas', () => { it('should accept missing resolutionTime (optional)', () => { const { resolutionTime, ...withoutResolution } = validMarket; - expect(() => - createMarketBody.parse(withoutResolution) - ).not.toThrow(); + expect(() => createMarketBody.parse(withoutResolution)).not.toThrow(); }); it('should strip XSS from title', () => { @@ -334,8 +328,7 @@ describe('Market schemas', () => { it('should strip HTML from description', () => { const result = createMarketBody.parse({ ...validMarket, - description: - 'This market resolves YES if boxer A wins.', + description: 'This market resolves YES if boxer A wins.', }); expect(result.description).toBe( 'This market resolves YES if boxer A wins.' @@ -361,15 +354,11 @@ describe('Market schemas', () => { }); it('should reject zero liquidity', () => { - expect(() => - createPoolBody.parse({ initialLiquidity: '0' }) - ).toThrow(); + expect(() => createPoolBody.parse({ initialLiquidity: '0' })).toThrow(); }); it('should reject non-numeric string', () => { - expect(() => - createPoolBody.parse({ initialLiquidity: 'abc' }) - ).toThrow(); + expect(() => createPoolBody.parse({ initialLiquidity: 'abc' })).toThrow(); }); it('should reject decimal values', () => { @@ -562,9 +551,7 @@ describe('Treasury schemas', () => { it('should accept valid distribution with one recipient', () => { expect(() => distributeLeaderboardBody.parse({ - recipients: [ - { address: VALID_STELLAR_KEY, amount: '1000' }, - ], + recipients: [{ address: VALID_STELLAR_KEY, amount: '1000' }], }) ).not.toThrow(); }); @@ -591,9 +578,7 @@ describe('Treasury schemas', () => { address: VALID_STELLAR_KEY, amount: '100', })); - expect(() => - distributeLeaderboardBody.parse({ recipients }) - ).toThrow(); + expect(() => distributeLeaderboardBody.parse({ recipients })).toThrow(); }); it('should accept exactly 100 recipients', () => { @@ -609,9 +594,7 @@ describe('Treasury schemas', () => { it('should reject invalid Stellar address in recipients', () => { expect(() => distributeLeaderboardBody.parse({ - recipients: [ - { address: 'invalid-address', amount: '1000' }, - ], + recipients: [{ address: 'invalid-address', amount: '1000' }], }) ).toThrow(); }); @@ -619,9 +602,7 @@ describe('Treasury schemas', () => { it('should reject zero amount', () => { expect(() => distributeLeaderboardBody.parse({ - recipients: [ - { address: VALID_STELLAR_KEY, amount: '0' }, - ], + recipients: [{ address: VALID_STELLAR_KEY, amount: '0' }], }) ).toThrow(); }); @@ -629,9 +610,7 @@ describe('Treasury schemas', () => { it('should reject non-numeric amount', () => { expect(() => distributeLeaderboardBody.parse({ - recipients: [ - { address: VALID_STELLAR_KEY, amount: 'abc' }, - ], + recipients: [{ address: VALID_STELLAR_KEY, amount: 'abc' }], }) ).toThrow(); }); @@ -723,9 +702,7 @@ describe('Shared primitives', () => { describe('uuidParam', () => { it('should accept valid UUID', () => { - expect(() => - uuidParam.parse({ id: VALID_UUID }) - ).not.toThrow(); + expect(() => uuidParam.parse({ id: VALID_UUID })).not.toThrow(); }); it('should reject invalid UUID', () => { @@ -739,15 +716,11 @@ describe('Shared primitives', () => { describe('marketIdParam', () => { it('should accept valid UUID', () => { - expect(() => - marketIdParam.parse({ marketId: VALID_UUID }) - ).not.toThrow(); + expect(() => marketIdParam.parse({ marketId: VALID_UUID })).not.toThrow(); }); it('should reject invalid UUID', () => { - expect(() => - marketIdParam.parse({ marketId: 'not-valid' }) - ).toThrow(); + expect(() => marketIdParam.parse({ marketId: 'not-valid' })).toThrow(); }); }); }); diff --git a/backend/tests/repositories/market.repository.integration.test.ts b/backend/tests/repositories/market.repository.integration.test.ts index a6138a30..e719d92f 100644 --- a/backend/tests/repositories/market.repository.integration.test.ts +++ b/backend/tests/repositories/market.repository.integration.test.ts @@ -74,9 +74,15 @@ describe('MarketRepository Integration Tests', () => { }); // Verify wrestling market is in results and boxing market is not - expect(wrestlingMarkets.some(m => m.id === wrestlingMarket.id)).toBe(true); - expect(wrestlingMarkets.some(m => m.id === boxingMarket.id)).toBe(false); - expect(wrestlingMarkets.every(m => m.category === MarketCategory.WRESTLING)).toBe(true); + expect(wrestlingMarkets.some((m) => m.id === wrestlingMarket.id)).toBe( + true + ); + expect(wrestlingMarkets.some((m) => m.id === boxingMarket.id)).toBe( + false + ); + expect( + wrestlingMarkets.every((m) => m.category === MarketCategory.WRESTLING) + ).toBe(true); }); }); @@ -185,7 +191,7 @@ describe('MarketRepository Integration Tests', () => { closingAt: new Date(Date.now() + 86400000), }); - // Removed failing test: should return markets sorted by volume + // Removed failing test: should return markets sorted by volume }); }); }); diff --git a/backend/tests/repositories/user.repository.integration.test.ts b/backend/tests/repositories/user.repository.integration.test.ts index bda24f0b..83cc5eab 100644 --- a/backend/tests/repositories/user.repository.integration.test.ts +++ b/backend/tests/repositories/user.repository.integration.test.ts @@ -42,7 +42,9 @@ describe('UserRepository Integration Tests', () => { }); it('should return null for non-existent email', async () => { - const found = await userRepo.findByEmail(`nonexistent-${Date.now()}@example.com`); + const found = await userRepo.findByEmail( + `nonexistent-${Date.now()}@example.com` + ); expect(found).toBeNull(); }); }); @@ -102,7 +104,10 @@ describe('UserRepository Integration Tests', () => { }); const walletAddress = `GTEST${timestamp}ABCDEFGHIJKLMNOPQRSTUVWXYZ`; - const updated = await userRepo.updateWalletAddress(user.id, walletAddress); + const updated = await userRepo.updateWalletAddress( + user.id, + walletAddress + ); expect(updated.walletAddress).toBe(walletAddress); }); diff --git a/backend/tests/services/auth.service.test.ts b/backend/tests/services/auth.service.test.ts index bab7c726..5735027f 100644 --- a/backend/tests/services/auth.service.test.ts +++ b/backend/tests/services/auth.service.test.ts @@ -1,8 +1,16 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Keypair } from '@stellar/stellar-sdk'; import { StellarService } from '../../src/services/stellar.service.js'; -import { signAccessToken, verifyAccessToken, signRefreshToken, verifyRefreshToken } from '../../src/utils/jwt.js'; -import { generateNonce, buildSignatureMessage } from '../../src/utils/crypto.js'; +import { + signAccessToken, + verifyAccessToken, + signRefreshToken, + verifyRefreshToken, +} from '../../src/utils/jwt.js'; +import { + generateNonce, + buildSignatureMessage, +} from '../../src/utils/crypto.js'; import { AuthError } from '../../src/types/auth.types.js'; describe('StellarService', () => { @@ -101,7 +109,10 @@ describe('JWT Utils', () => { }); it('should reject refresh token as access token', () => { - const refreshToken = signRefreshToken({ userId: 'user-123', tokenId: 'token-123' }); + const refreshToken = signRefreshToken({ + userId: 'user-123', + tokenId: 'token-123', + }); expect(() => verifyAccessToken(refreshToken)).toThrow(AuthError); }); }); diff --git a/backend/tests/services/blockchain/base.test.ts b/backend/tests/services/blockchain/base.test.ts index d6fe663d..27024e95 100644 --- a/backend/tests/services/blockchain/base.test.ts +++ b/backend/tests/services/blockchain/base.test.ts @@ -35,7 +35,13 @@ class TestBlockchainService extends BaseBlockchainService { super('TestService'); } - public async publicWaitForTransaction(txHash: string, fn: string, params: any, netRetries?: number, pollRetries?: number) { + public async publicWaitForTransaction( + txHash: string, + fn: string, + params: any, + netRetries?: number, + pollRetries?: number + ) { return this.waitForTransaction(txHash, fn, params, netRetries, pollRetries); } } @@ -77,11 +83,12 @@ describe('BaseBlockchainService', () => { mockGetTransaction.mockRejectedValue(new Error('Network error')); // Max network retries = 2 for faster test - await expect(service.publicWaitForTransaction('hash', 'testFn', {}, 2, 10)) - .rejects.toThrow('Max network retries reached'); + await expect( + service.publicWaitForTransaction('hash', 'testFn', {}, 2, 10) + ).rejects.toThrow('Max network retries reached'); expect(mockGetTransaction).toHaveBeenCalledTimes(2); - + // Check DLQ call const { prisma } = await import('../../../src/database/prisma.js'); expect(prisma.blockchainDeadLetterQueue.upsert).toHaveBeenCalled(); @@ -90,8 +97,9 @@ describe('BaseBlockchainService', () => { it('should stop and fail immediately to DLQ if transaction status is FAILED', async () => { mockGetTransaction.mockResolvedValueOnce({ status: 'FAILED' }); - await expect(service.publicWaitForTransaction('hash', 'testFn', {})) - .rejects.toThrow('Transaction failed on blockchain'); + await expect( + service.publicWaitForTransaction('hash', 'testFn', {}) + ).rejects.toThrow('Transaction failed on blockchain'); expect(mockGetTransaction).toHaveBeenCalledTimes(1); const { prisma } = await import('../../../src/database/prisma.js'); diff --git a/backend/tests/services/cron.service.test.ts b/backend/tests/services/cron.service.test.ts index ca039c14..e97b2293 100644 --- a/backend/tests/services/cron.service.test.ts +++ b/backend/tests/services/cron.service.test.ts @@ -39,7 +39,9 @@ describe('CronService.pollOracleConsensus()', () => { }); it('should do nothing when no CLOSED markets exist', async () => { - mockMarketRepository.getClosedMarketsAwaitingResolution.mockResolvedValue([]); + mockMarketRepository.getClosedMarketsAwaitingResolution.mockResolvedValue( + [] + ); await cronService.pollOracleConsensus(); @@ -105,18 +107,28 @@ describe('CronService.pollOracleConsensus()', () => { closedMarket('market-3'), ]); vi.mocked(oracleService.checkConsensus) - .mockResolvedValueOnce(null) // market-1: no consensus - .mockResolvedValueOnce(1) // market-2: consensus → outcome 1 - .mockResolvedValueOnce(0); // market-3: consensus → outcome 0 + .mockResolvedValueOnce(null) // market-1: no consensus + .mockResolvedValueOnce(1) // market-2: consensus → outcome 1 + .mockResolvedValueOnce(0); // market-3: consensus → outcome 0 - mockMarketService.resolveMarket.mockResolvedValue({ resolvedAt: new Date() }); + mockMarketService.resolveMarket.mockResolvedValue({ + resolvedAt: new Date(), + }); await cronService.pollOracleConsensus(); expect(oracleService.checkConsensus).toHaveBeenCalledTimes(3); expect(mockMarketService.resolveMarket).toHaveBeenCalledTimes(2); - expect(mockMarketService.resolveMarket).toHaveBeenCalledWith('market-2', 1, 'oracle-consensus'); - expect(mockMarketService.resolveMarket).toHaveBeenCalledWith('market-3', 0, 'oracle-consensus'); + expect(mockMarketService.resolveMarket).toHaveBeenCalledWith( + 'market-2', + 1, + 'oracle-consensus' + ); + expect(mockMarketService.resolveMarket).toHaveBeenCalledWith( + 'market-3', + 0, + 'oracle-consensus' + ); }); it('should continue processing remaining markets when one fails', async () => { @@ -128,14 +140,20 @@ describe('CronService.pollOracleConsensus()', () => { .mockRejectedValueOnce(new Error('RPC timeout')) .mockResolvedValueOnce(1); - mockMarketService.resolveMarket.mockResolvedValue({ resolvedAt: new Date() }); + mockMarketService.resolveMarket.mockResolvedValue({ + resolvedAt: new Date(), + }); await cronService.pollOracleConsensus(); // Should not throw; should still resolve the second market expect(oracleService.checkConsensus).toHaveBeenCalledTimes(2); expect(mockMarketService.resolveMarket).toHaveBeenCalledTimes(1); - expect(mockMarketService.resolveMarket).toHaveBeenCalledWith('market-good', 1, 'oracle-consensus'); + expect(mockMarketService.resolveMarket).toHaveBeenCalledWith( + 'market-good', + 1, + 'oracle-consensus' + ); }); it('should return early and not call oracle if fetching markets fails', async () => { diff --git a/backend/tests/services/market.service.unit.test.ts b/backend/tests/services/market.service.unit.test.ts index c4909bdf..a2b96c37 100644 --- a/backend/tests/services/market.service.unit.test.ts +++ b/backend/tests/services/market.service.unit.test.ts @@ -3,113 +3,143 @@ import { MarketService } from '../../src/services/market.service.js'; import { MarketStatus } from '@prisma/client'; describe('MarketService.resolveMarket Unit Tests', () => { - let marketService: MarketService; - let mockMarketRepository: any; - let mockPredictionRepository: any; - let mockUserService: any; - let mockLeaderboardService: any; - - beforeEach(() => { - mockMarketRepository = { - findById: vi.fn(), - updateMarketStatus: vi.fn(), - }; - mockPredictionRepository = { - findMarketPredictions: vi.fn().mockResolvedValue([]), - }; - mockUserService = { - calculateAndUpdateTier: vi.fn(), - }; - mockLeaderboardService = { - calculateRanks: vi.fn(), - handleSettlement: vi.fn(), - }; - - marketService = new MarketService( - mockMarketRepository, - mockPredictionRepository, - mockUserService, - mockLeaderboardService - ); - - // Spy on settlePredictions to avoid database/transaction issues - vi.spyOn(marketService as any, 'settlePredictions').mockResolvedValue(undefined); - - // Mock logger to avoid cluttering test output - vi.mock('../../src/utils/logger.js', () => ({ - logger: { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - }, - })); + let marketService: MarketService; + let mockMarketRepository: any; + let mockPredictionRepository: any; + let mockUserService: any; + let mockLeaderboardService: any; + + beforeEach(() => { + mockMarketRepository = { + findById: vi.fn(), + updateMarketStatus: vi.fn(), + }; + mockPredictionRepository = { + findMarketPredictions: vi.fn().mockResolvedValue([]), + }; + mockUserService = { + calculateAndUpdateTier: vi.fn(), + }; + mockLeaderboardService = { + calculateRanks: vi.fn(), + handleSettlement: vi.fn(), + }; + + marketService = new MarketService( + mockMarketRepository, + mockPredictionRepository, + mockUserService, + mockLeaderboardService + ); + + // Spy on settlePredictions to avoid database/transaction issues + vi.spyOn(marketService as any, 'settlePredictions').mockResolvedValue( + undefined + ); + + // Mock logger to avoid cluttering test output + vi.mock('../../src/utils/logger.js', () => ({ + logger: { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + }, + })); + }); + + it('should throw error if market not found', async () => { + mockMarketRepository.findById.mockResolvedValue(null); + await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow( + 'Market not found' + ); + }); + + it('should throw error if winning outcome is invalid', async () => { + mockMarketRepository.findById.mockResolvedValue({ + status: MarketStatus.CLOSED, }); - - it('should throw error if market not found', async () => { - mockMarketRepository.findById.mockResolvedValue(null); - await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow('Market not found'); + await expect(marketService.resolveMarket('1', 2, 'source')).rejects.toThrow( + 'Winning outcome must be 0 or 1' + ); + }); + + it('should throw error if market is already RESOLVED', async () => { + mockMarketRepository.findById.mockResolvedValue({ + status: MarketStatus.RESOLVED, }); - - it('should throw error if winning outcome is invalid', async () => { - mockMarketRepository.findById.mockResolvedValue({ status: MarketStatus.CLOSED }); - await expect(marketService.resolveMarket('1', 2, 'source')).rejects.toThrow('Winning outcome must be 0 or 1'); + await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow( + 'Market cannot be resolved in RESOLVED status' + ); + }); + + it('should throw error if market is CANCELLED', async () => { + mockMarketRepository.findById.mockResolvedValue({ + status: MarketStatus.CANCELLED, }); - - it('should throw error if market is already RESOLVED', async () => { - mockMarketRepository.findById.mockResolvedValue({ status: MarketStatus.RESOLVED }); - await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow('Market cannot be resolved in RESOLVED status'); + await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow( + 'Market cannot be resolved in CANCELLED status' + ); + }); + + it('should throw error if market is DISPUTED', async () => { + mockMarketRepository.findById.mockResolvedValue({ + status: MarketStatus.DISPUTED, }); - - it('should throw error if market is CANCELLED', async () => { - mockMarketRepository.findById.mockResolvedValue({ status: MarketStatus.CANCELLED }); - await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow('Market cannot be resolved in CANCELLED status'); + await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow( + 'Market cannot be resolved in DISPUTED status' + ); + }); + + it('should throw error if market is OPEN but closingAt has not passed', async () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + mockMarketRepository.findById.mockResolvedValue({ + status: MarketStatus.OPEN, + closingAt: futureDate, }); - - it('should throw error if market is DISPUTED', async () => { - mockMarketRepository.findById.mockResolvedValue({ status: MarketStatus.DISPUTED }); - await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow('Market cannot be resolved in DISPUTED status'); + await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow( + 'Market is still open and has not reached closing time' + ); + }); + + it('should resolve market if status is CLOSED', async () => { + mockMarketRepository.findById.mockResolvedValue({ + status: MarketStatus.CLOSED, }); - - it('should throw error if market is OPEN but closingAt has not passed', async () => { - const futureDate = new Date(); - futureDate.setFullYear(futureDate.getFullYear() + 1); - mockMarketRepository.findById.mockResolvedValue({ - status: MarketStatus.OPEN, - closingAt: futureDate - }); - await expect(marketService.resolveMarket('1', 1, 'source')).rejects.toThrow('Market is still open and has not reached closing time'); + mockMarketRepository.updateMarketStatus.mockResolvedValue({ + id: '1', + status: MarketStatus.RESOLVED, }); - it('should resolve market if status is CLOSED', async () => { - mockMarketRepository.findById.mockResolvedValue({ status: MarketStatus.CLOSED }); - mockMarketRepository.updateMarketStatus.mockResolvedValue({ id: '1', status: MarketStatus.RESOLVED }); - - const result = await marketService.resolveMarket('1', 1, 'source'); - - expect(mockMarketRepository.updateMarketStatus).toHaveBeenCalledWith( - '1', - MarketStatus.RESOLVED, - expect.objectContaining({ winningOutcome: 1, resolutionSource: 'source' }) - ); - expect(result.status).toBe(MarketStatus.RESOLVED); + const result = await marketService.resolveMarket('1', 1, 'source'); + + expect(mockMarketRepository.updateMarketStatus).toHaveBeenCalledWith( + '1', + MarketStatus.RESOLVED, + expect.objectContaining({ winningOutcome: 1, resolutionSource: 'source' }) + ); + expect(result.status).toBe(MarketStatus.RESOLVED); + }); + + it('should resolve market if status is OPEN but closingAt has passed', async () => { + const pastDate = new Date(); + pastDate.setFullYear(pastDate.getFullYear() - 1); + mockMarketRepository.findById.mockResolvedValue({ + status: MarketStatus.OPEN, + closingAt: pastDate, }); - - it('should resolve market if status is OPEN but closingAt has passed', async () => { - const pastDate = new Date(); - pastDate.setFullYear(pastDate.getFullYear() - 1); - mockMarketRepository.findById.mockResolvedValue({ - status: MarketStatus.OPEN, - closingAt: pastDate - }); - mockMarketRepository.updateMarketStatus.mockResolvedValue({ id: '1', status: MarketStatus.RESOLVED }); - - const result = await marketService.resolveMarket('1', 1, 'source'); - - expect(mockMarketRepository.updateMarketStatus).toHaveBeenCalledWith( - '1', - MarketStatus.RESOLVED, - expect.objectContaining({ winningOutcome: 1, resolutionSource: 'source' }) - ); - expect(result.status).toBe(MarketStatus.RESOLVED); + mockMarketRepository.updateMarketStatus.mockResolvedValue({ + id: '1', + status: MarketStatus.RESOLVED, }); + + const result = await marketService.resolveMarket('1', 1, 'source'); + + expect(mockMarketRepository.updateMarketStatus).toHaveBeenCalledWith( + '1', + MarketStatus.RESOLVED, + expect.objectContaining({ winningOutcome: 1, resolutionSource: 'source' }) + ); + expect(result.status).toBe(MarketStatus.RESOLVED); + }); }); diff --git a/backend/tests/services/prediction.service.test.ts b/backend/tests/services/prediction.service.test.ts new file mode 100644 index 00000000..b9440044 --- /dev/null +++ b/backend/tests/services/prediction.service.test.ts @@ -0,0 +1,540 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { PredictionService } from '../../src/services/prediction.service.js'; +import { MarketStatus, PredictionStatus } from '@prisma/client'; +import * as cryptoUtils from '../../src/utils/crypto.js'; +import { PredictionRepository } from '../../src/repositories/prediction.repository.js'; +import { MarketRepository } from '../../src/repositories/market.repository.js'; +import { UserRepository } from '../../src/repositories/user.repository.js'; + +// Define hoisted mocks +const { + mockPredictionRepo, + mockMarketRepo, + mockUserRepo, + mockBlockchainService, +} = vi.hoisted(() => ({ + mockPredictionRepo: { + findById: vi.fn(), + findByUserAndMarket: vi.fn(), + createPrediction: vi.fn(), + revealPrediction: vi.fn(), + claimWinnings: vi.fn(), + findUserPredictions: vi.fn(), + findMarketPredictions: vi.fn(), + getUnclaimedWinnings: vi.fn(), + getUserPredictionStats: vi.fn(), + getMarketPredictionStats: vi.fn(), + }, + mockMarketRepo: { + findById: vi.fn(), + updateMarketVolume: vi.fn(), + }, + mockUserRepo: { + findById: vi.fn(), + updateBalance: vi.fn(), + }, + mockBlockchainService: { + commitPrediction: vi.fn(), + revealPrediction: vi.fn(), + claimWinnings: vi.fn(), + }, +})); + +// Mock the repositories to return the shared instances +vi.mock('../../src/repositories/prediction.repository.js', () => ({ + PredictionRepository: vi.fn().mockImplementation(() => mockPredictionRepo), +})); + +vi.mock('../../src/repositories/market.repository.js', () => ({ + MarketRepository: vi.fn().mockImplementation(() => mockMarketRepo), +})); + +vi.mock('../../src/repositories/user.repository.js', () => ({ + UserRepository: vi.fn().mockImplementation(() => mockUserRepo), +})); + +vi.mock('../../src/services/blockchain/market.js', () => ({ + marketBlockchainService: mockBlockchainService, + MarketBlockchainService: vi + .fn() + .mockImplementation(() => mockBlockchainService), +})); + +// Mock the transaction helper +vi.mock('../../src/database/transaction.js', () => ({ + executeTransaction: vi.fn((callback) => callback({})), +})); + +// Mock the crypto utils +vi.mock('../../src/utils/crypto.js', async () => { + const actual = (await vi.importActual('../../src/utils/crypto.js')) as any; + return { + ...actual, + generateSalt: vi.fn(() => 'mock-salt'), + createCommitmentHash: vi.fn(() => 'mock-hash'), + encrypt: vi.fn(() => ({ encrypted: 'mock-encrypted', iv: 'mock-iv' })), + decrypt: vi.fn(() => 'mock-salt'), + }; +}); + +describe('PredictionService', () => { + let predictionService: PredictionService; + + beforeEach(() => { + vi.clearAllMocks(); + predictionService = new PredictionService(); + + // Default blockchain mocks + mockBlockchainService.commitPrediction.mockResolvedValue({ + txHash: 'mock-commit-tx', + }); + mockBlockchainService.revealPrediction.mockResolvedValue({ + txHash: 'mock-reveal-tx', + }); + }); + + describe('commitPrediction', () => { + const userId = 'user-1'; + const marketId = 'market-1'; + const predictedOutcome = 1; + const amountUsdc = 100; + + it('should successfully commit a prediction', async () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + + mockMarketRepo.findById.mockResolvedValue({ + id: marketId, + contractAddress: 'stellar-market-addr', + status: MarketStatus.OPEN, + closingAt: futureDate, + }); + mockPredictionRepo.findByUserAndMarket.mockResolvedValue(null); + mockUserRepo.findById.mockResolvedValue({ + id: userId, + usdcBalance: 500, + }); + + mockPredictionRepo.createPrediction.mockResolvedValue({ id: 'pred-1' }); + + const result = await predictionService.commitPrediction( + userId, + marketId, + predictedOutcome, + amountUsdc + ); + + expect(result).toBeDefined(); + expect(mockBlockchainService.commitPrediction).toHaveBeenCalledWith( + 'stellar-market-addr', + 'mock-hash', + amountUsdc + ); + expect(mockPredictionRepo.createPrediction).toHaveBeenCalledWith( + expect.objectContaining({ + userId, + marketId, + commitmentHash: 'mock-hash', + status: PredictionStatus.COMMITTED, + }) + ); + expect(mockUserRepo.updateBalance).toHaveBeenCalledWith(userId, 400); + expect(mockMarketRepo.updateMarketVolume).toHaveBeenCalledWith( + marketId, + amountUsdc, + true + ); + }); + + it('should throw error if market not found', async () => { + mockMarketRepo.findById.mockResolvedValue(null); + + await expect( + predictionService.commitPrediction( + userId, + marketId, + predictedOutcome, + amountUsdc + ) + ).rejects.toThrow('Market not found'); + }); + + it('should throw error if market is not open', async () => { + mockMarketRepo.findById.mockResolvedValue({ + status: MarketStatus.CLOSED, + }); + + await expect( + predictionService.commitPrediction( + userId, + marketId, + predictedOutcome, + amountUsdc + ) + ).rejects.toThrow('Market is not open for predictions'); + }); + + it('should throw error if market has already closed', async () => { + const pastDate = new Date(); + pastDate.setFullYear(pastDate.getFullYear() - 1); + + mockMarketRepo.findById.mockResolvedValue({ + status: MarketStatus.OPEN, + closingAt: pastDate, + }); + + await expect( + predictionService.commitPrediction( + userId, + marketId, + predictedOutcome, + amountUsdc + ) + ).rejects.toThrow('Market has closed'); + }); + + it('should throw error if user already has a prediction', async () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + + mockMarketRepo.findById.mockResolvedValue({ + status: MarketStatus.OPEN, + closingAt: futureDate, + }); + mockPredictionRepo.findByUserAndMarket.mockResolvedValue({ + id: 'existing-pred', + }); + + await expect( + predictionService.commitPrediction( + userId, + marketId, + predictedOutcome, + amountUsdc + ) + ).rejects.toThrow('User already has a prediction for this market'); + }); + + it('should throw error if amount is zero or negative', async () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + + mockMarketRepo.findById.mockResolvedValue({ + status: MarketStatus.OPEN, + closingAt: futureDate, + }); + mockPredictionRepo.findByUserAndMarket.mockResolvedValue(null); + + await expect( + predictionService.commitPrediction( + userId, + marketId, + predictedOutcome, + 0 + ) + ).rejects.toThrow('Amount must be greater than 0'); + }); + + it('should throw error if predicted outcome is invalid', async () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + + mockMarketRepo.findById.mockResolvedValue({ + status: MarketStatus.OPEN, + closingAt: futureDate, + }); + mockPredictionRepo.findByUserAndMarket.mockResolvedValue(null); + + await expect( + predictionService.commitPrediction(userId, marketId, 2, amountUsdc) + ).rejects.toThrow('Predicted outcome must be 0 (NO) or 1 (YES)'); + }); + + it('should throw error if user balance is insufficient', async () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + + mockMarketRepo.findById.mockResolvedValue({ + status: MarketStatus.OPEN, + closingAt: futureDate, + }); + mockPredictionRepo.findByUserAndMarket.mockResolvedValue(null); + mockUserRepo.findById.mockResolvedValue({ + id: userId, + usdcBalance: 50, + }); + + await expect( + predictionService.commitPrediction( + userId, + marketId, + predictedOutcome, + amountUsdc + ) + ).rejects.toThrow('Insufficient balance'); + }); + }); + + describe('revealPrediction', () => { + const userId = 'user-1'; + const predictionId = 'pred-1'; + const marketId = 'market-1'; + + it('should successfully reveal a prediction', async () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + + mockPredictionRepo.findById.mockResolvedValue({ + id: predictionId, + userId, + marketId, + status: PredictionStatus.COMMITTED, + encryptedSalt: 'enc-salt', + saltIv: 'iv', + commitmentHash: 'mock-hash', + }); + + mockMarketRepo.findById.mockResolvedValue({ + id: marketId, + contractAddress: 'stellar-market-addr', + closingAt: futureDate, + }); + + // Special mock for createCommitmentHash to simulate correct outcome search + vi.mocked(cryptoUtils.createCommitmentHash).mockImplementation( + (uid, mid, outcome, salt) => { + if (outcome === 1) return 'mock-hash'; + return 'wrong-hash'; + } + ); + + mockPredictionRepo.revealPrediction.mockResolvedValue({ + id: predictionId, + status: PredictionStatus.REVEALED, + }); + + const result = await predictionService.revealPrediction( + userId, + predictionId, + marketId + ); + + expect(result).toBeDefined(); + expect(mockBlockchainService.revealPrediction).toHaveBeenCalledWith( + 'stellar-market-addr', + 1, + 'mock-salt' + ); + expect(mockPredictionRepo.revealPrediction).toHaveBeenCalledWith( + predictionId, + 1, + 'mock-reveal-tx' + ); + }); + + it('should throw error if prediction not found', async () => { + mockPredictionRepo.findById.mockResolvedValue(null); + + await expect( + predictionService.revealPrediction(userId, predictionId, marketId) + ).rejects.toThrow('Prediction not found'); + }); + + it('should throw error if unauthorized', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + userId: 'wrong-user', + }); + + await expect( + predictionService.revealPrediction(userId, predictionId, marketId) + ).rejects.toThrow('Unauthorized'); + }); + + it('should throw error if market ID mismatch', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + userId, + marketId: 'wrong-market', + }); + + await expect( + predictionService.revealPrediction(userId, predictionId, marketId) + ).rejects.toThrow('Market ID mismatch'); + }); + + it('should throw error if prediction status is not COMMITTED', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + userId, + marketId, + status: PredictionStatus.REVEALED, + }); + + await expect( + predictionService.revealPrediction(userId, predictionId, marketId) + ).rejects.toThrow('Prediction already revealed or invalid status'); + }); + + it('should throw error if salt or iv missing', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + userId, + marketId, + status: PredictionStatus.COMMITTED, + encryptedSalt: null, + saltIv: null, + }); + + await expect( + predictionService.revealPrediction(userId, predictionId, marketId) + ).rejects.toThrow('Salt not found - cannot reveal prediction'); + }); + + it('should throw error if reveal period has ended', async () => { + const pastDate = new Date(); + pastDate.setFullYear(pastDate.getFullYear() - 1); + + mockPredictionRepo.findById.mockResolvedValue({ + userId, + marketId, + status: PredictionStatus.COMMITTED, + encryptedSalt: 'enc-salt', + saltIv: 'iv', + }); + + mockMarketRepo.findById.mockResolvedValue({ + closingAt: pastDate, + }); + + await expect( + predictionService.revealPrediction(userId, predictionId, marketId) + ).rejects.toThrow('Reveal period has ended'); + }); + + it('should throw error if commitment hash verification fails', async () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + + mockPredictionRepo.findById.mockResolvedValue({ + userId, + marketId, + status: PredictionStatus.COMMITTED, + encryptedSalt: 'enc-salt', + saltIv: 'iv', + commitmentHash: 'original-hash', + }); + + mockMarketRepo.findById.mockResolvedValue({ + closingAt: futureDate, + }); + + // Mock createCommitmentHash to never return the original hash + vi.mocked(cryptoUtils.createCommitmentHash).mockReturnValue( + 'different-hash' + ); + + await expect( + predictionService.revealPrediction(userId, predictionId, marketId) + ).rejects.toThrow( + 'Invalid commitment hash - cannot determine predicted outcome' + ); + }); + }); + + describe('claimWinnings', () => { + const userId = 'user-1'; + const predictionId = 'pred-1'; + + it('should successfully claim winnings for a winning prediction', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + id: predictionId, + userId, + status: PredictionStatus.SETTLED, + isWinner: true, + winningsClaimed: false, + pnlUsd: 180, + }); + + mockUserRepo.findById.mockResolvedValue({ + id: userId, + usdcBalance: 400, + }); + + const result = await predictionService.claimWinnings( + userId, + predictionId + ); + + expect(result.winnings).toBe(180); + expect(mockPredictionRepo.claimWinnings).toHaveBeenCalledWith( + predictionId + ); + expect(mockUserRepo.updateBalance).toHaveBeenCalledWith(userId, 580); + }); + + it('should throw error if prediction not found', async () => { + mockPredictionRepo.findById.mockResolvedValue(null); + + await expect( + predictionService.claimWinnings(userId, predictionId) + ).rejects.toThrow('Prediction not found'); + }); + + it('should throw error if unauthorized', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + userId: 'wrong-user', + }); + + await expect( + predictionService.claimWinnings(userId, predictionId) + ).rejects.toThrow('Unauthorized'); + }); + + it('should throw error if prediction is not settled', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + userId, + status: PredictionStatus.REVEALED, + }); + + await expect( + predictionService.claimWinnings(userId, predictionId) + ).rejects.toThrow('Prediction not settled'); + }); + + it('should throw error if prediction did not win', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + userId, + status: PredictionStatus.SETTLED, + isWinner: false, + }); + + await expect( + predictionService.claimWinnings(userId, predictionId) + ).rejects.toThrow('Prediction did not win'); + }); + + it('should throw error if winnings already claimed', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + userId, + status: PredictionStatus.SETTLED, + isWinner: true, + winningsClaimed: true, + }); + + await expect( + predictionService.claimWinnings(userId, predictionId) + ).rejects.toThrow('Winnings already claimed'); + }); + + it('should throw error if no winnings to claim', async () => { + mockPredictionRepo.findById.mockResolvedValue({ + userId, + status: PredictionStatus.SETTLED, + isWinner: true, + winningsClaimed: false, + pnlUsd: 0, + }); + + await expect( + predictionService.claimWinnings(userId, predictionId) + ).rejects.toThrow('No winnings to claim'); + }); + }); +}); diff --git a/backend/tests/services/user.service.integration.test.ts b/backend/tests/services/user.service.integration.test.ts index 6af7dc89..50a3d574 100644 --- a/backend/tests/services/user.service.integration.test.ts +++ b/backend/tests/services/user.service.integration.test.ts @@ -86,13 +86,19 @@ describe('UserService Integration Tests', () => { }); await expect( - userService.authenticateUser(`wrongpass-${timestamp}@example.com`, 'WrongPassword') + userService.authenticateUser( + `wrongpass-${timestamp}@example.com`, + 'WrongPassword' + ) ).rejects.toThrow('Invalid credentials'); }); it('should reject non-existent user', async () => { await expect( - userService.authenticateUser(`nonexistent-${Date.now()}@example.com`, 'AnyPassword') + userService.authenticateUser( + `nonexistent-${Date.now()}@example.com`, + 'AnyPassword' + ) ).rejects.toThrow('Invalid credentials'); }); }); @@ -172,7 +178,7 @@ describe('UserService Integration Tests', () => { password: 'SecurePass123!', }); - // Removed failing test: should search users by username + // Removed failing test: should search users by username }); }); }); diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts index 22b1944c..3bb6ccf9 100644 --- a/backend/tests/setup.ts +++ b/backend/tests/setup.ts @@ -22,7 +22,8 @@ process.env.DATABASE_URL = process.env.DATABASE_URL || 'postgresql://postgres:password@localhost:5432/boxmeout_test'; process.env.REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; -process.env.ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'test-encryption-key-32-chars!!'; +process.env.ENCRYPTION_KEY = + process.env.ENCRYPTION_KEY || 'test-encryption-key-32-chars!!'; // Stellar test configuration process.env.ADMIN_WALLET_SECRET = @@ -36,13 +37,12 @@ process.env.AMM_CONTRACT_ADDRESS = // Mock console methods to keep test output clean for middleware tests beforeEach(() => { - vi.spyOn(console, 'log').mockImplementation(() => { }); - vi.spyOn(console, 'info').mockImplementation(() => { }); - vi.spyOn(console, 'warn').mockImplementation(() => { }); - vi.spyOn(console, 'error').mockImplementation(() => { }); + vi.spyOn(console, 'log').mockImplementation(() => {}); + vi.spyOn(console, 'info').mockImplementation(() => {}); + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.spyOn(console, 'error').mockImplementation(() => {}); }); - // Database setup (only for integration tests that actually need it) let prisma: PrismaClient | null = null; @@ -57,7 +57,8 @@ beforeAll(async () => { } // Only setup database for integration tests - const hasDatabaseUrl = process.env.DATABASE_URL_TEST || process.env.DATABASE_URL; + const hasDatabaseUrl = + process.env.DATABASE_URL_TEST || process.env.DATABASE_URL; if (hasDatabaseUrl && !process.env.SKIP_DB_SETUP) { logger.info('Setting up test database for integration tests'); @@ -124,4 +125,4 @@ afterAll(async () => { }); // Only export prisma if it was created -export { prisma }; \ No newline at end of file +export { prisma };