Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion listener/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ module.exports = {
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
'^.+\\.tsx?$': ['ts-jest', {
diagnostics: {
ignoreCodes: [2307]
}
}]
},
moduleNameMapper: {
'^@stellar/stellar-sdk$': '<rootDir>/src/__mocks__/@stellar/stellar-sdk.ts',
'^node-cache$': '<rootDir>/src/__mocks__/node-cache.ts'
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.js']
};
5 changes: 1 addition & 4 deletions listener/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@
"lint": "node ./node_modules/typescript/bin/tsc --noEmit",
"test": "node ./node_modules/jest/bin/jest.js",
"migrate": "ts-node src/scripts/migrate-db.ts",
"migrate:templates": "ts-node src/scripts/migrate-templates.ts"
"typecheck": "node ./node_modules/typescript/bin/tsc --noEmit",
"lint": "node ./node_modules/typescript/bin/tsc --noEmit",
"migrate": "ts-node src/scripts/migrate-db.ts",
"migrate:templates": "ts-node src/scripts/migrate-templates.ts",
"check-migrations": "ts-node src/scripts/check-migrations.ts",
"validate:batch": "ts-node src/utils/batch-validator.ts"
},
Expand Down
40 changes: 40 additions & 0 deletions listener/src/__mocks__/@stellar/stellar-sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Manual mock for @stellar/stellar-sdk.
* Used by Jest (via moduleNameMapper) when the real package is not installed.
*/

export const rpc = {
Server: jest.fn().mockImplementation(() => ({
getHealth: jest.fn().mockResolvedValue({ status: 'healthy' }),
getEvents: jest.fn().mockResolvedValue({ events: [] }),
})),
};

export const Contract = jest.fn().mockImplementation(() => ({
call: jest.fn(),
}));

export const Keypair = {
random: jest.fn().mockReturnValue({
publicKey: jest.fn().mockReturnValue('GABC1234'),
secret: jest.fn().mockReturnValue('SECRET'),
}),
};

export const Account = jest.fn().mockImplementation((publicKey: string, sequence: string) => ({
publicKey: () => publicKey,
sequence,
}));

export const Networks = {
TESTNET: 'Test SDF Network ; September 2015',
MAINNET: 'Public Global Stellar Network ; September 2015',
};

export default {
rpc,
Contract,
Keypair,
Account,
Networks,
};
49 changes: 49 additions & 0 deletions listener/src/__mocks__/node-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Manual mock for node-cache.
* Used by Jest (via moduleNameMapper) when the real package is not installed.
*/

class NodeCache {
private store: Map<string, any> = new Map();
private listeners: Map<string, ((...args: any[]) => void)[]> = new Map();

constructor(_options?: Record<string, any>) {}

get<T>(key: string): T | undefined {
return this.store.get(key) as T | undefined;
}

set(key: string, value: any, _ttl?: number): boolean {
this.store.set(key, value);
return true;
}

del(key: string | string[]): number {
const keys = Array.isArray(key) ? key : [key];
let count = 0;
for (const k of keys) {
if (this.store.delete(k)) count++;
}
return count;
}

has(key: string): boolean {
return this.store.has(key);
}

flushAll(): void {
this.store.clear();
}

getStats() {
return { hits: 0, misses: 0, keys: this.store.size, ksize: 0, vsize: 0 };
}

on(event: string, callback: (...args: any[]) => void): this {
const existing = this.listeners.get(event) ?? [];
this.listeners.set(event, [...existing, callback]);
return this;
}
}

export default NodeCache;
29 changes: 11 additions & 18 deletions listener/src/api/archive-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
* GET /api/archive/:id – single archived record by archive PK
* POST /api/archive/run – trigger an on-demand archive cycle (admin)
*
* All endpoints return JSON. The optional `archiveService` parameter is only
* needed for the admin /run endpoint; read-only endpoints only require `store`.
* All endpoints return JSON using the standardised response envelope
* (Issue #385): { success: true, data: … } / { success: false, error: … }
*/
import http from 'http';
import { ArchiveStore } from '../services/archive-store';
import { ArchiveService } from '../services/archive-service';
import logger from '../utils/logger';
import { sendOk, sendErr, ErrorCode } from '../utils/response';

export interface ArchiveApiHandlerDeps {
store: ArchiveStore;
Expand All @@ -36,19 +37,16 @@ export async function handleArchiveRequest(
// POST /api/archive/run – trigger on-demand cycle
if (req.method === 'POST' && pathname === '/api/archive/run') {
if (!deps.service) {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Archive service not enabled' }));
sendErr(res, 503, 'Archive service not enabled', ErrorCode.SERVICE_UNAVAILABLE);
return true;
}
logger.info('Handling POST /api/archive/run', { requestId });
try {
const result = await deps.service.runCycle();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
sendOk(res, 200, result);
} catch (err) {
logger.error('Archive run failed', { error: err, requestId });
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
sendErr(res, 500, (err as Error).message, ErrorCode.INTERNAL_ERROR);
}
return true;
}
Expand All @@ -61,16 +59,13 @@ export async function handleArchiveRequest(
try {
const record = await deps.store.getById(id);
if (!record) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Archived record not found' }));
sendErr(res, 404, 'Archived record not found', ErrorCode.NOT_FOUND);
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(record));
sendOk(res, 200, record);
} catch (err) {
logger.error('Failed to fetch archive record', { error: err, requestId, id });
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
sendErr(res, 500, (err as Error).message, ErrorCode.INTERNAL_ERROR);
}
return true;
}
Expand All @@ -88,12 +83,10 @@ export async function handleArchiveRequest(
endDate: url.searchParams.get('endDate') ?? undefined,
};
const result = await deps.store.query(options);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
sendOk(res, 200, result);
} catch (err) {
logger.error('Failed to query archive', { error: err, requestId });
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
sendErr(res, 500, (err as Error).message, ErrorCode.INTERNAL_ERROR);
}
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion listener/src/api/events-server.health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jest.mock('@stellar/stellar-sdk', () => ({
getHealth: mockGetHealth,
})),
},
}));
}), { virtual: true });

jest.mock('../store/event-registry', () => ({
eventRegistry: { getEvents: jest.fn(() => []), count: jest.fn(() => 0) },
Expand Down
Loading
Loading