Skip to content

Commit

Permalink
split out routes
Browse files Browse the repository at this point in the history
  • Loading branch information
0age committed Dec 8, 2024
1 parent 426740a commit 0a8c2c9
Show file tree
Hide file tree
Showing 13 changed files with 1,901 additions and 1,691 deletions.
963 changes: 11 additions & 952 deletions src/__tests__/routes.test.ts

Large diffs are not rendered by default.

402 changes: 402 additions & 0 deletions src/__tests__/routes/balance.test.ts

Large diffs are not rendered by default.

336 changes: 336 additions & 0 deletions src/__tests__/routes/compact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
import { FastifyInstance } from 'fastify';
import {
createTestServer,
validPayload,
getFreshCompact,
cleanupTestServer,
compactToAPI,
generateSignature,
} from '../utils/test-server';
import {
graphqlClient,
AllocatorResponse,
AccountDeltasResponse,
AccountResponse,
} from '../../graphql';
import { RequestDocument, Variables, RequestOptions } from 'graphql-request';
import { hexToBytes } from 'viem/utils';

describe('Compact Routes', () => {
let server: FastifyInstance;
let sessionId: string;
let originalRequest: typeof graphqlClient.request;

beforeEach(async () => {
server = await createTestServer();

// Store original function
originalRequest = graphqlClient.request;

// Mock GraphQL response
graphqlClient.request = async <
V extends Variables = Variables,
T = AllocatorResponse & AccountDeltasResponse & AccountResponse,
>(
_documentOrOptions: RequestDocument | RequestOptions<V, T>,
..._variablesAndRequestHeaders: unknown[]
): Promise<
AllocatorResponse & AccountDeltasResponse & AccountResponse
> => ({
allocator: {
supportedChains: {
items: [{ allocatorId: '1' }],
},
},
accountDeltas: {
items: [],
},
account: {
resourceLocks: {
items: [
{
withdrawalStatus: 0,
balance: '1000000000000000000000',
},
],
},
claims: {
items: [],
},
},
});

// Create a session for testing
const sessionResponse = await server.inject({
method: 'GET',
url: `/session/1/${validPayload.address}`,
});

expect(sessionResponse.statusCode).toBe(200);
const sessionRequest = JSON.parse(sessionResponse.payload);

// Normalize timestamps to match database precision
const payload = {
...sessionRequest.session,
issuedAt: new Date(sessionRequest.session.issuedAt).toISOString(),
expirationTime: new Date(
sessionRequest.session.expirationTime
).toISOString(),
};

const signature = await generateSignature(payload);
const response = await server.inject({
method: 'POST',
url: '/session',
payload: {
payload,
signature,
},
});

const result = JSON.parse(response.payload);
expect(response.statusCode).toBe(200);
expect(result.session?.id).toBeDefined();
sessionId = result.session.id;
});

afterEach(async () => {
await cleanupTestServer();
// Restore original function
graphqlClient.request = originalRequest;
});

describe('GET /suggested-nonce/:chainId', () => {
it('should return a valid nonce for authenticated user', async () => {
const response = await server.inject({
method: 'GET',
url: '/suggested-nonce/1',
headers: {
'x-session-id': sessionId,
},
});

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);
expect(result).toHaveProperty('nonce');
expect(result.nonce).toMatch(/^0x[0-9a-f]{64}$/i);

// Verify nonce format: first 20 bytes should match sponsor address
const nonceHex = BigInt(result.nonce).toString(16).padStart(64, '0');
const sponsorHex = validPayload.address.toLowerCase().slice(2);
expect(nonceHex.slice(0, 40)).toBe(sponsorHex);
});

it('should reject request without session', async () => {
const response = await server.inject({
method: 'GET',
url: '/suggested-nonce/1',
});

expect(response.statusCode).toBe(401);
});
});

describe('POST /compact', () => {
it('should submit valid compact', async () => {
const freshCompact = getFreshCompact();
const compactPayload = {
chainId: '1',
compact: compactToAPI(freshCompact),
};

const response = await server.inject({
method: 'POST',
url: '/compact',
headers: {
'x-session-id': sessionId,
},
payload: compactPayload,
});

if (response.statusCode !== 200) {
console.error('Error submitting compact:', response.payload);
}

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);
expect(result).toHaveProperty('hash');
expect(result).toHaveProperty('signature');
expect(result).toHaveProperty('nonce');
expect(result.nonce).toBe(
'0x' + freshCompact.nonce.toString(16).padStart(64, '0')
);
});

it('should handle null nonce by generating one', async () => {
const freshCompact = getFreshCompact();
const compactPayload = {
chainId: '1',
compact: {
...compactToAPI(freshCompact),
nonce: null,
},
};

const response = await server.inject({
method: 'POST',
url: '/compact',
headers: {
'x-session-id': sessionId,
},
payload: compactPayload,
});

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);
expect(result).toHaveProperty('hash');
expect(result).toHaveProperty('signature');
expect(result).toHaveProperty('nonce');
// Verify nonce format: first 20 bytes should match sponsor address
const nonceHex = BigInt(result.nonce).toString(16).padStart(64, '0');
const sponsorHex = freshCompact.sponsor.toLowerCase().slice(2);
expect(nonceHex.slice(0, 40)).toBe(sponsorHex);
});

it('should reject request without session', async () => {
const freshCompact = getFreshCompact();
const compactPayload = {
chainId: '1',
compact: compactToAPI(freshCompact),
};

const response = await server.inject({
method: 'POST',
url: '/compact',
payload: compactPayload,
});

expect(response.statusCode).toBe(401);
});

it('should store nonce after successful submission', async (): Promise<void> => {
const freshCompact = getFreshCompact();
const chainId = '1';

// Submit compact
const response = await server.inject({
method: 'POST',
url: '/compact',
headers: {
'x-session-id': sessionId,
},
payload: { chainId, compact: compactToAPI(freshCompact) },
});

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);

// Extract nonce components
const nonceHex = BigInt(result.nonce).toString(16).padStart(64, '0');
const fragmentPart = nonceHex.slice(40); // last 12 bytes (24 hex chars)
const fragmentBigInt = BigInt('0x' + fragmentPart);
const nonceLow = Number(fragmentBigInt & BigInt(0xffffffff));
const nonceHigh = Number(fragmentBigInt >> BigInt(32));

// Verify nonce was stored with correct high and low values
const dbResult = await server.db.query<{ count: number }>(
'SELECT COUNT(*) as count FROM nonces WHERE chain_id = $1 AND sponsor = $2 AND nonce_high = $3 AND nonce_low = $4',
[
chainId,
hexToBytes(freshCompact.sponsor as `0x${string}`),
nonceHigh,
nonceLow,
]
);
expect(dbResult.rows[0].count).toBe(1);
});
});

