test(e2e): add comprehensive E2E tests for listings, search, payments, subscriptions, admin
Expand Playwright E2E test coverage from 17 to 86 tests covering: - Listings CRUD (create, search, filter, detail, status update) - Search (text search, geo search, validation, Typesense fallback) - Payments (create, list transactions, auth guards) - Subscriptions (plans, create, quota, billing, usage metering) - Admin authorization guards (all endpoints reject non-admin users) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
188
e2e/api/listings.spec.ts
Normal file
188
e2e/api/listings.spec.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { test, expect, registerUser, createTestUser } from '../fixtures';
|
||||
import { createTestListing, createListing } from '../fixtures/listings.fixture';
|
||||
|
||||
test.describe('Listings API', () => {
|
||||
let accessToken: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const { accessToken: token } = await registerUser(request);
|
||||
accessToken = token;
|
||||
});
|
||||
|
||||
test.describe('POST /listings — Create listing', () => {
|
||||
test('creates a listing with valid data', async ({ request }) => {
|
||||
const data = createTestListing();
|
||||
const res = await request.post('/listings', {
|
||||
data,
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
expect(res.status()).toBe(201);
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('id');
|
||||
expect(body.title).toBe(data.title);
|
||||
expect(body.propertyType).toBe('APARTMENT');
|
||||
expect(body.transactionType).toBe('SALE');
|
||||
expect(body.city).toBe('Hồ Chí Minh');
|
||||
});
|
||||
|
||||
test('rejects listing with missing required fields', async ({ request }) => {
|
||||
const res = await request.post('/listings', {
|
||||
data: { title: 'Incomplete' },
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(400);
|
||||
});
|
||||
|
||||
test('rejects listing with invalid property type', async ({ request }) => {
|
||||
const data = createTestListing({ propertyType: 'INVALID_TYPE' });
|
||||
const res = await request.post('/listings', {
|
||||
data,
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(400);
|
||||
});
|
||||
|
||||
test('rejects unauthenticated request', async ({ request }) => {
|
||||
const res = await request.post('/listings', {
|
||||
data: createTestListing(),
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('creates a RENT listing', async ({ request }) => {
|
||||
const data = createTestListing({
|
||||
transactionType: 'RENT',
|
||||
rentPriceMonthly: 15000000,
|
||||
});
|
||||
const res = await request.post('/listings', {
|
||||
data,
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
expect(res.status()).toBe(201);
|
||||
const body = await res.json();
|
||||
expect(body.transactionType).toBe('RENT');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('GET /listings — Search listings', () => {
|
||||
test('returns paginated listing results', async ({ request }) => {
|
||||
const res = await request.get('/listings');
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('data');
|
||||
expect(Array.isArray(body.data)).toBeTruthy();
|
||||
expect(body).toHaveProperty('total');
|
||||
});
|
||||
|
||||
test('filters by property type', async ({ request }) => {
|
||||
const res = await request.get('/listings', {
|
||||
params: { propertyType: 'APARTMENT' },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const body = await res.json();
|
||||
for (const listing of body.data) {
|
||||
expect(listing.propertyType).toBe('APARTMENT');
|
||||
}
|
||||
});
|
||||
|
||||
test('filters by transaction type', async ({ request }) => {
|
||||
const res = await request.get('/listings', {
|
||||
params: { transactionType: 'SALE' },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const body = await res.json();
|
||||
for (const listing of body.data) {
|
||||
expect(listing.transactionType).toBe('SALE');
|
||||
}
|
||||
});
|
||||
|
||||
test('filters by city', async ({ request }) => {
|
||||
const res = await request.get('/listings', {
|
||||
params: { city: 'Hồ Chí Minh' },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('paginates correctly', async ({ request }) => {
|
||||
const res = await request.get('/listings', {
|
||||
params: { page: 1, limit: 2 },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const body = await res.json();
|
||||
expect(body.data.length).toBeLessThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('GET /listings/:id — Get listing detail', () => {
|
||||
test('returns listing by id', async ({ request }) => {
|
||||
// First create a listing
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.get(`/listings/${listing.id}`);
|
||||
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.id).toBe(listing.id);
|
||||
expect(body).toHaveProperty('title');
|
||||
expect(body).toHaveProperty('address');
|
||||
expect(body).toHaveProperty('latitude');
|
||||
expect(body).toHaveProperty('longitude');
|
||||
});
|
||||
|
||||
test('returns 404 for non-existent listing', async ({ request }) => {
|
||||
const res = await request.get('/listings/non-existent-id-12345');
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect([404, 400]).toContain(res.status());
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PATCH /listings/:id/status — Update listing status', () => {
|
||||
test('updates listing status', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.patch(`/listings/${listing.id}/status`, {
|
||||
data: { status: 'ACTIVE' },
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
// May succeed or fail depending on business rules (e.g. moderation required)
|
||||
expect([200, 400, 403]).toContain(res.status());
|
||||
});
|
||||
|
||||
test('rejects invalid status value', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.patch(`/listings/${listing.id}/status`, {
|
||||
data: { status: 'INVALID_STATUS' },
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(400);
|
||||
});
|
||||
|
||||
test('rejects unauthenticated status update', async ({ request }) => {
|
||||
const { listing } = await createListing(request, accessToken);
|
||||
|
||||
const res = await request.patch(`/listings/${listing.id}/status`, {
|
||||
data: { status: 'ACTIVE' },
|
||||
});
|
||||
|
||||
expect(res.status()).toBe(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user