Initial project setup: Add contracts, API definitions, tests, and documentation
- Add Foundry project configuration (foundry.toml, foundry.lock) - Add Solidity contracts (TokenFactory138, BridgeVault138, ComplianceRegistry, etc.) - Add API definitions (OpenAPI, GraphQL, gRPC, AsyncAPI) - Add comprehensive test suite (unit, integration, fuzz, invariants) - Add API services (REST, GraphQL, orchestrator, packet service) - Add documentation (ISO20022 mapping, runbooks, adapter guides) - Add development tools (RBC tool, Swagger UI, mock server) - Update OpenZeppelin submodules to v5.0.0
This commit is contained in:
72
test/api/README.md
Normal file
72
test/api/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# API Tests
|
||||
|
||||
This directory contains integration and contract tests for the eMoney Token Factory API.
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
test/api/
|
||||
├── integration/ # Integration tests
|
||||
│ ├── rest-api.test.ts
|
||||
│ └── graphql.test.ts
|
||||
└── contract/ # Contract validation tests
|
||||
├── openapi-validation.test.ts
|
||||
└── event-schema-validation.test.ts
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run only integration tests
|
||||
pnpm run test:integration
|
||||
|
||||
# Run only contract tests
|
||||
pnpm run test:contract
|
||||
|
||||
# Watch mode
|
||||
pnpm run test:watch
|
||||
```
|
||||
|
||||
## Test Types
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Test actual API endpoints against running services:
|
||||
- REST API operations
|
||||
- GraphQL queries and mutations
|
||||
- End-to-end flows
|
||||
|
||||
### Contract Tests
|
||||
|
||||
Validate that implementations conform to specifications:
|
||||
- OpenAPI schema validation
|
||||
- AsyncAPI event schema validation
|
||||
- Request/response format validation
|
||||
|
||||
## Mock Servers
|
||||
|
||||
Use mock servers for testing without requiring full infrastructure:
|
||||
|
||||
```bash
|
||||
# Start all mock servers
|
||||
cd api/tools/mock-server
|
||||
pnpm run start:all
|
||||
|
||||
# Or start individually
|
||||
pnpm run start:rest # REST API mock (port 4010)
|
||||
pnpm run start:graphql # GraphQL mock (port 4020)
|
||||
```
|
||||
|
||||
## Test Environment
|
||||
|
||||
Set environment variables:
|
||||
|
||||
```bash
|
||||
export API_URL=http://localhost:3000
|
||||
export GRAPHQL_URL=http://localhost:4000/graphql
|
||||
export ACCESS_TOKEN=your-test-token
|
||||
```
|
||||
|
||||
64
test/api/contract/event-schema-validation.test.ts
Normal file
64
test/api/contract/event-schema-validation.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Event Schema Validation Tests
|
||||
* Ensures events conform to AsyncAPI specification
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import Ajv from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
|
||||
const ASYNCAPI_SPEC = join(__dirname, '../../../api/packages/asyncapi/asyncapi.yaml');
|
||||
|
||||
describe('AsyncAPI Event Schema Validation', () => {
|
||||
let asyncapiSpec: any;
|
||||
let ajv: Ajv;
|
||||
|
||||
beforeAll(() => {
|
||||
const specContent = readFileSync(ASYNCAPI_SPEC, 'utf-8');
|
||||
asyncapiSpec = yaml.load(specContent);
|
||||
ajv = new Ajv();
|
||||
addFormats(ajv);
|
||||
});
|
||||
|
||||
it('should have valid AsyncAPI structure', () => {
|
||||
expect(asyncapiSpec).toHaveProperty('asyncapi');
|
||||
expect(asyncapiSpec.asyncapi).toMatch(/^3\.\d+\.\d+$/);
|
||||
expect(asyncapiSpec).toHaveProperty('channels');
|
||||
});
|
||||
|
||||
it('should have all required event channels', () => {
|
||||
const requiredChannels = [
|
||||
'triggers.created',
|
||||
'triggers.state.updated',
|
||||
'liens.placed',
|
||||
'liens.reduced',
|
||||
'liens.released',
|
||||
'packets.generated',
|
||||
'packets.dispatched',
|
||||
'packets.acknowledged',
|
||||
'bridge.locked',
|
||||
'bridge.unlocked',
|
||||
'compliance.updated',
|
||||
'policy.updated',
|
||||
];
|
||||
|
||||
requiredChannels.forEach((channel) => {
|
||||
expect(asyncapiSpec.channels).toHaveProperty(channel);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have event envelope schema', () => {
|
||||
expect(asyncapiSpec.components).toHaveProperty('schemas');
|
||||
expect(asyncapiSpec.components.schemas).toHaveProperty('EventEnvelope');
|
||||
|
||||
const envelopeSchema = asyncapiSpec.components.schemas.EventEnvelope;
|
||||
expect(envelopeSchema.required).toContain('eventId');
|
||||
expect(envelopeSchema.required).toContain('eventType');
|
||||
expect(envelopeSchema.required).toContain('occurredAt');
|
||||
expect(envelopeSchema.required).toContain('payload');
|
||||
});
|
||||
});
|
||||
|
||||
62
test/api/contract/openapi-validation.test.ts
Normal file
62
test/api/contract/openapi-validation.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* OpenAPI Contract Validation Tests
|
||||
* Ensures API implementation conforms to OpenAPI specification
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import Ajv from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
|
||||
const OPENAPI_SPEC = join(__dirname, '../../../api/packages/openapi/v1/openapi.yaml');
|
||||
|
||||
describe('OpenAPI Contract Validation', () => {
|
||||
let openapiSpec: any;
|
||||
|
||||
beforeAll(() => {
|
||||
const specContent = readFileSync(OPENAPI_SPEC, 'utf-8');
|
||||
openapiSpec = yaml.load(specContent);
|
||||
});
|
||||
|
||||
it('should have valid OpenAPI structure', () => {
|
||||
expect(openapiSpec).toHaveProperty('openapi');
|
||||
expect(openapiSpec.openapi).toMatch(/^3\.\d+\.\d+$/);
|
||||
expect(openapiSpec).toHaveProperty('info');
|
||||
expect(openapiSpec).toHaveProperty('paths');
|
||||
});
|
||||
|
||||
it('should have all required paths', () => {
|
||||
const requiredPaths = [
|
||||
'/tokens',
|
||||
'/tokens/{code}',
|
||||
'/liens',
|
||||
'/liens/{lienId}',
|
||||
'/compliance/accounts/{accountRefId}',
|
||||
'/triggers',
|
||||
'/triggers/{triggerId}',
|
||||
'/iso/inbound',
|
||||
'/iso/outbound',
|
||||
'/packets',
|
||||
'/bridge/lock',
|
||||
'/bridge/unlock',
|
||||
];
|
||||
|
||||
requiredPaths.forEach((path) => {
|
||||
expect(openapiSpec.paths).toHaveProperty(path);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have security schemes defined', () => {
|
||||
expect(openapiSpec.components).toHaveProperty('securitySchemes');
|
||||
expect(openapiSpec.components.securitySchemes).toHaveProperty('oauth2');
|
||||
expect(openapiSpec.components.securitySchemes).toHaveProperty('mtls');
|
||||
});
|
||||
|
||||
it('should have idempotency markers', () => {
|
||||
expect(openapiSpec).toHaveProperty('x-idempotency');
|
||||
expect(Array.isArray(openapiSpec['x-idempotency'])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
91
test/api/integration/graphql.test.ts
Normal file
91
test/api/integration/graphql.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* GraphQL API Integration Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll } from '@jest/globals';
|
||||
import { GraphQLClient } from 'graphql-request';
|
||||
|
||||
const GRAPHQL_URL = process.env.GRAPHQL_URL || 'http://localhost:4000/graphql';
|
||||
|
||||
describe('GraphQL API Integration Tests', () => {
|
||||
let client: GraphQLClient;
|
||||
|
||||
beforeAll(() => {
|
||||
client = new GraphQLClient(GRAPHQL_URL, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.ACCESS_TOKEN || 'test-token'}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('Queries', () => {
|
||||
it('should query token', async () => {
|
||||
const query = `
|
||||
query GetToken($code: String!) {
|
||||
token(code: $code) {
|
||||
code
|
||||
address
|
||||
name
|
||||
symbol
|
||||
policy {
|
||||
lienMode
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await client.request(query, { code: 'USDW' });
|
||||
expect(data).toHaveProperty('token');
|
||||
expect(data.token).toHaveProperty('code');
|
||||
});
|
||||
|
||||
it('should query triggers', async () => {
|
||||
const query = `
|
||||
query GetTriggers($filter: TriggerFilter, $paging: Paging) {
|
||||
triggers(filter: $filter, paging: $paging) {
|
||||
items {
|
||||
triggerId
|
||||
rail
|
||||
state
|
||||
}
|
||||
total
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await client.request(query, {
|
||||
filter: { state: 'PENDING' },
|
||||
paging: { limit: 10, offset: 0 },
|
||||
});
|
||||
|
||||
expect(data).toHaveProperty('triggers');
|
||||
expect(data.triggers).toHaveProperty('items');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mutations', () => {
|
||||
it('should deploy token via mutation', async () => {
|
||||
const mutation = `
|
||||
mutation DeployToken($input: DeployTokenInput!) {
|
||||
deployToken(input: $input) {
|
||||
code
|
||||
address
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await client.request(mutation, {
|
||||
input: {
|
||||
name: 'Test Token',
|
||||
symbol: 'TEST',
|
||||
decimals: 18,
|
||||
issuer: '0x1234567890123456789012345678901234567890',
|
||||
},
|
||||
});
|
||||
|
||||
expect(data).toHaveProperty('deployToken');
|
||||
expect(data.deployToken).toHaveProperty('code');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
105
test/api/integration/rest-api.test.ts
Normal file
105
test/api/integration/rest-api.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* REST API Integration Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
|
||||
import axios from 'axios';
|
||||
|
||||
const BASE_URL = process.env.API_URL || 'http://localhost:3000';
|
||||
const API_KEY = process.env.API_KEY || 'test-key';
|
||||
|
||||
describe('REST API Integration Tests', () => {
|
||||
let accessToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// TODO: Get OAuth2 token
|
||||
// accessToken = await getAccessToken();
|
||||
});
|
||||
|
||||
describe('Token Operations', () => {
|
||||
it('should deploy a token', async () => {
|
||||
const response = await axios.post(
|
||||
`${BASE_URL}/v1/tokens`,
|
||||
{
|
||||
name: 'Test Token',
|
||||
symbol: 'TEST',
|
||||
decimals: 18,
|
||||
issuer: '0x1234567890123456789012345678901234567890',
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Idempotency-Key': `test-${Date.now()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.data).toHaveProperty('code');
|
||||
expect(response.data).toHaveProperty('address');
|
||||
});
|
||||
|
||||
it('should list tokens', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/v1/tokens`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.data).toHaveProperty('items');
|
||||
expect(Array.isArray(response.data.items)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lien Operations', () => {
|
||||
it('should place a lien', async () => {
|
||||
const response = await axios.post(
|
||||
`${BASE_URL}/v1/liens`,
|
||||
{
|
||||
debtor: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
|
||||
amount: '1000000000000000000',
|
||||
priority: 1,
|
||||
reasonCode: 'DEBT_ENFORCEMENT',
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.data).toHaveProperty('lienId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ISO-20022 Operations', () => {
|
||||
it('should submit outbound message', async () => {
|
||||
const response = await axios.post(
|
||||
`${BASE_URL}/v1/iso/outbound`,
|
||||
{
|
||||
msgType: 'pacs.008',
|
||||
instructionId: `0x${'1'.repeat(64)}`,
|
||||
payloadHash: `0x${'a'.repeat(64)}`,
|
||||
payload: '<Document>...</Document>',
|
||||
rail: 'FEDWIRE',
|
||||
token: '0x1234567890123456789012345678901234567890',
|
||||
amount: '1000000000000000000',
|
||||
accountRefId: `0x${'b'.repeat(64)}`,
|
||||
counterpartyRefId: `0x${'c'.repeat(64)}`,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Idempotency-Key': `test-${Date.now()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.data).toHaveProperty('triggerId');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
40
test/api/package.json
Normal file
40
test/api/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@emoney/api-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "API integration and contract tests",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"test:integration": "jest --testPathPattern=integration",
|
||||
"test:contract": "jest --testPathPattern=contract",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-request": "^6.1.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.10.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"**/test/**/*.test.ts"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"api/services/**/*.ts",
|
||||
"!**/*.d.ts",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user