Skip to content
Merged
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
2 changes: 1 addition & 1 deletion stellar-payment-platform/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ describe('API Integration Lifecycle Suite', () => {

expect(fedRes.status).toBe(200);
expect(fedRes.body).toMatchObject({
stellar_address: user1.address,
stellar_address: user1.federationTag,
account_id: user1.address,
});

Expand Down
1 change: 1 addition & 0 deletions stellar-payment-platform/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('events').EventEmitter.defaultMaxListeners = 100;
3 changes: 2 additions & 1 deletion stellar-payment-platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"contract:upgrade": "node ../scripts/deploy.js upgrade"
},
"jest": {
"testEnvironment": "node"
"testEnvironment": "node",
"setupFiles": ["./jest.setup.js"]
},
"prisma": {
"seed": "node scripts/seed.js"
Expand Down
4 changes: 3 additions & 1 deletion stellar-payment-platform/src/middleware/errorHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ const buildErrorHandler = (isPrismaConnectionError) =>

if (statusCode >= 500) {
const referenceId = crypto.randomUUID();
console.warn(`[Correlation ID: ${req.correlationId}] [Error ID: ${referenceId}]`, err);
if (process.env.NODE_ENV !== 'test') {
console.warn(`[Correlation ID: ${req.correlationId}] [Error ID: ${referenceId}]`, err);
}
logger.error(`[Correlation ID: ${req.correlationId}] [Error ID: ${referenceId}]`, err);

return res.status(statusCode).json(
Expand Down
9 changes: 6 additions & 3 deletions stellar-payment-platform/src/routes/v1/federationRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ module.exports = (redisClient) => {

if (!row) return null;

const domain = process.env.DOMAIN || 'localhost';
const stellar_address = row.username.includes('*') ? row.username : `${row.username}*${domain}`;
const response = {
stellar_address: `${row.username}*${process.env.DOMAIN || 'localhost'}`,
stellar_address,
account_id: row.address,
};
if (row.memoType) {
Expand All @@ -58,14 +60,14 @@ module.exports = (redisClient) => {
const cached = await federationLookupCached(cacheKey, async () => {
const row = await prisma.user.findFirst({
where: { username: queryName, deletedAt: null },
select: { address: true, memoType: true, memo: true },
select: { username: true, address: true, memoType: true, memo: true },
});

const address = row?.address || USER_DATABASE[queryName];
if (!address) return null;

const response = {
stellar_address: address,
stellar_address: queryName,
account_id: address,
};
if (row?.memoType) {
Expand All @@ -88,6 +90,7 @@ module.exports = (redisClient) => {
);
}
} catch (error) {
console.log("FEDERATION LOOKUP ERROR:", error);
const dbError = new Error('Database lookup failed', { cause: error });
dbError.statusCode = 500;
return next(dbError);
Expand Down
108 changes: 108 additions & 0 deletions stellar-payment-platform/tests/e2e/admin.e2e.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use strict';

process.env.ADMIN_API_KEY = 'e2e-admin-key';

jest.mock('../../src/cleanup-cron', () => ({ scheduleCleanupJob: jest.fn() }));
jest.mock('../../src/soft-delete-purge-cron', () => ({ scheduleSoftDeletePurgeJob: jest.fn() }));
jest.mock('@stellar/stellar-sdk', () => ({
Horizon: { Server: jest.fn() },
StrKey: { isValidEd25519PublicKey: jest.fn(() => true) },
}));
jest.mock('pdfkit', () => jest.fn());

const mockDbUsers = new Map();

jest.mock('../../prismaClient', () => {
const prisma = {
user: {
findFirst: jest.fn(async ({ where }) => {
let row = null;
if (where.address) {
for (const entry of mockDbUsers.values()) {
if (entry.address === where.address) {
row = entry;
break;
}
}
}
return row ? { ...row } : null;
}),
update: jest.fn(async ({ where, data }) => {
const entry = mockDbUsers.get(where.address);
if (!entry) throw new Error('Not found');
const updated = { ...entry, ...data };
mockDbUsers.set(where.address, updated);
return updated;
}),
updateMany: jest.fn(async ({ where, data }) => {
let count = 0;
for (const [address, entry] of mockDbUsers.entries()) {
if (where.address && entry.address === where.address) {
mockDbUsers.set(address, { ...entry, ...data });
count++;
}
}
return { count };
}),
findMany: jest.fn(async () => {
return Array.from(mockDbUsers.values());
}),
count: jest.fn(async () => {
return mockDbUsers.size;
})
},
payment: {
findMany: jest.fn().mockResolvedValue([{ id: 'payment_1' }])
},
$transaction: jest.fn(async (ops) => Promise.all(ops)),
$disconnect: jest.fn().mockResolvedValue(undefined),
};
return { prisma, isPrismaConnectionError: () => false };
});

const request = require('supertest');
const { app } = require('../../server');

describe('E2E: Admin Flow', () => {
beforeEach(() => {
mockDbUsers.clear();
mockDbUsers.set('GABC123XYZ456789ADMIN', {
username: 'admin_test_user',
address: 'GABC123XYZ456789ADMIN',
flaggedAt: null,
createdAt: new Date(),
});
});

it('should allow admin to block a user and export users', async () => {
// 1. Block User
let res = await request(app)
.post('/api/v1/admin/block')
.set('x-api-key', 'e2e-admin-key')
.send({ address: 'GABC123XYZ456789ADMIN' });

expect(res.status).toBe(200);
expect(res.body.address).toBe('GABC123XYZ456789ADMIN');
expect(res.body.flaggedAt).toBeDefined();

// 2. Export Payments
res = await request(app)
.get('/api/v1/admin/export?format=json')
.set('x-api-key', 'e2e-admin-key');

expect(res.status).toBe(200);
const lines = res.text.trim().split('\n').filter(Boolean);
expect(lines.length).toBeGreaterThan(0);
const parsed = JSON.parse(lines[0]);
expect(parsed.id).toBe('payment_1');
});

it('should return 401 for unauthorized admin access', async () => {
const res = await request(app)
.post('/api/v1/admin/block')
.set('x-api-key', 'wrong-key')
.send({ address: 'GABC123XYZ456789ADMIN' });

expect(res.status).toBe(401);
});
});
102 changes: 102 additions & 0 deletions stellar-payment-platform/tests/e2e/federation.e2e.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

jest.mock('../../src/cleanup-cron', () => ({ scheduleCleanupJob: jest.fn() }));
jest.mock('../../src/soft-delete-purge-cron', () => ({ scheduleSoftDeletePurgeJob: jest.fn() }));
jest.mock('@stellar/stellar-sdk', () => ({
Horizon: { Server: jest.fn() },
StrKey: { isValidEd25519PublicKey: jest.fn(() => true) },
}));
jest.mock('pdfkit', () => jest.fn());

const mockDb = new Map();

jest.mock('../../prismaClient', () => {
const prisma = {
user: {
findFirst: jest.fn(async ({ where }) => {
let row = null;
if (where.username) {
const u = typeof where.username === 'string' ? where.username : where.username.equals;
for (const entry of mockDb.values()) {
if (entry.username === u) {
row = entry;
break;
}
}
} else if (where.address) {
const a = typeof where.address === 'string' ? where.address : where.address.equals;
for (const entry of mockDb.values()) {
if (entry.address.toLowerCase() === a.toLowerCase()) {
row = entry;
break;
}
}
}
return row ? { ...row } : null;
}),
create: jest.fn(async ({ data }) => {
const row = {
username: data.username,
address: data.address,
memoType: data.memoType || null,
memo: data.memo || null,
createdAt: new Date(),
};
mockDb.set(data.address, row);
return row;
}),
},
$transaction: jest.fn(async (ops) => Promise.all(ops)),
$disconnect: jest.fn().mockResolvedValue(undefined),
};
return { prisma, isPrismaConnectionError: () => false };
});

jest.mock('../../src/multisigner-verifier', () => ({
verifyMultiSignerThreshold: jest.fn().mockResolvedValue({ success: true }),
isSingleSignerAccount: jest.fn().mockReturnValue(true),
}));

jest.mock('bad-words', () => {
return jest.fn().mockImplementation(() => ({
isProfane: jest.fn(() => false),
}));
});

const request = require('supertest');
const { app } = require('../../server');
const { prisma } = require('../../prismaClient');

describe('E2E: Federation Flow', () => {
beforeEach(() => {
mockDb.clear();
});

it('should successfully lookup a user by name and ID', async () => {
const validUser = {
username: 'federationtest*localhost',
address: 'GABC123XYZ456789FEDERATION',
};
mockDb.set(validUser.address, validUser);

// 1. Lookup by Name (default behavior)
let res = await request(app)
.get(`/api/v1/federation?q=${encodeURIComponent(validUser.username)}&type=name`);

expect(res.status).toBe(200);
expect(res.body.account_id).toBe(validUser.address);

// 2. Lookup by ID
res = await request(app)
.get(`/api/v1/federation?q=${validUser.address}&type=id`);

expect(res.status).toBe(200);
expect(res.body.stellar_address).toBe(validUser.username);

// 3. Not Found
res = await request(app)
.get(`/api/v1/federation?q=nonexistent*localhost&type=name`);

expect(res.status).toBe(404);
});
});
Loading
Loading