From e414625a4c22d61cbab609900f57b6f630f55e7d Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 5 Dec 2024 04:39:07 -0800 Subject: [PATCH] added testing suite --- indexer/jest.config.js | 14 +++ indexer/package.json | 15 ++- indexer/src/__tests__/db/mongodb.test.ts | 106 +++++++++++++++++ indexer/src/__tests__/routes/api.test.ts | 138 +++++++++++++++++++++++ indexer/src/__tests__/setup.ts | 26 +++++ indexer/src/routes/blocks.ts | 24 ++-- 6 files changed, 308 insertions(+), 15 deletions(-) create mode 100644 indexer/jest.config.js create mode 100644 indexer/src/__tests__/db/mongodb.test.ts create mode 100644 indexer/src/__tests__/routes/api.test.ts create mode 100644 indexer/src/__tests__/setup.ts diff --git a/indexer/jest.config.js b/indexer/jest.config.js new file mode 100644 index 0000000..ce74eea --- /dev/null +++ b/indexer/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.test.ts'], + moduleFileExtensions: ['ts', 'js', 'json', 'node'], + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/**/__tests__/**' + ], + coverageDirectory: 'coverage', + setupFilesAfterEnv: ['/src/__tests__/setup.ts'] +}; \ No newline at end of file diff --git a/indexer/package.json b/indexer/package.json index fee8b79..b552eb4 100644 --- a/indexer/package.json +++ b/indexer/package.json @@ -6,18 +6,27 @@ "scripts": { "build": "tsc", "start": "node dist/index.js", - "dev": "ts-node src/index.ts" + "dev": "ts-node src/index.ts", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" }, "dependencies": { + "dotenv": "^16.3.1", "ethers": "^5.7.2", "express": "^4.18.2", - "dotenv": "^16.3.1", "mongodb": "^6.3.0" }, "devDependencies": { "@types/express": "^4.17.21", + "@types/jest": "^29.5.14", "@types/node": "^20.10.4", + "@types/supertest": "^2.0.16", + "jest": "^29.7.0", + "mongodb-memory-server": "^9.1.1", + "supertest": "^6.3.3", + "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typescript": "^5.3.3" } -} \ No newline at end of file +} diff --git a/indexer/src/__tests__/db/mongodb.test.ts b/indexer/src/__tests__/db/mongodb.test.ts new file mode 100644 index 0000000..6c1e36c --- /dev/null +++ b/indexer/src/__tests__/db/mongodb.test.ts @@ -0,0 +1,106 @@ +import { MongoDBAdapter } from '../../db/mongodb'; +import { BlockData, ContractConfig, EventData } from '../../db/adapter'; +import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals'; + +describe('MongoDBAdapter', () => { + let adapter: MongoDBAdapter; + + beforeAll(async () => { + adapter = new MongoDBAdapter(process.env.MONGODB_URI!); + await adapter.connect(); + }); + + afterAll(async () => { + await adapter.disconnect(); + }); + + beforeEach(async () => { + // Clear collections before each test + const db = (adapter as any).client.db(); + await db.collection('blocks').deleteMany({}); + await db.collection('contracts').deleteMany({}); + await db.collection('events').deleteMany({}); + }); + + describe('Blocks', () => { + const mockBlock: BlockData = { + number: 1, + hash: '0x123', + timestamp: 1000, + transactions: ['0xabc'] + }; + + it('should save and retrieve a block', async () => { + await adapter.saveBlock(mockBlock); + const retrieved = await adapter.getBlock(mockBlock.number); + expect(retrieved).toEqual(mockBlock); + }); + + it('should get latest block', async () => { + await adapter.saveBlock({ ...mockBlock, number: 1 }); + await adapter.saveBlock({ ...mockBlock, number: 2 }); + const latest = await adapter.getLatestBlock(); + expect(latest?.number).toBe(2); + }); + }); + + describe('Contracts', () => { + const mockContract: ContractConfig = { + address: '0x123', + name: 'Test Contract', + abi: '[]', + events: [], + isActive: true, + createdAt: new Date(), + updatedAt: new Date() + }; + + it('should add and retrieve a contract', async () => { + await adapter.addContract(mockContract); + const retrieved = await adapter.getContract(mockContract.address); + expect(retrieved?.address).toBe(mockContract.address); + }); + + it('should list active contracts', async () => { + await adapter.addContract(mockContract); + await adapter.addContract({ ...mockContract, address: '0x456', isActive: false }); + const activeContracts = await adapter.listContracts(true); + expect(activeContracts.length).toBe(1); + expect(activeContracts[0].address).toBe('0x123'); + }); + }); + + describe('Events', () => { + const mockEvent: EventData = { + id: '1', + contractAddress: '0x123', + eventName: 'Transfer', + blockNumber: 1, + transactionHash: '0xabc', + timestamp: 1000, + returnValues: { from: '0x123', to: '0x456', value: '1000' }, + raw: { data: '0x', topics: [] } + }; + + it('should save and retrieve events', async () => { + await adapter.saveEvent(mockEvent); + const events = await adapter.getEvents(mockEvent.contractAddress); + expect(events.length).toBe(1); + expect(events[0].id).toBe(mockEvent.id); + }); + + it('should query events with filters', async () => { + await adapter.saveEvent(mockEvent); + await adapter.saveEvent({ ...mockEvent, id: '2', blockNumber: 2 }); + + const result = await adapter.queryEvents({ + contractAddress: '0x123', + fromBlock: 1, + toBlock: 1 + }); + + expect(result.events.length).toBe(1); + expect(result.total).toBe(1); + }); + }); +}); \ No newline at end of file diff --git a/indexer/src/__tests__/routes/api.test.ts b/indexer/src/__tests__/routes/api.test.ts new file mode 100644 index 0000000..e8d4fb4 --- /dev/null +++ b/indexer/src/__tests__/routes/api.test.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals'; +import express from 'express'; +import request from 'supertest'; +import { MongoDBAdapter } from '../../db/mongodb'; +import { createContractRoutes } from '../../routes/contracts'; +import { createEventRoutes } from '../../routes/events'; +import { createBlockRoutes } from '../../routes/blocks'; + +describe('API Routes', () => { + let app: express.Application; + let db: MongoDBAdapter; + + beforeAll(async () => { + db = new MongoDBAdapter(process.env.MONGODB_URI!); + await db.connect(); + + app = express(); + app.use(express.json()); + app.use('/contracts', createContractRoutes(db, '../out', '../deployments')); + app.use('/events', createEventRoutes(db)); + app.use('/blocks', createBlockRoutes(db)); + }); + + afterAll(async () => { + await db.disconnect(); + }); + + beforeEach(async () => { + const dbClient = (db as any).client.db(); + await dbClient.collection('blocks').deleteMany({}); + await dbClient.collection('contracts').deleteMany({}); + await dbClient.collection('events').deleteMany({}); + }); + + describe('Contract Routes', () => { + const mockContract = { + address: '0x123', + name: 'Test Contract', + abi: '[]' + }; + + it('should add a new contract', async () => { + const response = await request(app) + .post('/contracts') + .send(mockContract); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Contract added successfully'); + }); + + it('should list contracts', async () => { + await db.addContract({ + ...mockContract, + events: [], + isActive: true + }); + + const response = await request(app) + .get('/contracts'); + + expect(response.status).toBe(200); + expect(response.body.length).toBe(1); + expect(response.body[0].address).toBe(mockContract.address); + }); + }); + + describe('Event Routes', () => { + const mockEvent = { + name: 'Transfer', + signature: 'Transfer(address,address,uint256)', + abi: 'event Transfer(address indexed from, address indexed to, uint256 value)' + }; + + beforeEach(async () => { + await db.addContract({ + address: '0x123', + name: 'Test Contract', + abi: '[]', + events: [], + isActive: true + }); + }); + + it('should add an event to a contract', async () => { + const response = await request(app) + .post('/events/contracts/0x123/events') + .send(mockEvent); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Event added successfully'); + }); + + it('should query events', async () => { + const response = await request(app) + .post('/events/query') + .send({ + contractAddress: '0x123', + fromBlock: 1, + toBlock: 100 + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('events'); + expect(response.body).toHaveProperty('total'); + }); + }); + + describe('Block Routes', () => { + const mockBlock = { + number: 1, + hash: '0x123', + timestamp: 1000, + transactions: [] + }; + + beforeEach(async () => { + await db.saveBlock(mockBlock); + }); + + it('should get block by number', async () => { + const response = await request(app) + .get('/blocks/1'); + + expect(response.status).toBe(200); + expect(response.body.number).toBe(1); + expect(response.body.hash).toBe('0x123'); + }); + + it('should get indexing status', async () => { + const response = await request(app) + .get('/blocks/status'); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('latestIndexedBlock'); + expect(response.body).toHaveProperty('totalIndexedBlocks'); + }); + }); +}); \ No newline at end of file diff --git a/indexer/src/__tests__/setup.ts b/indexer/src/__tests__/setup.ts new file mode 100644 index 0000000..a94617b --- /dev/null +++ b/indexer/src/__tests__/setup.ts @@ -0,0 +1,26 @@ +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { MongoClient } from 'mongodb'; +import '@jest/globals'; + +let mongoServer: MongoMemoryServer; +let mongoClient: MongoClient; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + mongoClient = new MongoClient(mongoUri); + await mongoClient.connect(); + + // Set environment variables for testing + process.env.MONGODB_URI = mongoUri; + process.env.ETHEREUM_RPC_URL = 'http://localhost:8545'; // For local testing +}); + +afterAll(async () => { + if (mongoClient) { + await mongoClient.close(); + } + if (mongoServer) { + await mongoServer.stop(); + } +}); \ No newline at end of file diff --git a/indexer/src/routes/blocks.ts b/indexer/src/routes/blocks.ts index a9f1b30..d80a4cc 100644 --- a/indexer/src/routes/blocks.ts +++ b/indexer/src/routes/blocks.ts @@ -4,18 +4,6 @@ import { DatabaseAdapter } from '../db/adapter'; export function createBlockRoutes(db: DatabaseAdapter) { const router = Router(); - // Get block by number - router.get('/:number', async (req, res) => { - const blockNumber = parseInt(req.params.number); - const block = await db.getBlock(blockNumber); - - if (!block) { - return res.status(404).json({ error: 'Block not found' }); - } - - res.json(block); - }); - // Get indexing status router.get('/status', async (req, res) => { const totalBlocks = await db.getTotalBlocks(); @@ -33,5 +21,17 @@ export function createBlockRoutes(db: DatabaseAdapter) { }); }); + // Get block by number + router.get('/:number', async (req, res) => { + const blockNumber = parseInt(req.params.number); + const block = await db.getBlock(blockNumber); + + if (!block) { + return res.status(404).json({ error: 'Block not found' }); + } + + res.json(block); + }); + return router; } \ No newline at end of file