Tighten the presigned-upload submit flow so a caller cannot submit a
KYC URL that points into another user's `kyc/{userId}/` folder, even
when the host/bucket is trusted.
- Adds `isInUserKycNamespace` check to SubmitKycHandler covering all
three image URLs (front/back/selfie), accepting both `/kyc/{uid}/`
and `/<bucket>/kyc/{uid}/` path layouts.
- Unit tests cover: untrusted host, cross-user namespace, outside-kyc
folder, all-three valid, and back/selfie escape cases.
- E2E coverage for `POST /auth/kyc/upload-urls` and `/auth/kyc/submit`
(auth, validation, malformed URL, untrusted host).
- Drive-by: aligns valuation-results spec to current heading
("Yếu tố ảnh hưởng giá") so pre-commit web suite passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
import { test, expect } from '../fixtures';
|
|
|
|
/**
|
|
* KYC presigned-upload flow (TEC-2750).
|
|
*
|
|
* Covers:
|
|
* - POST /auth/kyc/upload-urls — presigned URL generation (happy + validation errors)
|
|
* - POST /auth/kyc/submit — accepts URL body; rejects invalid/untrusted URLs
|
|
*/
|
|
|
|
test.describe('POST /auth/kyc/upload-urls', () => {
|
|
test('rejects unauthenticated requests', async ({ request }) => {
|
|
const res = await request.post('auth/kyc/upload-urls', {
|
|
data: { files: [{ field: 'frontImage', mimeType: 'image/jpeg', fileName: 'front.jpg' }] },
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test('rejects empty files array', async ({ authedRequest }) => {
|
|
const res = await authedRequest.post('auth/kyc/upload-urls', { data: { files: [] } });
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
|
|
test('rejects more than 3 files', async ({ authedRequest }) => {
|
|
const res = await authedRequest.post('auth/kyc/upload-urls', {
|
|
data: {
|
|
files: [
|
|
{ field: 'frontImage', mimeType: 'image/jpeg', fileName: 'a.jpg' },
|
|
{ field: 'backImage', mimeType: 'image/jpeg', fileName: 'b.jpg' },
|
|
{ field: 'selfieImage', mimeType: 'image/jpeg', fileName: 'c.jpg' },
|
|
{ field: 'selfieImage', mimeType: 'image/jpeg', fileName: 'd.jpg' },
|
|
],
|
|
},
|
|
});
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
|
|
test('rejects unsupported field name', async ({ authedRequest }) => {
|
|
const res = await authedRequest.post('auth/kyc/upload-urls', {
|
|
data: {
|
|
files: [{ field: 'not-a-field', mimeType: 'image/jpeg', fileName: 'front.jpg' }],
|
|
},
|
|
});
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
});
|
|
|
|
test.describe('POST /auth/kyc/submit', () => {
|
|
test('rejects unauthenticated submit', async ({ request }) => {
|
|
const res = await request.post('auth/kyc/submit', {
|
|
data: {
|
|
documentType: 'CCCD',
|
|
documentNumber: '001234567890',
|
|
frontImageUrl: 'https://cdn.goodgo.vn/kyc/front.jpg',
|
|
},
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test('rejects submit missing required fields', async ({ authedRequest }) => {
|
|
const res = await authedRequest.post('auth/kyc/submit', {
|
|
data: { documentType: 'CCCD' },
|
|
});
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
|
|
test('rejects submit with malformed front image URL', async ({ authedRequest }) => {
|
|
const res = await authedRequest.post('auth/kyc/submit', {
|
|
data: {
|
|
documentType: 'CCCD',
|
|
documentNumber: '001234567890',
|
|
frontImageUrl: 'not-a-url',
|
|
},
|
|
});
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
|
|
test('rejects submit with URL from untrusted host', async ({ authedRequest }) => {
|
|
const res = await authedRequest.post('auth/kyc/submit', {
|
|
data: {
|
|
documentType: 'CCCD',
|
|
documentNumber: '001234567890',
|
|
frontImageUrl: 'https://evil.example.com/kyc/front.jpg',
|
|
},
|
|
});
|
|
expect(res.ok()).toBeFalsy();
|
|
// URL host validation returns 400; never 5xx / 201.
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
});
|