diff --git a/e2e/api/auth-agent-profile.spec.ts b/e2e/api/auth-agent-profile.spec.ts new file mode 100644 index 0000000..07f15cf --- /dev/null +++ b/e2e/api/auth-agent-profile.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '../fixtures'; + +test.describe('GET /auth/profile/agent', () => { + test('returns agent profile or null for authenticated user', async ({ authedRequest }) => { + const res = await authedRequest.get('/auth/profile/agent'); + + expect(res.status()).toBe(200); + const body = await res.json(); + // Regular user may not have an agent — null is valid + expect([null, expect.objectContaining({})]).toContainEqual(body); + }); + + test('rejects unauthenticated requests', async ({ request }) => { + const res = await request.get('/auth/profile/agent'); + + expect(res.status()).toBe(401); + }); + + test('rejects requests with invalid token', async ({ request }) => { + const res = await request.get('/auth/profile/agent', { + headers: { Authorization: 'Bearer invalid.jwt.token' }, + }); + + expect(res.status()).toBe(401); + }); +}); diff --git a/e2e/api/auth-kyc.spec.ts b/e2e/api/auth-kyc.spec.ts new file mode 100644 index 0000000..8fe5c11 --- /dev/null +++ b/e2e/api/auth-kyc.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '../fixtures'; + +test.describe('PATCH /auth/kyc — KYC verification (admin only)', () => { + test('rejects unauthenticated KYC update', async ({ request }) => { + const res = await request.patch('/auth/kyc', { + data: { + userId: 'some-user-id', + kycStatus: 'VERIFIED', + }, + }); + + expect(res.status()).toBe(401); + }); + + test('rejects KYC update from non-admin user', async ({ authedRequest }) => { + const res = await authedRequest.patch('/auth/kyc', { + data: { + userId: 'some-user-id', + kycStatus: 'VERIFIED', + }, + }); + + expect(res.ok()).toBeFalsy(); + expect([401, 403]).toContain(res.status()); + }); + + test('rejects KYC update with invalid status', async ({ authedRequest }) => { + const res = await authedRequest.patch('/auth/kyc', { + data: { + userId: 'some-user-id', + kycStatus: 'INVALID_STATUS', + }, + }); + + expect(res.ok()).toBeFalsy(); + expect([400, 401, 403]).toContain(res.status()); + }); +}); diff --git a/e2e/api/listings-media.spec.ts b/e2e/api/listings-media.spec.ts new file mode 100644 index 0000000..70d3049 --- /dev/null +++ b/e2e/api/listings-media.spec.ts @@ -0,0 +1,75 @@ +import { test, expect, registerUser, createListing } from '../fixtures'; +import * as path from 'path'; +import * as fs from 'fs'; + +test.describe('POST /listings/:id/media — Media upload', () => { + let accessToken: string; + let listingId: string; + + test.beforeAll(async ({ request }) => { + const { accessToken: token } = await registerUser(request); + accessToken = token; + + // Create a listing to attach media to + const { listing } = await createListing(request, token); + listingId = listing.id; + }); + + test('rejects unauthenticated media upload', async ({ request }) => { + const res = await request.post(`/listings/${listingId}/media`, { + multipart: { + file: { + name: 'test.jpg', + mimeType: 'image/jpeg', + buffer: Buffer.from('fake-jpeg-data'), + }, + }, + }); + + expect(res.status()).toBe(401); + }); + + test('rejects upload without file', async ({ request }) => { + const res = await request.post(`/listings/${listingId}/media`, { + headers: { Authorization: `Bearer ${accessToken}` }, + multipart: { + caption: 'Missing file', + }, + }); + + expect(res.ok()).toBeFalsy(); + expect([400, 422]).toContain(res.status()); + }); + + test('rejects upload with invalid MIME type', async ({ request }) => { + const res = await request.post(`/listings/${listingId}/media`, { + headers: { Authorization: `Bearer ${accessToken}` }, + multipart: { + file: { + name: 'test.pdf', + mimeType: 'application/pdf', + buffer: Buffer.from('fake-pdf-data'), + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect([400, 415, 422]).toContain(res.status()); + }); + + test('rejects upload for non-existent listing', async ({ request }) => { + const res = await request.post('/listings/non-existent-id/media', { + headers: { Authorization: `Bearer ${accessToken}` }, + multipart: { + file: { + name: 'test.jpg', + mimeType: 'image/jpeg', + buffer: Buffer.from('fake-jpeg-data'), + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect([400, 404]).toContain(res.status()); + }); +}); diff --git a/e2e/api/listings-moderate.spec.ts b/e2e/api/listings-moderate.spec.ts new file mode 100644 index 0000000..5438033 --- /dev/null +++ b/e2e/api/listings-moderate.spec.ts @@ -0,0 +1,66 @@ +import { test, expect, registerUser, createListing } from '../fixtures'; + +test.describe('PATCH /listings/:id/moderate — Listing moderation (admin only)', () => { + let accessToken: string; + let listingId: string; + + test.beforeAll(async ({ request }) => { + const { accessToken: token } = await registerUser(request); + accessToken = token; + + const { listing } = await createListing(request, token); + listingId = listing.id; + }); + + test('rejects unauthenticated moderation', async ({ request }) => { + const res = await request.patch(`/listings/${listingId}/moderate`, { + data: { + action: 'approve', + moderationScore: 95, + notes: 'Looks good', + }, + }); + + expect(res.status()).toBe(401); + }); + + test('rejects moderation from non-admin user', async ({ request }) => { + const res = await request.patch(`/listings/${listingId}/moderate`, { + data: { + action: 'approve', + moderationScore: 95, + notes: 'Attempted non-admin moderation', + }, + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + expect(res.ok()).toBeFalsy(); + expect([401, 403]).toContain(res.status()); + }); + + test('rejects moderation with invalid action', async ({ request }) => { + const res = await request.patch(`/listings/${listingId}/moderate`, { + data: { + action: 'INVALID_ACTION', + notes: 'Invalid action test', + }, + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + expect(res.ok()).toBeFalsy(); + expect([400, 401, 403]).toContain(res.status()); + }); + + test('rejects moderation for non-existent listing', async ({ request }) => { + const res = await request.patch('/listings/non-existent-id/moderate', { + data: { + action: 'reject', + notes: 'Non-existent listing', + }, + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + expect(res.ok()).toBeFalsy(); + expect([400, 401, 403, 404]).toContain(res.status()); + }); +}); diff --git a/e2e/api/payments-callback.spec.ts b/e2e/api/payments-callback.spec.ts new file mode 100644 index 0000000..9a9d54f --- /dev/null +++ b/e2e/api/payments-callback.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '../fixtures'; + +test.describe('POST /payments/callback/:provider — Payment webhooks', () => { + test.describe('VNPay callback', () => { + test('handles VNPay callback with query params', async ({ request }) => { + const res = await request.post('/payments/callback/vnpay', { + params: { + vnp_TxnRef: 'TEST_TXN_001', + vnp_ResponseCode: '00', + vnp_Amount: '50000000', // VNPay uses x100 + vnp_TransactionNo: '14000000', + vnp_SecureHash: 'invalid_hash_for_test', + }, + }); + + // Callback endpoint should not crash — expect a handled response + // May return 200 (processed) or 400 (invalid hash) but never 500 + expect(res.status()).toBeLessThan(500); + }); + + test('handles VNPay callback with failed transaction code', async ({ request }) => { + const res = await request.post('/payments/callback/vnpay', { + params: { + vnp_TxnRef: 'TEST_TXN_002', + vnp_ResponseCode: '24', // Customer cancelled + vnp_Amount: '50000000', + vnp_TransactionNo: '0', + vnp_SecureHash: 'invalid_hash_for_test', + }, + }); + + expect(res.status()).toBeLessThan(500); + }); + }); + + test.describe('MoMo callback', () => { + test('handles MoMo callback with body payload', async ({ request }) => { + const res = await request.post('/payments/callback/momo', { + data: { + orderId: 'TEST_ORDER_001', + resultCode: 0, + amount: 500000, + transId: 'MOMO_TXN_001', + signature: 'invalid_signature_for_test', + }, + }); + + expect(res.status()).toBeLessThan(500); + }); + }); + + test.describe('ZaloPay callback', () => { + test('handles ZaloPay callback with body payload', async ({ request }) => { + const res = await request.post('/payments/callback/zalopay', { + data: { + data: '{"app_trans_id":"TEST_001","amount":500000}', + mac: 'invalid_mac_for_test', + type: 1, + }, + }); + + expect(res.status()).toBeLessThan(500); + }); + }); + + test.describe('Invalid provider', () => { + test('rejects callback for unknown provider', async ({ request }) => { + const res = await request.post('/payments/callback/unknown_provider', { + data: { txn: 'test' }, + }); + + expect(res.ok()).toBeFalsy(); + expect([400, 404]).toContain(res.status()); + }); + }); +});