-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
1,901 additions
and
1,691 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.