describe('GET /compacts', () => {
it('should return compacts for authenticated user', async () => {
const response = await server.inject({
method: 'GET',
url: '/compacts',
headers: {
'x-session-id': sessionId,
},
});

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);
expect(Array.isArray(result)).toBe(true);
});

it('should reject request without session', async () => {
const response = await server.inject({
method: 'GET',
url: '/compacts',
});

expect(response.statusCode).toBe(401);
});
});

describe('GET /compact/:chainId/:claimHash', () => {
it('should return specific compact', async () => {
const freshCompact = getFreshCompact();
const compactPayload = {
chainId: '1',
compact: compactToAPI(freshCompact),
};

// First submit a compact
const submitResponse = await server.inject({
method: 'POST',
url: '/compact',
headers: {
'x-session-id': sessionId,
},
payload: compactPayload,
});

const submitResult = JSON.parse(submitResponse.payload);
if (submitResponse.statusCode !== 200 || !submitResult?.hash) {
console.error('Failed to submit compact:', submitResponse.payload);
throw new Error('Failed to submit compact');
}

const { hash } = submitResult;

const response = await server.inject({
method: 'GET',
url: `/compact/1/${hash}`,
headers: {
'x-session-id': sessionId,
},
});

if (response.statusCode === 500) {
console.error('Got 500 error:', {
payload: response.payload,
hash,
sessionId,
});
}

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);
expect(result).toHaveProperty('chainId', '1');
expect(result).toHaveProperty('hash', hash);
expect(result).toHaveProperty('compact');
expect(result.compact).toHaveProperty('nonce');
});

it('should return error for non-existent compact', async () => {
const response = await server.inject({
method: 'GET',
url: '/compact/1/0x0000000000000000000000000000000000000000000000000000000000000000',
headers: {
'x-session-id': sessionId,
},
});

expect(response.statusCode).toBe(404);
});
});
});
57 changes: 57 additions & 0 deletions src/__tests__/routes/health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { FastifyInstance } from 'fastify';
import { createTestServer, cleanupTestServer } from '../utils/test-server';

describe('Health Routes', () => {
let server: FastifyInstance;

beforeEach(async () => {
server = await createTestServer();
});

afterEach(async () => {
await cleanupTestServer();
});

describe('GET /health', () => {
it('should return health status and addresses', async () => {
const response = await server.inject({
method: 'GET',
url: '/health',
});

expect(response.statusCode).toBe(200);
const body = JSON.parse(response.payload);

expect(body).toHaveProperty('status', 'healthy');
expect(body).toHaveProperty('allocatorAddress');
expect(body).toHaveProperty('signingAddress');
expect(body).toHaveProperty('timestamp');

// Verify timestamp is a valid ISO 8601 date
expect(new Date(body.timestamp).toISOString()).toBe(body.timestamp);
});

it('should fail if environment variables are not set', async () => {
// Store original env vars
const originalAllocator = process.env.ALLOCATOR_ADDRESS;
const originalSigning = process.env.SIGNING_ADDRESS;

// Unset env vars
delete process.env.ALLOCATOR_ADDRESS;
delete process.env.SIGNING_ADDRESS;

try {
const response = await server.inject({
method: 'GET',
url: '/health',
});

expect(response.statusCode).toBe(500);
} finally {
// Restore env vars
process.env.ALLOCATOR_ADDRESS = originalAllocator;
process.env.SIGNING_ADDRESS = originalSigning;
}
});
});
});
Loading

0 comments on commit 0a8c2c9

Please sign in to comment.