feat(listings): complete PATCH /api/v1/listings/:id endpoint
- Add mediaOrder field to UpdateListingDto, Command, and Handler for reordering media items - Add updateMediaOrder method to IPropertyRepository and Prisma impl - Fix PrismaPropertyRepository.update() to persist amenities, nearbyPOIs, floors, floor, totalFloors, and metroDistanceM columns - Add unit tests for media order updates in handler spec - Add DTO validation tests for mediaOrder with nested validation - Add e2e integration tests covering content updates, auth, ownership guard, and forbidden field rejection Existing guards enforced: - Only seller or assigned agent can update (403 for others) - ACTIVE listings transition to PENDING_REVIEW on edit - propertyType, address, location blocked via DTO whitelist Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -146,6 +146,112 @@ test.describe('Listings API', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PATCH /listings/:id — Update listing content', () => {
|
||||
test('updates title and description', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.patch(`listings/${listing.listingId}`, {
|
||||
data: {
|
||||
title: 'Tiêu đề cập nhật qua E2E test',
|
||||
description: 'Mô tả chi tiết cập nhật qua E2E test cho căn hộ',
|
||||
},
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.listingId).toBe(listing.listingId);
|
||||
expect(body.updatedFields).toContain('title');
|
||||
expect(body.updatedFields).toContain('description');
|
||||
});
|
||||
|
||||
test('updates price', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.patch(`listings/${listing.listingId}`, {
|
||||
data: { priceVND: '6000000000' },
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.updatedFields).toContain('priceVND');
|
||||
});
|
||||
|
||||
test('updates amenities', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.patch(`listings/${listing.listingId}`, {
|
||||
data: { amenities: ['Hồ bơi', 'Gym', 'Sân tennis'] },
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.updatedFields).toContain('amenities');
|
||||
});
|
||||
|
||||
test('rejects update with no fields', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.patch(`listings/${listing.listingId}`, {
|
||||
data: {},
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(400);
|
||||
});
|
||||
|
||||
test('rejects unauthenticated update', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.patch(`listings/${listing.listingId}`, {
|
||||
data: { title: 'Unauthorized update attempt' },
|
||||
});
|
||||
|
||||
expect(res.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('rejects update from non-owner', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
// Register a different user
|
||||
const { accessToken: otherToken } = await registerUser(request);
|
||||
|
||||
const res = await request.patch(`listings/${listing.listingId}`, {
|
||||
data: { title: 'Hacker update attempt here' },
|
||||
headers: { Authorization: `Bearer ${otherToken}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(403);
|
||||
});
|
||||
|
||||
test('rejects forbidden fields (propertyType, address) via whitelist', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.patch(`listings/${listing.listingId}`, {
|
||||
data: {
|
||||
title: 'Valid title for update',
|
||||
propertyType: 'VILLA',
|
||||
address: '456 New Street',
|
||||
},
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
// With whitelist validation, forbidden fields should cause 400
|
||||
// OR be silently stripped — either way propertyType should NOT change
|
||||
if (res.ok()) {
|
||||
const body = await res.json();
|
||||
expect(body.updatedFields).not.toContain('propertyType');
|
||||
expect(body.updatedFields).not.toContain('address');
|
||||
} else {
|
||||
expect(res.status()).toBe(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PATCH /listings/:id/status — Update listing status', () => {
|
||||
test('updates listing status', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
Reference in New Issue
Block a user