test(e2e): add coverage for agent profile, KYC, payment callbacks, media upload, and listing moderation
Fills coverage gaps for untested API endpoints: - GET /auth/profile/agent (auth + unauth) - PATCH /auth/kyc (admin-only guard tests) - POST /payments/callback/:provider (VNPay, MoMo, ZaloPay webhooks) - POST /listings/:id/media (multipart upload validation) - PATCH /listings/:id/moderate (admin-only moderation) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
26
e2e/api/auth-agent-profile.spec.ts
Normal file
26
e2e/api/auth-agent-profile.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
38
e2e/api/auth-kyc.spec.ts
Normal file
38
e2e/api/auth-kyc.spec.ts
Normal file
@@ -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());
|
||||
});
|
||||
});
|
||||
75
e2e/api/listings-media.spec.ts
Normal file
75
e2e/api/listings-media.spec.ts
Normal file
@@ -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());
|
||||
});
|
||||
});
|
||||
66
e2e/api/listings-moderate.spec.ts
Normal file
66
e2e/api/listings-moderate.spec.ts
Normal file
@@ -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());
|
||||
});
|
||||
});
|
||||
76
e2e/api/payments-callback.spec.ts
Normal file
76
e2e/api/payments-callback.spec.ts
Normal file
@@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user