diff --git a/stellar-payment-platform/integration.test.js b/stellar-payment-platform/integration.test.js index f8b8a27..e85f5d9 100644 --- a/stellar-payment-platform/integration.test.js +++ b/stellar-payment-platform/integration.test.js @@ -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, }); diff --git a/stellar-payment-platform/jest.setup.js b/stellar-payment-platform/jest.setup.js new file mode 100644 index 0000000..2a0c99a --- /dev/null +++ b/stellar-payment-platform/jest.setup.js @@ -0,0 +1 @@ +require('events').EventEmitter.defaultMaxListeners = 100; diff --git a/stellar-payment-platform/package.json b/stellar-payment-platform/package.json index f5d6572..cd7c7af 100644 --- a/stellar-payment-platform/package.json +++ b/stellar-payment-platform/package.json @@ -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" diff --git a/stellar-payment-platform/src/middleware/errorHandler.js b/stellar-payment-platform/src/middleware/errorHandler.js index cd6cd8f..c1c14ac 100644 --- a/stellar-payment-platform/src/middleware/errorHandler.js +++ b/stellar-payment-platform/src/middleware/errorHandler.js @@ -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( diff --git a/stellar-payment-platform/src/routes/v1/federationRoutes.js b/stellar-payment-platform/src/routes/v1/federationRoutes.js index 772eb8b..eb7c366 100644 --- a/stellar-payment-platform/src/routes/v1/federationRoutes.js +++ b/stellar-payment-platform/src/routes/v1/federationRoutes.js @@ -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) { @@ -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) { @@ -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); diff --git a/stellar-payment-platform/tests/e2e/admin.e2e.test.js b/stellar-payment-platform/tests/e2e/admin.e2e.test.js new file mode 100644 index 0000000..0fc986a --- /dev/null +++ b/stellar-payment-platform/tests/e2e/admin.e2e.test.js @@ -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); + }); +}); diff --git a/stellar-payment-platform/tests/e2e/federation.e2e.test.js b/stellar-payment-platform/tests/e2e/federation.e2e.test.js new file mode 100644 index 0000000..e7ecc91 --- /dev/null +++ b/stellar-payment-platform/tests/e2e/federation.e2e.test.js @@ -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); + }); +}); diff --git a/stellar-payment-platform/tests/e2e/registration.e2e.test.js b/stellar-payment-platform/tests/e2e/registration.e2e.test.js new file mode 100644 index 0000000..52f439b --- /dev/null +++ b/stellar-payment-platform/tests/e2e/registration.e2e.test.js @@ -0,0 +1,129 @@ +'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: { + findUnique: jest.fn(async ({ where }) => { + let row = null; + if (where.username) { + for (const entry of mockDb.values()) { + if (entry.username === where.username) { + row = entry; + break; + } + } + } + return row ? { ...row } : null; + }), + findFirst: jest.fn(async ({ where }) => { + let row = null; + if (where.username) { + for (const entry of mockDb.values()) { + if (entry.username === where.username) { + row = entry; + break; + } + } + } else if (where.address) { + for (const entry of mockDb.values()) { + if (entry.address === where.address) { + row = entry; + break; + } + } + } + return row ? { ...row } : null; + }), + create: jest.fn(async ({ data }) => { + for (const entry of mockDb.values()) { + if (entry.username === data.username || entry.address === data.address) { + const err = new Error('Unique constraint failed'); + err.code = 'P2002'; + throw err; + } + } + 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; + }), + count: jest.fn(async ({ where }) => { + let count = 0; + for (const entry of mockDb.values()) { + if (where.address && entry.address === where.address) count++; + } + return count; + }), + }, + $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'); + +describe('E2E: Registration Flow', () => { + beforeEach(() => { + mockDb.clear(); + }); + + it('should successfully register a new user and handle duplicate registration gracefully', async () => { + // 1. Successful Registration + const validUser = { + username: 'e2eregistertest', + address: 'GABC123XYZ456789REGISTRATION', + }; + + let res = await request(app) + .post('/api/v1/register') + .send(validUser); + + expect(res.status).toBe(201); + expect(res.body.ok).toBe(true); + expect(res.body.username).toContain(validUser.username); + expect(res.body.address).toBe(validUser.address); + + // 2. Duplicate Registration should return 409 + res = await request(app) + .post('/api/v1/register') + .send(validUser); + + expect(res.status).toBe(409); + expect(res.body.error).toBeDefined(); + + // 3. Invalid input (e.g., empty username) + res = await request(app) + .post('/api/v1/register') + .send({ address: 'GDEF456XYZ' }); + + expect(res.status).toBe(422); // Validation error + }); +}); diff --git a/stellar-payment-platform/tests/e2e/webhooks.e2e.test.js b/stellar-payment-platform/tests/e2e/webhooks.e2e.test.js new file mode 100644 index 0000000..a2cef94 --- /dev/null +++ b/stellar-payment-platform/tests/e2e/webhooks.e2e.test.js @@ -0,0 +1,142 @@ +'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) }, + Keypair: { + fromPublicKey: jest.fn(() => ({ + verify: jest.fn(() => true), + })), + }, +})); +jest.mock('pdfkit', () => jest.fn()); + +const mockDbUsers = new Map(); +const mockDbWebhooks = new Map(); + +jest.mock('../../prismaClient', () => { + const prisma = { + user: { + findUnique: jest.fn(async ({ where }) => { + let row = null; + if (where.username) { + for (const entry of mockDbUsers.values()) { + if (entry.username === where.username) { + row = entry; + break; + } + } + } + return row ? { ...row } : null; + }), + }, + webhook: { + create: jest.fn(async ({ data }) => { + const row = { + ...data, + createdAt: data.createdAt || new Date(), + }; + mockDbWebhooks.set(data.id, row); + return row; + }), + findMany: jest.fn(async ({ where, orderBy }) => { + let results = Array.from(mockDbWebhooks.values()); + if (where && where.username) { + results = results.filter(w => w.username === where.username); + } + return results; + }), + deleteMany: jest.fn(async ({ where }) => { + let count = 0; + for (const [id, entry] of mockDbWebhooks.entries()) { + if (entry.id === where.id && entry.username === where.username) { + mockDbWebhooks.delete(id); + count++; + } + } + return { count }; + }), + }, + $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), +})); + +const request = require('supertest'); +const { app } = require('../../server'); + +describe('E2E: Webhooks Flow', () => { + beforeEach(() => { + mockDbUsers.clear(); + mockDbWebhooks.clear(); + + // Set up a user for the webhooks + mockDbUsers.set('GABC123XYZ456789WEBHOOK', { + username: 'webhook_test_user*localhost', + address: 'GABC123XYZ456789WEBHOOK', + }); + }); + + it('should create, list, and delete a webhook', async () => { + // We mock verifyFreighterSignedMessage implicitly by mocking StrKey and Keypair in stellar-sdk. + + // 1. Create Webhook + let res = await request(app) + .post('/api/v1/webhooks') + .send({ + username: 'webhook_test_user', + signature: 'mock_signature', + url: 'https://example.com/webhook', + }); + + if (res.status !== 201) console.log("Webhook Create Error Response:", res.status, res.body); + expect(res.status).toBe(201); + expect(res.body.ok).toBe(true); + expect(res.body.webhook.url).toBe('https://example.com/webhook'); + expect(res.body.webhook.id).toBeDefined(); + + const webhookId = res.body.webhook.id; + + // 2. List Webhooks + res = await request(app) + .get('/api/v1/webhooks') + .send({ + username: 'webhook_test_user', + signature: 'mock_signature', + }); + + expect(res.status).toBe(200); + expect(res.body.webhooks.length).toBe(1); + expect(res.body.webhooks[0].id).toBe(webhookId); + + // 3. Delete Webhook + res = await request(app) + .delete(`/api/v1/webhooks/${webhookId}`) + .send({ + username: 'webhook_test_user', + signature: 'mock_signature', + }); + + expect(res.status).toBe(200); + expect(res.body.deleted).toBe(true); + + // 4. Verify Delete + res = await request(app) + .get('/api/v1/webhooks') + .send({ + username: 'webhook_test_user', + signature: 'mock_signature', + }); + + expect(res.status).toBe(200); + expect(res.body.webhooks.length).toBe(0); + }); +});