Comprehensive testing strategies and validation procedures for Neo N3 MCP Server
Neo N3 MCP Server maintains 90%+ test coverage through a comprehensive testing strategy that includes unit tests, integration tests, security validation, and performance testing.
# Run all tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watch
# Run specific test suite
npm run test:unit
npm run test:integration
npm run test:security
# Run tests with detailed output
npm run test:verbose
# Run performance tests
npm run test:performance
# Build test image
docker build -t neo-n3-mcp:test --target test .
# Run tests in container
docker run --rm neo-n3-mcp:test npm test
# Run with test network
docker-compose -f docker-compose.test.yml up --abort-on-container-exit
# Interactive test environment
docker run -it --rm neo-n3-mcp:test bash
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'
import { WalletService } from '../src/services/wallet-service'
import { MockNeoRpcClient } from './mocks/neo-rpc-client'
describe('WalletService', () => {
let walletService: WalletService
let mockRpcClient: MockNeoRpcClient
beforeEach(() => {
mockRpcClient = new MockNeoRpcClient()
walletService = new WalletService(mockRpcClient)
})
afterEach(() => {
jest.clearAllMocks()
})
describe('createWallet', () => {
it('should create a new wallet with valid password', async () => {
const result = await walletService.createWallet('secure-password-123')
expect(result).toHaveProperty('address')
expect(result).toHaveProperty('publicKey')
expect(result.address).toMatch(/^N[A-Za-z0-9]{33}$/)
expect(result.encrypted).toBe(true)
})
it('should reject weak passwords', async () => {
await expect(walletService.createWallet('123'))
.rejects.toThrow('Password too weak')
})
it('should handle network errors gracefully', async () => {
mockRpcClient.setNetworkError(true)
await expect(walletService.createWallet('secure-password-123'))
.rejects.toThrow('Network connection failed')
})
})
describe('getBalance', () => {
it('should return balance for valid address', async () => {
const address = 'NZNos2WqTbu5oCgyfss9kUJgBXJqhuYAaj'
mockRpcClient.setBalance(address, { NEO: '100', GAS: '50.5' })
const balance = await walletService.getBalance(address)
expect(balance.NEO).toBe('100')
expect(balance.GAS).toBe('50.5')
})
it('should validate address format', async () => {
await expect(walletService.getBalance('invalid-address'))
.rejects.toThrow('Invalid address format')
})
})
})
describe('ContractService', () => {
describe('Famous Contracts', () => {
it('should list all supported contracts', async () => {
const contracts = await contractService.listFamousContracts()
expect(contracts).toHaveLength(6)
expect(contracts.map(c => c.name)).toEqual([
'NeoFS', 'NeoBurger', 'Flamingo',
'NeoCompound', 'GrandShare', 'GhostMarket'
])
})
it('should get contract information', async () => {
const contract = await contractService.getFamousContract('NeoFS')
expect(contract).toHaveProperty('hash')
expect(contract).toHaveProperty('name', 'NeoFS')
expect(contract).toHaveProperty('description')
expect(contract.methods).toBeInstanceOf(Array)
})
it('should invoke read-only contract methods', async () => {
const result = await contractService.invokeRead(
'NeoFS',
'totalSupply',
[]
)
expect(result).toHaveProperty('type', 'Integer')
expect(result).toHaveProperty('value')
})
})
})
describe('MCP Protocol Integration', () => {
let mcpServer: MCPServer
beforeAll(async () => {
mcpServer = new MCPServer()
await mcpServer.initialize()
})
afterAll(async () => {
await mcpServer.shutdown()
})
describe('Tools', () => {
it('should list all available tools', async () => {
const tools = await mcpServer.listTools()
expect(tools).toHaveLength(34)
expect(tools.every(tool => tool.name && tool.description)).toBe(true)
})
it('should execute blockchain tools', async () => {
const result = await mcpServer.callTool('get_blockchain_info', {})
expect(result.isError).toBe(false)
expect(result.content).toHaveProperty('network')
expect(result.content).toHaveProperty('blockHeight')
})
it('should handle tool execution errors', async () => {
const result = await mcpServer.callTool('get_balance', {
address: 'invalid-address'
})
expect(result.isError).toBe(true)
expect(result.content).toContain('Invalid address format')
})
})
describe('Resources', () => {
it('should list all available resources', async () => {
const resources = await mcpServer.listResources()
expect(resources).toHaveLength(9)
expect(resources.every(resource => resource.uri && resource.name)).toBe(true)
})
it('should read network status resource', async () => {
const content = await mcpServer.readResource('neo://network/status')
expect(content).toHaveProperty('mimeType', 'application/json')
expect(content.text).toBeTruthy()
const data = JSON.parse(content.text)
expect(data).toHaveProperty('network')
expect(data).toHaveProperty('status')
})
})
})
describe('Neo N3 Network Integration', () => {
describe('Testnet Integration', () => {
it('should connect to testnet', async () => {
const neoService = new NeoService('testnet')
await neoService.initialize()
const info = await neoService.getBlockchainInfo()
expect(info.network).toBe('testnet')
expect(info.blockHeight).toBeGreaterThan(0)
})
it('should retrieve real block data', async () => {
const block = await neoService.getBlock(1)
expect(block).toHaveProperty('hash')
expect(block).toHaveProperty('index', 1)
expect(block).toHaveProperty('previousblockhash')
expect(block.tx).toBeInstanceOf(Array)
})
it('should handle network timeouts', async () => {
const neoService = new NeoService('testnet', {
timeout: 1 // Very short timeout
})
await expect(neoService.getBlockchainInfo())
.rejects.toThrow(/timeout/i)
})
})
describe('Famous Contracts Integration', () => {
it('should interact with real NeoFS contract', async () => {
const result = await contractService.invokeRead(
'NeoFS',
'symbol',
[]
)
expect(result.value).toBe('FS')
})
it('should retrieve Flamingo token information', async () => {
const result = await contractService.invokeRead(
'Flamingo',
'totalSupply',
[]
)
expect(parseInt(result.value)).toBeGreaterThan(0)
})
})
})
describe('Security Validation', () => {
describe('Input Sanitization', () => {
const maliciousInputs = [
'',
'javascript:alert("xss")',
'../../etc/passwd',
'DROP TABLE users;',
'${process.env.SECRET}',
'\x00\x01\x02',
'A'.repeat(10000) // Large input
]
maliciousInputs.forEach(input => {
it(`should sanitize malicious input: ${input.substring(0, 20)}...`, async () => {
await expect(mcpServer.callTool('get_balance', {
address: input
})).rejects.toThrow(/Invalid address format|Input validation failed/)
})
})
})
describe('Rate Limiting', () => {
it('should enforce rate limits', async () => {
const promises = Array(150).fill(0).map(() =>
mcpServer.callTool('get_blockchain_info', {})
)
const results = await Promise.allSettled(promises)
const rejected = results.filter(r => r.status === 'rejected')
expect(rejected.length).toBeGreaterThan(0)
expect(rejected.some(r =>
r.reason.message.includes('Rate limit exceeded')
)).toBe(true)
})
})
describe('Confirmation Requirements', () => {
it('should require confirmation for write operations', async () => {
await expect(mcpServer.callTool('transfer_assets', {
fromWIF: 'test-key',
toAddress: 'test-address',
asset: 'GAS',
amount: '1',
confirm: false // Missing confirmation
})).rejects.toThrow('Confirmation required')
})
})
describe('Private Key Security', () => {
it('should never log private keys', async () => {
const consoleSpy = jest.spyOn(console, 'log')
const errorSpy = jest.spyOn(console, 'error')
try {
await mcpServer.callTool('import_wallet', {
key: 'KxDgvEKzgSBPPfuVfw67oPQBSjidEiqTHURKSDL1R7yGaGYAeYnr',
password: 'test'
})
} catch (e) {
// Expected to fail in test environment
}
const allLogs = [
...consoleSpy.mock.calls.flat(),
...errorSpy.mock.calls.flat()
].join(' ')
expect(allLogs).not.toContain('KxDgvEKzgSBPPfuVfw67oPQBSjidEiqTHURKSDL1R7yGaGYAeYnr')
consoleSpy.mockRestore()
errorSpy.mockRestore()
})
})
})
describe('Network Security', () => {
it('should isolate mainnet and testnet operations', async () => {
const mainnetService = new NeoService('mainnet')
const testnetService = new NeoService('testnet')
const mainnetInfo = await mainnetService.getBlockchainInfo()
const testnetInfo = await testnetService.getBlockchainInfo()
expect(mainnetInfo.network).toBe('mainnet')
expect(testnetInfo.network).toBe('testnet')
expect(mainnetInfo.magic).not.toBe(testnetInfo.magic)
})
it('should validate RPC endpoint certificates', async () => {
const neoService = new NeoService('mainnet', {
rpcUrl: 'https://invalid-certificate.example.com'
})
await expect(neoService.getBlockchainInfo())
.rejects.toThrow(/certificate|SSL|TLS/)
})
})
describe('Performance Tests', () => {
describe('Throughput', () => {
it('should handle 100 concurrent requests', async () => {
const startTime = Date.now()
const promises = Array(100).fill(0).map((_, i) =>
mcpServer.callTool('get_blockchain_info', {})
)
const results = await Promise.all(promises)
const endTime = Date.now()
const duration = endTime - startTime
const requestsPerSecond = 100 / (duration / 1000)
expect(results.every(r => !r.isError)).toBe(true)
expect(requestsPerSecond).toBeGreaterThan(10) // At least 10 RPS
expect(duration).toBeLessThan(30000) // Under 30 seconds
})
})
describe('Response Times', () => {
const tools = [
'get_blockchain_info',
'get_network_mode',
'list_famous_contracts'
]
tools.forEach(toolName => {
it(`should respond to ${toolName} within 2 seconds`, async () => {
const startTime = Date.now()
const result = await mcpServer.callTool(toolName, {})
const duration = Date.now() - startTime
expect(result.isError).toBe(false)
expect(duration).toBeLessThan(2000) // Under 2 seconds
})
})
})
describe('Memory Usage', () => {
it('should not leak memory under load', async () => {
const initialMemory = process.memoryUsage().heapUsed
// Perform many operations
for (let i = 0; i < 1000; i++) {
await mcpServer.callTool('get_blockchain_info', {})
if (i % 100 === 0) {
global.gc && global.gc() // Force garbage collection if available
}
}
const finalMemory = process.memoryUsage().heapUsed
const memoryIncrease = finalMemory - initialMemory
// Memory should not increase by more than 50MB
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024)
})
})
})
describe('Cache Performance', () => {
it('should improve response times with caching', async () => {
// Clear cache
await cacheService.clear()
// First request (cache miss)
const start1 = Date.now()
await mcpServer.callTool('get_blockchain_info', {})
const time1 = Date.now() - start1
// Second request (cache hit)
const start2 = Date.now()
await mcpServer.callTool('get_blockchain_info', {})
const time2 = Date.now() - start2
// Cache hit should be significantly faster
expect(time2).toBeLessThan(time1 * 0.5)
})
it('should maintain high cache hit rate', async () => {
const operations = Array(100).fill(0).map(() =>
mcpServer.callTool('get_blockchain_info', {})
)
await Promise.all(operations)
const cacheStats = await cacheService.getStats()
expect(cacheStats.hitRate).toBeGreaterThan(0.8) // 80% hit rate
})
})
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit
- run: npm run test:coverage
- uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
integration-tests:
runs-on: ubuntu-latest
services:
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run test:integration
env:
NEO_NETWORK: testnet
REDIS_URL: redis://localhost:6379
security-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run test:security
- run: npm audit --audit-level moderate
performance-tests:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run test:performance
- name: Performance regression check
run: |
node scripts/check-performance-regression.js
# Setup test environment
npm run test:setup
# Generate test wallets
npm run test:generate-wallets
# Setup test contracts
npm run test:deploy-contracts
# Cleanup test data
npm run test:cleanup
# Reset test environment
npm run test:reset