diff --git a/payment_router/src/lib.rs b/payment_router/src/lib.rs index cfab1027..e2d2c1a3 100644 --- a/payment_router/src/lib.rs +++ b/payment_router/src/lib.rs @@ -319,9 +319,7 @@ impl PaymentRouter { .get(&DataKey::TimelockNonce) .unwrap_or(0u64); let next = current + 1; - env.storage() - .instance() - .set(&DataKey::TimelockNonce, &next); + env.storage().instance().set(&DataKey::TimelockNonce, &next); next } @@ -509,9 +507,7 @@ impl PaymentRouter { .set(&DataKey::MaxAmount, &max_amount); env.storage().instance().set(&DataKey::Paused, &false); env.storage().instance().set(&DataKey::Frozen, &false); - env.storage() - .instance() - .set(&DataKey::TimelockNonce, &0u64); + env.storage().instance().set(&DataKey::TimelockNonce, &0u64); env.storage().instance().extend_ttl( Self::INSTANCE_LIFETIME_THRESHOLD, Self::INSTANCE_BUMP_AMOUNT, @@ -548,7 +544,10 @@ impl PaymentRouter { let nonce = Self::next_nonce(&env); let queued_at = env.ledger().timestamp(); - let entry = TimelockEntry { queued_at, action: action.clone() }; + let entry = TimelockEntry { + queued_at, + action: action.clone(), + }; let key = DataKey::TimelockEntry(nonce); env.storage().persistent().set(&key, &entry); @@ -626,17 +625,13 @@ impl PaymentRouter { env.storage().instance().set(&DataKey::FeeCap, &fee_cap); } ActionType::SetFeeBps(new_fee_bps) => { - env.storage() - .instance() - .set(&DataKey::FeeBps, &new_fee_bps); + env.storage().instance().set(&DataKey::FeeBps, &new_fee_bps); } ActionType::SetGovernance(gov) => { env.storage().instance().set(&DataKey::Governance, &gov); } ActionType::SetMinLimit(min_limit) => { - env.storage() - .instance() - .set(&DataKey::MinLimit, &min_limit); + env.storage().instance().set(&DataKey::MinLimit, &min_limit); } ActionType::TransferAdmin(new_admin) => { env.storage().instance().set(&DataKey::Admin, &new_admin); @@ -651,10 +646,8 @@ impl PaymentRouter { Self::INSTANCE_BUMP_AMOUNT, ); - env.events().publish( - (Symbol::new(&env, "action_executed"), admin), - nonce, - ); + env.events() + .publish((Symbol::new(&env, "action_executed"), admin), nonce); log!(&env, "Timelock action executed for nonce {}", nonce); Ok(()) @@ -678,10 +671,8 @@ impl PaymentRouter { env.storage().persistent().remove(&key); - env.events().publish( - (Symbol::new(&env, "action_cancelled"), admin), - nonce, - ); + env.events() + .publish((Symbol::new(&env, "action_cancelled"), admin), nonce); log!(&env, "Timelock action cancelled for nonce {}", nonce); Ok(()) 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 00000000..d22d1dbd --- /dev/null +++ b/stellar-payment-platform/tests/e2e/admin.e2e.test.js @@ -0,0 +1,96 @@ +'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; + }), + findMany: jest.fn(async () => { + return Array.from(mockDbUsers.values()); + }), + count: jest.fn(async () => { + return mockDbUsers.size; + }) + }, + $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 Users + res = await request(app) + .get('/api/v1/admin/export?format=json') + .set('x-api-key', 'e2e-admin-key'); + + expect(res.status).toBe(200); + expect(res.body.data).toBeDefined(); + expect(Array.isArray(res.body.data)).toBe(true); + expect(res.body.data.length).toBe(1); + expect(res.body.data[0].address).toBe('GABC123XYZ456789ADMIN'); + expect(res.body.data[0].flaggedAt).not.toBeNull(); + }); + + 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 00000000..18b2805c --- /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: 'federation_test*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 00000000..3b9c3c91 --- /dev/null +++ b/stellar-payment-platform/tests/e2e/registration.e2e.test.js @@ -0,0 +1,122 @@ +'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; + }), + }, + $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: 'e2e_register_test', + 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 00000000..635d15fa --- /dev/null +++ b/stellar-payment-platform/tests/e2e/webhooks.e2e.test.js @@ -0,0 +1,141 @@ +'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', + 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', + }); + + 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); + }); +});