chore: organize docs — move 37 files from root into docs/ subfolders
Root now contains only essential files: README.md, CLAUDE.md, CHANGELOG.md, CONTRIBUTING.md Reorganized into: docs/audits/ — all audit reports & checklists (71 files) docs/architecture/ — codebase overview, implementation plan docs/guides/ — auth guide, implementation checklist docs/load-testing/ — k6 load test guides & endpoints docs/security/ — payment & security reviews Also removed 5 untracked debug/investigation files and cleaned up playwright-report/ & test-results/ artifacts. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
342
docs/load-testing/K6_ENDPOINTS_SUMMARY.md
Normal file
342
docs/load-testing/K6_ENDPOINTS_SUMMARY.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# GoodGo Platform — K6 Load Testing Endpoints Summary
|
||||
|
||||
Quick reference for all testable API endpoints.
|
||||
|
||||
## 📍 Base URL
|
||||
```
|
||||
http://localhost:3001/api/v1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication (Auth Module)
|
||||
|
||||
### Public Endpoints (No Auth Required)
|
||||
|
||||
| Method | Path | Rate Limit | Purpose |
|
||||
|--------|------|-----------|---------|
|
||||
| **POST** | `/auth/register` | 5/hour | Register new user with phone/password/name |
|
||||
| **POST** | `/auth/login` | 5/hour | Login with phone/password (basic auth) |
|
||||
| **POST** | `/auth/refresh` | 5/hour | Refresh expired access token |
|
||||
| **POST** | `/auth/logout` | None | Clear auth cookies |
|
||||
| **POST** | `/auth/exchange-token` | None | Exchange OAuth tokens for httpOnly cookies |
|
||||
|
||||
### Protected Endpoints (JWT Required)
|
||||
|
||||
| Method | Path | Rate Limit | Purpose |
|
||||
|--------|------|-----------|---------|
|
||||
| **GET** | `/auth/profile` | None | Get authenticated user profile |
|
||||
| **GET** | `/auth/profile/agent` | None | Get agent profile (if user is agent) |
|
||||
|
||||
### Admin Only
|
||||
|
||||
| Method | Path | Rate Limit | Auth | Purpose |
|
||||
|--------|------|-----------|------|---------|
|
||||
| **PATCH** | `/auth/kyc` | None | JWT+Admin | Verify/update user KYC status |
|
||||
|
||||
---
|
||||
|
||||
## 🏠 Listings (Listings Module)
|
||||
|
||||
### Public Endpoints
|
||||
|
||||
| Method | Path | Rate Limit | Purpose |
|
||||
|--------|------|-----------|---------|
|
||||
| **GET** | `/listings` | None | Search/filter listings (queryable) |
|
||||
| **GET** | `/listings/:id` | None | Get listing detail by ID |
|
||||
|
||||
### Protected Endpoints (JWT Required)
|
||||
|
||||
| Method | Path | Quota Gate | Purpose |
|
||||
|--------|------|-----------|---------|
|
||||
| **POST** | `/listings` | Yes | Create new property listing |
|
||||
| **PATCH** | `/listings/:id/status` | No | Update listing status (owner only) |
|
||||
| **POST** | `/listings/:id/media` | No | Upload photo/video for listing (owner only) |
|
||||
|
||||
### Admin Only
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| **GET** | `/listings/pending` | Get listings awaiting moderation (paginated) |
|
||||
| **PATCH** | `/listings/:id/moderate` | Approve/reject listing & score it |
|
||||
|
||||
---
|
||||
|
||||
## 💳 Payments (Payments Module)
|
||||
|
||||
### Protected Endpoints (JWT Required)
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| **POST** | `/payments` | Create payment (initiates payment flow) |
|
||||
| **GET** | `/payments` | List user's transactions (paginated) |
|
||||
| **GET** | `/payments/:id` | Get payment status by ID |
|
||||
|
||||
### Admin Only
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| **POST** | `/payments/:id/refund` | Initiate refund for payment |
|
||||
|
||||
### Webhook (Unthrottled, No Auth)
|
||||
|
||||
| Method | Path | Rate Limit | Purpose |
|
||||
|--------|------|-----------|---------|
|
||||
| **POST** | `/payments/callback/:provider` | 20/min | Handle payment provider callbacks (VNPay, MoMo, ZaloPay) |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Search (Search Module)
|
||||
|
||||
### Public Endpoints
|
||||
|
||||
| Method | Path | Purpose | Query Params |
|
||||
|--------|------|---------|--------------|
|
||||
| **GET** | `/search` | Full-text search listings | q, propertyType, transactionType, priceMin/Max, areaMin/Max, bedrooms, district, city, sortBy, page, perPage |
|
||||
| **GET** | `/search/geo` | Geographic radius search | lat, lng, radiusKm, propertyType, transactionType, priceMin/Max, sortBy, page, perPage |
|
||||
|
||||
### Admin Only
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| **POST** | `/search/reindex` | Reindex all properties in search engine |
|
||||
|
||||
---
|
||||
|
||||
## 📊 Key Data Shapes
|
||||
|
||||
### User Registration
|
||||
```json
|
||||
POST /auth/register
|
||||
{
|
||||
"phone": "0901234567",
|
||||
"password": "SecurePass123!",
|
||||
"fullName": "Nguyen Van A",
|
||||
"email": "user@example.com" // optional
|
||||
}
|
||||
```
|
||||
|
||||
### User Login
|
||||
```json
|
||||
POST /auth/login
|
||||
{
|
||||
"phone": "0901234567",
|
||||
"password": "SecurePass123!"
|
||||
}
|
||||
```
|
||||
|
||||
### Create Listing (Minimal)
|
||||
```json
|
||||
POST /listings
|
||||
{
|
||||
"transactionType": "SALE",
|
||||
"priceVND": "5500000000",
|
||||
"propertyType": "APARTMENT",
|
||||
"title": "Căn hộ 3PN view sông",
|
||||
"description": "Căn hộ cao cấp 3 phòng ngủ, nội thất đầy đủ...",
|
||||
"address": "208 Nguyễn Hữu Cảnh",
|
||||
"ward": "Phường 22",
|
||||
"district": "Bình Thạnh",
|
||||
"city": "Hồ Chí Minh",
|
||||
"latitude": 10.7942,
|
||||
"longitude": 106.7219,
|
||||
"areaM2": 85.5
|
||||
}
|
||||
```
|
||||
|
||||
### Search Listings
|
||||
```json
|
||||
GET /listings?transactionType=SALE&city=Hồ Chi Minh&minPrice=2000000000&maxPrice=10000000000&page=1&limit=20
|
||||
```
|
||||
|
||||
### Full-Text Search
|
||||
```json
|
||||
GET /search?q=chung cu quan 7&propertyType=apartment&transactionType=sale&page=1&perPage=20
|
||||
```
|
||||
|
||||
### Geo Search
|
||||
```json
|
||||
GET /search/geo?lat=10.7769&lng=106.7009&radiusKm=5&transactionType=sale&page=1&perPage=20
|
||||
```
|
||||
|
||||
### Create Payment
|
||||
```json
|
||||
POST /payments
|
||||
{
|
||||
"provider": "VNPAY",
|
||||
"type": "LISTING_FEE",
|
||||
"amountVND": 500000,
|
||||
"description": "Listing fee",
|
||||
"returnUrl": "https://example.com/return",
|
||||
"idempotencyKey": "uuid-123"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 K6 Test Scenarios
|
||||
|
||||
### Load Test 1: Search Load (High Volume, Public)
|
||||
- **Endpoint**: `GET /search` & `GET /search/geo`
|
||||
- **Load Profile**: Ramp 100 → 1000 users over 10 min
|
||||
- **Success Criteria**: p(95) < 500ms, p(99) < 1000ms
|
||||
- **Expected**: Public search should handle high concurrent load
|
||||
|
||||
### Load Test 2: Authentication (Moderate Load, Gated)
|
||||
- **Endpoint**: `POST /auth/register`, `POST /auth/login`
|
||||
- **Load Profile**: Ramp 10 → 100 users
|
||||
- **Rate Limit**: 5/hour per endpoint
|
||||
- **Success Criteria**: All requests within rate limit, p(95) < 300ms
|
||||
- **Expected**: Should gracefully reject over-limit requests
|
||||
|
||||
### Load Test 3: Listing Creation (Low Load, Quota Gated)
|
||||
- **Endpoint**: `POST /listings`
|
||||
- **Load Profile**: Ramp 5 → 50 users over 5 min
|
||||
- **Rate Limit**: Quota-gated (per subscription plan)
|
||||
- **Success Criteria**: 201 for successful creates, 403 for quota exceeded
|
||||
- **Expected**: Quota guard enforces plan limits
|
||||
|
||||
### Load Test 4: Payment Processing (Medium Load, Unthrottled)
|
||||
- **Endpoint**: `POST /payments`
|
||||
- **Load Profile**: Ramp 20 → 200 users
|
||||
- **Dependencies**: Requires authenticated session (JWT)
|
||||
- **Success Criteria**: p(95) < 1s (external provider latency)
|
||||
- **Expected**: System handles payment initiation concurrently
|
||||
|
||||
### Load Test 5: Payment Webhooks (High Volume, Throttled)
|
||||
- **Endpoint**: `POST /payments/callback/vnpay`
|
||||
- **Load Profile**: Sustained 20 requests/min (rate limit)
|
||||
- **Rate Limit**: 20/min enforced
|
||||
- **Success Criteria**: All requests process within rate limit
|
||||
- **Expected**: Webhook queue prevents abuse
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Authentication Flow for K6
|
||||
|
||||
### Cookie-Based Flow (Recommended)
|
||||
```javascript
|
||||
// 1. Register/Login
|
||||
const loginRes = http.post(`${BASE_URL}/auth/login`, {
|
||||
phone, password
|
||||
});
|
||||
// Cookies: access_token, refresh_token, goodgo_authenticated
|
||||
|
||||
// 2. Use cookies for authenticated requests
|
||||
const profileRes = http.get(`${BASE_URL}/auth/profile`);
|
||||
// Browser automatically sends cookies
|
||||
|
||||
// 3. Refresh when needed
|
||||
const refreshRes = http.post(`${BASE_URL}/auth/refresh`);
|
||||
```
|
||||
|
||||
### Token-Based Flow (Alternative)
|
||||
```javascript
|
||||
// 1. Capture tokens from register/login
|
||||
const loginRes = http.post(`${BASE_URL}/auth/login`, { phone, password });
|
||||
const { accessToken, refreshToken } = loginRes.json();
|
||||
|
||||
// 2. Use Bearer token header
|
||||
const params = {
|
||||
headers: { Authorization: `Bearer ${accessToken}` }
|
||||
};
|
||||
const profileRes = http.get(`${BASE_URL}/auth/profile`, params);
|
||||
|
||||
// 3. Refresh access token
|
||||
const refreshRes = http.post(`${BASE_URL}/auth/refresh`,
|
||||
{ refreshToken },
|
||||
params
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏪 Test Data Resources
|
||||
|
||||
### Available Seed Data
|
||||
- **Users**: Various roles (USER, AGENT, ADMIN)
|
||||
- **Listings**: Properties across Ho Chi Minh City districts
|
||||
- **Districts/Wards**: Vietnamese administrative data
|
||||
- **Property Types**: APARTMENT, HOUSE, LAND, SHOP, etc.
|
||||
- **Transaction Types**: SALE, RENT
|
||||
|
||||
### Test Fixtures
|
||||
- See `e2e/fixtures.ts` for test data generators
|
||||
- Use `createTestUser()` to generate unique test users
|
||||
- Test database seeded in `global-setup.ts`
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Quick K6 Script Template
|
||||
|
||||
```javascript
|
||||
import http from 'k6/http';
|
||||
import { check, group, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001/api/v1';
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '2m', target: 100 }, // Ramp up
|
||||
{ duration: '5m', target: 100 }, // Sustain
|
||||
{ duration: '2m', target: 0 }, // Ramp down
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500', 'p(99)<1000'],
|
||||
http_req_failed: ['rate<0.1'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function() {
|
||||
group('Search - Public, High Volume', () => {
|
||||
const res = http.get(`${BASE_URL}/search?q=chung cu&page=1&perPage=20`);
|
||||
check(res, { 'status is 200': (r) => r.status === 200 });
|
||||
sleep(1);
|
||||
});
|
||||
|
||||
group('Geo Search - Public', () => {
|
||||
const res = http.get(`${BASE_URL}/search/geo?lat=10.77&lng=106.70&radiusKm=5&perPage=20`);
|
||||
check(res, { 'status is 200': (r) => r.status === 200 });
|
||||
sleep(1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 Important Rate Limits
|
||||
|
||||
| Endpoint | Limit | Window |
|
||||
|----------|-------|--------|
|
||||
| `/auth/register` | 5 | per hour |
|
||||
| `/auth/login` | 5 | per hour |
|
||||
| `/auth/refresh` | 5 | per hour |
|
||||
| `/payments/callback/*` | 20 | per minute |
|
||||
| Others | None | (quota gates apply instead) |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Files to Reference
|
||||
|
||||
```
|
||||
K6_LOAD_TESTING_GUIDE.md # Comprehensive guide (THIS FILE IS SUMMARY OF)
|
||||
apps/api/src/modules/*/presentation/controllers/
|
||||
apps/api/src/modules/*/presentation/dto/
|
||||
e2e/fixtures.ts # Test data generators
|
||||
e2e/api/ # Existing E2E tests (reference)
|
||||
.env.example # Environment setup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist Before Running K6
|
||||
|
||||
- [ ] API running: `pnpm dev`
|
||||
- [ ] Database seeded: `pnpm db:seed`
|
||||
- [ ] Test database migrated: `.env.test` configured
|
||||
- [ ] K6 installed: `brew install k6` or Docker
|
||||
- [ ] JWT_SECRET set in `.env`
|
||||
- [ ] Base URL correct: `http://localhost:3001/api/v1`
|
||||
|
||||
---
|
||||
|
||||
805
docs/load-testing/K6_LOAD_TESTING_GUIDE.md
Normal file
805
docs/load-testing/K6_LOAD_TESTING_GUIDE.md
Normal file
@@ -0,0 +1,805 @@
|
||||
# GoodGo Platform API — K6 Load Testing Guide
|
||||
|
||||
## 🎯 Quick Summary
|
||||
|
||||
**Base URL**: `http://localhost:3001/api/v1`
|
||||
**Node Version**: >= 22.0.0
|
||||
**Testing Framework**: Playwright (E2E), Vitest (Unit)
|
||||
**No existing K6 or load testing setup found**
|
||||
|
||||
---
|
||||
|
||||
## 📋 Project Structure
|
||||
|
||||
### Root Directory
|
||||
```
|
||||
goodgo-platform/
|
||||
├── apps/api # NestJS backend (port 3001)
|
||||
├── apps/web # Next.js 14 frontend (port 3000)
|
||||
├── libs/mcp-servers # MCP tool server library
|
||||
├── prisma/ # Database schema & migrations
|
||||
├── e2e/ # Playwright E2E tests (api + web)
|
||||
├── turbo.json # Turborepo config
|
||||
├── package.json # Root workspace scripts
|
||||
├── .env.example # Environment variables template
|
||||
└── playwright.config.ts # Playwright configuration
|
||||
```
|
||||
|
||||
### Key Scripts (package.json)
|
||||
```bash
|
||||
pnpm dev # Start all apps (API :3001, Web :3000)
|
||||
pnpm test # Unit tests via Vitest (API only)
|
||||
pnpm test:e2e # Playwright E2E tests
|
||||
pnpm test:e2e:api # API E2E tests only
|
||||
pnpm test:e2e:web # Web E2E tests only
|
||||
pnpm build # Production build
|
||||
pnpm lint # ESLint
|
||||
pnpm typecheck # TypeScript checking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ API Module Structure
|
||||
|
||||
### API Base Architecture: `apps/api/src/modules/`
|
||||
|
||||
Each module follows DDD layers: `domain/` → `application/` → `infrastructure/` → `presentation/`
|
||||
|
||||
```
|
||||
modules/
|
||||
├── auth/ # Authentication & JWT
|
||||
├── listings/ # Property listings CRUD
|
||||
├── payments/ # Payment processing (VNPay, MoMo, ZaloPay)
|
||||
├── search/ # Full-text & geo search (Typesense)
|
||||
├── subscriptions/ # Plans, quotas, usage tracking
|
||||
├── admin/ # Moderation, KYC, user management
|
||||
├── analytics/ # Market data, heatmaps, price trends
|
||||
├── reviews/ # User reviews
|
||||
├── notifications/ # Email, push (FCM), in-app
|
||||
├── metrics/ # Prometheus metrics
|
||||
├── health/ # Health checks
|
||||
├── shared/ # Domain primitives, guards, pipes, logging
|
||||
└── mcp/ # MCP tool server endpoints
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 AUTH MODULE
|
||||
|
||||
### Controllers & Endpoints
|
||||
|
||||
#### File: `apps/api/src/modules/auth/presentation/controllers/auth.controller.ts`
|
||||
|
||||
| Method | Endpoint | Rate Limit | Auth | Description |
|
||||
|--------|----------|-----------|------|-------------|
|
||||
| POST | `/auth/register` | 5/hour | No | Register new user |
|
||||
| POST | `/auth/login` | 5/hour | LocalAuth | Login with phone + password |
|
||||
| POST | `/auth/refresh` | 5/hour | No | Refresh access token |
|
||||
| POST | `/auth/logout` | No limit | No | Clear auth cookies |
|
||||
| POST | `/auth/exchange-token` | No limit | No | Exchange OAuth tokens for cookies |
|
||||
| GET | `/auth/profile` | No limit | JWT | Get current user profile |
|
||||
| GET | `/auth/profile/agent` | No limit | JWT | Get agent profile for user |
|
||||
| PATCH | `/auth/kyc` | No limit | JWT+Admin | Verify user KYC (admin only) |
|
||||
|
||||
### DTOs
|
||||
|
||||
#### LoginDto
|
||||
```typescript
|
||||
{
|
||||
phone: string // Required, example: "0901234567"
|
||||
password: string // Required, example: "P@ssw0rd!"
|
||||
}
|
||||
```
|
||||
|
||||
#### RegisterDto
|
||||
```typescript
|
||||
{
|
||||
phone: string // Required, example: "0901234567"
|
||||
password: string // Required, min 8 chars, example: "P@ssw0rd!"
|
||||
fullName: string // Required, example: "Nguyen Van A"
|
||||
email?: string // Optional, valid email format
|
||||
}
|
||||
```
|
||||
|
||||
#### RefreshTokenDto
|
||||
```typescript
|
||||
{
|
||||
refreshToken?: string // Optional if using cookie
|
||||
}
|
||||
```
|
||||
|
||||
#### VerifyKycDto
|
||||
```typescript
|
||||
{
|
||||
userId: string
|
||||
kycStatus: string
|
||||
kycData?: object
|
||||
}
|
||||
```
|
||||
|
||||
### Cookies & Authentication
|
||||
|
||||
**Access Token**:
|
||||
- Cookie: `access_token`
|
||||
- Max Age: 15 minutes (900s)
|
||||
- HttpOnly: true
|
||||
- Secure: true (production only)
|
||||
- SameSite: strict
|
||||
- Path: /
|
||||
|
||||
**Refresh Token**:
|
||||
- Cookie: `refresh_token`
|
||||
- Max Age: 30 days
|
||||
- HttpOnly: true
|
||||
- Secure: true (production only)
|
||||
- SameSite: strict
|
||||
- Path: `/auth`
|
||||
|
||||
**Session Indicator**:
|
||||
- Cookie: `goodgo_authenticated` = "1"
|
||||
- HttpOnly: false (visible to frontend)
|
||||
|
||||
### OAuth Support
|
||||
- Google OAuth 2.0
|
||||
- Zalo OAuth (Vietnamese platform)
|
||||
- Environment: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `ZALO_APP_ID`, `ZALO_APP_SECRET`
|
||||
|
||||
---
|
||||
|
||||
## 🏠 LISTINGS MODULE
|
||||
|
||||
### Controllers & Endpoints
|
||||
|
||||
#### File: `apps/api/src/modules/listings/presentation/controllers/listings.controller.ts`
|
||||
|
||||
| Method | Endpoint | Auth | Quota | Description |
|
||||
|--------|----------|------|-------|-------------|
|
||||
| POST | `/listings` | JWT | Yes | Create new listing |
|
||||
| GET | `/listings` | No | No | Search/filter listings (public) |
|
||||
| GET | `/listings/:id` | No | No | Get listing detail |
|
||||
| GET | `/listings/pending` | JWT+Admin | No | Get listings pending moderation |
|
||||
| PATCH | `/listings/:id/status` | JWT | No | Update listing status |
|
||||
| POST | `/listings/:id/media` | JWT | No | Upload photo/video |
|
||||
| PATCH | `/listings/:id/moderate` | JWT+Admin | No | Moderate a listing (admin) |
|
||||
|
||||
### DTOs
|
||||
|
||||
#### CreateListingDto
|
||||
```typescript
|
||||
{
|
||||
transactionType: 'SALE' | 'RENT',
|
||||
priceVND: bigint | string,
|
||||
propertyType: 'APARTMENT' | 'HOUSE' | 'LAND' | etc.,
|
||||
title: string, // Min 5 chars
|
||||
description: string, // Min 10 chars
|
||||
address: string,
|
||||
ward: string,
|
||||
district: string,
|
||||
city: string,
|
||||
latitude: number, // -90 to 90
|
||||
longitude: number, // -180 to 180
|
||||
areaM2: number, // Total area
|
||||
usableAreaM2?: number,
|
||||
bedrooms?: number,
|
||||
bathrooms?: number,
|
||||
floors?: number, // For houses
|
||||
floor?: number, // For apartments
|
||||
totalFloors?: number,
|
||||
direction?: 'EAST' | 'WEST' | 'NORTH' | 'SOUTH' | etc.,
|
||||
yearBuilt?: number,
|
||||
legalStatus?: string,
|
||||
amenities?: string[], // e.g., ['Hồ bơi', 'Gym']
|
||||
nearbyPOIs?: object, // e.g., { schools: [], hospitals: [] }
|
||||
metroDistanceM?: number,
|
||||
projectName?: string,
|
||||
agentId?: string,
|
||||
rentPriceMonthly?: bigint | string,
|
||||
commissionPct?: number,
|
||||
}
|
||||
```
|
||||
|
||||
#### SearchListingsDto
|
||||
```typescript
|
||||
{
|
||||
status?: 'ACTIVE' | 'INACTIVE' | 'ARCHIVED',
|
||||
transactionType?: 'SALE' | 'RENT',
|
||||
propertyType?: 'APARTMENT' | 'HOUSE' | 'LAND' | etc.,
|
||||
city?: string,
|
||||
district?: string,
|
||||
minPrice?: bigint | string,
|
||||
maxPrice?: bigint | string,
|
||||
minArea?: number,
|
||||
maxArea?: number,
|
||||
bedrooms?: number,
|
||||
page?: number, // Default: 1
|
||||
limit?: number, // Default: 20, Max: 100
|
||||
}
|
||||
```
|
||||
|
||||
#### UpdateListingStatusDto
|
||||
```typescript
|
||||
{
|
||||
status: string,
|
||||
moderationNotes?: string,
|
||||
}
|
||||
```
|
||||
|
||||
#### ModerateListingDto
|
||||
```typescript
|
||||
{
|
||||
action: 'APPROVE' | 'REJECT',
|
||||
moderationScore?: number,
|
||||
notes?: string,
|
||||
}
|
||||
```
|
||||
|
||||
### Response Structures
|
||||
|
||||
#### ListingDetailData
|
||||
Contains full listing information including:
|
||||
- id, title, description
|
||||
- propertyType, transactionType
|
||||
- address, latitude, longitude, ward, district, city
|
||||
- priceVND, rentPriceMonthly
|
||||
- areaM2, usableAreaM2, bedrooms, bathrooms, floors
|
||||
- amenities, nearbyPOIs
|
||||
- legalStatus, yearBuilt, direction
|
||||
- mediaUrls (photos/videos)
|
||||
- agentInfo
|
||||
- createdAt, updatedAt
|
||||
|
||||
#### PaginatedResult<ListingSearchItem>
|
||||
```typescript
|
||||
{
|
||||
items: ListingSearchItem[],
|
||||
total: number,
|
||||
page: number,
|
||||
limit: number,
|
||||
totalPages: number,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💳 PAYMENTS MODULE
|
||||
|
||||
### Controllers & Endpoints
|
||||
|
||||
#### File: `apps/api/src/modules/payments/presentation/controllers/payments.controller.ts`
|
||||
|
||||
| Method | Endpoint | Auth | Rate Limit | Description |
|
||||
|--------|----------|------|-----------|-------------|
|
||||
| POST | `/payments` | JWT | No | Create payment |
|
||||
| GET | `/payments` | JWT | No | List user transactions |
|
||||
| GET | `/payments/:id` | JWT | No | Get payment status |
|
||||
| POST | `/payments/callback/:provider` | No | 20/min | Handle payment callback (webhook) |
|
||||
| POST | `/payments/:id/refund` | JWT+Admin | No | Refund payment (admin) |
|
||||
|
||||
### DTOs
|
||||
|
||||
#### CreatePaymentDto
|
||||
```typescript
|
||||
{
|
||||
provider: 'VNPAY' | 'MOMO' | 'ZALOPAY',
|
||||
type: 'LISTING_FEE' | 'SUBSCRIPTION' | 'AGENT_COMMISSION',
|
||||
amountVND: number, // 1 to 100,000,000,000
|
||||
description: string, // Payment description
|
||||
returnUrl: string, // URL (must be valid)
|
||||
transactionId?: string, // External ID
|
||||
idempotencyKey?: string, // For idempotency
|
||||
}
|
||||
```
|
||||
|
||||
#### ListTransactionsDto
|
||||
```typescript
|
||||
{
|
||||
status?: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
}
|
||||
```
|
||||
|
||||
#### RefundPaymentDto
|
||||
```typescript
|
||||
{
|
||||
reason: string,
|
||||
}
|
||||
```
|
||||
|
||||
### Payment Providers
|
||||
|
||||
- **VNPay** (Primary for Vietnam)
|
||||
- Environment: `VNPAY_TMN_CODE`, `VNPAY_HASH_SECRET`
|
||||
- Sandbox: `https://sandbox.vnpayment.vn/paymentv2/vpcpay.html`
|
||||
- API: `https://sandbox.vnpayment.vn/merchant_webapi/api/transaction`
|
||||
|
||||
- **MoMo** (Mobile wallet)
|
||||
- Environment: `MOMO_PARTNER_CODE`, `MOMO_ACCESS_KEY`, `MOMO_SECRET_KEY`
|
||||
- Endpoint: `https://test-payment.momo.vn/v2/gateway/api`
|
||||
|
||||
- **ZaloPay** (Zalo integrated)
|
||||
- Environment: `ZALOPAY_APP_ID`, `ZALOPAY_KEY1`, `ZALOPAY_KEY2`
|
||||
- Endpoint: `https://sb-openapi.zalopay.vn/v2`
|
||||
|
||||
### Callback Processing
|
||||
|
||||
**Webhook URL Pattern**: `/payments/callback/{provider}`
|
||||
|
||||
Supports both:
|
||||
- Query parameters (VNPay)
|
||||
- Request body (MoMo, ZaloPay)
|
||||
- Merged data handling internally
|
||||
|
||||
---
|
||||
|
||||
## 🔍 SEARCH MODULE
|
||||
|
||||
### Controllers & Endpoints
|
||||
|
||||
#### File: `apps/api/src/modules/search/presentation/controllers/search.controller.ts`
|
||||
|
||||
| Method | Endpoint | Auth | Description |
|
||||
|--------|----------|------|-------------|
|
||||
| GET | `/search` | No | Full-text search (public) |
|
||||
| GET | `/search/geo` | No | Geographic radius search (public) |
|
||||
| POST | `/search/reindex` | JWT+Admin | Reindex all properties (admin) |
|
||||
|
||||
### DTOs
|
||||
|
||||
#### SearchPropertiesDto (Full-text search)
|
||||
```typescript
|
||||
{
|
||||
q?: string, // Free-text query, e.g., 'chung cu quan 7'
|
||||
propertyType?: string, // Filter by type
|
||||
transactionType?: string, // 'sale' or 'rent'
|
||||
priceMin?: number, // Min price in VND
|
||||
priceMax?: number, // Max price in VND
|
||||
areaMin?: number, // Min area in m²
|
||||
areaMax?: number, // Max area in m²
|
||||
bedrooms?: number, // Number of bedrooms
|
||||
district?: string, // District name
|
||||
city?: string, // City name
|
||||
sortBy?: 'price_asc' | 'price_desc' | 'date_desc' | 'relevance',
|
||||
page?: number, // 1-based, default: 1
|
||||
perPage?: number, // Default: 20, Max: 100
|
||||
}
|
||||
```
|
||||
|
||||
#### GeoSearchDto (Geographic search)
|
||||
```typescript
|
||||
{
|
||||
lat: number, // Latitude, -90 to 90
|
||||
lng: number, // Longitude, -180 to 180
|
||||
radiusKm: number, // Radius, 0.1 to 100
|
||||
propertyType?: string,
|
||||
transactionType?: string,
|
||||
priceMin?: number,
|
||||
priceMax?: number,
|
||||
sortBy?: 'distance' | 'price_asc' | 'price_desc' | 'date_desc',
|
||||
page?: number, // Default: 1
|
||||
perPage?: number, // Default: 20, Max: 100
|
||||
}
|
||||
```
|
||||
|
||||
### Search Engine
|
||||
|
||||
**Typesense** integration for fast full-text & faceted search
|
||||
- Environment: `TYPESENSE_HOST`, `TYPESENSE_PORT`, `TYPESENSE_API_KEY`
|
||||
- Default: `http://localhost:8108`
|
||||
|
||||
### Response Structure
|
||||
|
||||
#### SearchResult
|
||||
```typescript
|
||||
{
|
||||
results: SearchHit[],
|
||||
facets?: {
|
||||
propertyType?: { value: string; count: number }[],
|
||||
district?: { value: string; count: number }[],
|
||||
transactionType?: { value: string; count: number }[],
|
||||
},
|
||||
total: number,
|
||||
page: number,
|
||||
perPage: number,
|
||||
totalPages: number,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database & Environment
|
||||
|
||||
### PostgreSQL with PostGIS
|
||||
|
||||
```
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=goodgo
|
||||
DB_USER=goodgo
|
||||
DB_PASSWORD=<change_me>
|
||||
DATABASE_URL=postgresql://goodgo:password@localhost:5432/goodgo?schema=public
|
||||
```
|
||||
|
||||
### Redis Cache
|
||||
|
||||
```
|
||||
REDIS_URL=redis://localhost:6379
|
||||
```
|
||||
|
||||
### Key Environment Variables
|
||||
|
||||
```bash
|
||||
# JWT Secrets (REQUIRED)
|
||||
JWT_SECRET=<openssl rand -base64 48>
|
||||
JWT_REFRESH_SECRET=<openssl rand -base64 48>
|
||||
JWT_EXPIRES_IN=15m
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
|
||||
|
||||
# Node Environment
|
||||
NODE_ENV=development|test|production
|
||||
PORT=3001 # API port
|
||||
|
||||
# OAuth
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
ZALO_APP_ID=
|
||||
ZALO_APP_SECRET=
|
||||
|
||||
# Typesense Search
|
||||
TYPESENSE_HOST=localhost
|
||||
TYPESENSE_PORT=8108
|
||||
TYPESENSE_API_KEY=
|
||||
|
||||
# MinIO/S3 Storage
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_ACCESS_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
MINIO_BUCKET=goodgo-media
|
||||
|
||||
# Payment Gateways
|
||||
VNPAY_TMN_CODE=
|
||||
VNPAY_HASH_SECRET=
|
||||
MOMO_PARTNER_CODE=
|
||||
ZALOPAY_APP_ID=
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Existing Test Setup
|
||||
|
||||
### Playwright Configuration
|
||||
|
||||
**File**: `playwright.config.ts`
|
||||
|
||||
```typescript
|
||||
testDir: './e2e'
|
||||
globalSetup: './e2e/global-setup.ts'
|
||||
globalTeardown: './e2e/global-teardown.ts'
|
||||
|
||||
Projects:
|
||||
- "api": Tests NestJS API (port 3001)
|
||||
baseURL: http://localhost:3001/api/v1
|
||||
- "web": Tests Next.js frontend (port 3000)
|
||||
baseURL: http://localhost:3000
|
||||
```
|
||||
|
||||
### Playwright Scripts
|
||||
|
||||
```bash
|
||||
pnpm test:e2e # Run all E2E tests
|
||||
pnpm test:e2e:api # API tests only
|
||||
pnpm test:e2e:web # Web tests only
|
||||
pnpm test:e2e:report # Show HTML report
|
||||
```
|
||||
|
||||
### Test Database
|
||||
|
||||
- CI uses `goodgo_test` database
|
||||
- Local uses `.env.test` for test database URL
|
||||
- Migrations & seed run in `global-setup.ts`
|
||||
- Cleanup in `global-teardown.ts`
|
||||
|
||||
### Example E2E Test
|
||||
|
||||
**File**: `e2e/api/auth-register.spec.ts`
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { createTestUser } from '../fixtures';
|
||||
|
||||
test.describe('POST /auth/register', () => {
|
||||
test('registers a new user and returns token pair', async ({ request }) => {
|
||||
const user = createTestUser();
|
||||
const res = await request.post('/auth/register', { data: user });
|
||||
|
||||
expect(res.status()).toBe(201);
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('accessToken');
|
||||
expect(body).toHaveProperty('refreshToken');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Unit Tests (Vitest)
|
||||
|
||||
```bash
|
||||
pnpm test # Run unit tests (API only)
|
||||
pnpm test:integration # Integration tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 CI/CD Setup
|
||||
|
||||
### GitHub Actions Workflows
|
||||
|
||||
#### `ci.yml` - Lint → Typecheck → Test → Build
|
||||
- Runs on: `push main` and `pull_request`
|
||||
- Services: PostgreSQL 16 + PostGIS
|
||||
- Steps: lint → typecheck → test → build
|
||||
|
||||
#### `e2e.yml` - Playwright E2E Tests
|
||||
- Runs on: `push main` and `pull_request`
|
||||
- Services:
|
||||
- PostgreSQL 16 + PostGIS
|
||||
- Redis 7
|
||||
- Typesense 27.1
|
||||
- MinIO (S3-compatible storage)
|
||||
- Artifacts: HTML report + traces
|
||||
|
||||
#### `security.yml` - Code Security
|
||||
- Dependency scanning
|
||||
- SAST analysis
|
||||
|
||||
#### `deploy.yml` - Production Deployment
|
||||
- Docker builds
|
||||
- Registry push
|
||||
- Deployment orchestration
|
||||
|
||||
---
|
||||
|
||||
## 📊 Architecture Patterns
|
||||
|
||||
### NestJS CQRS Pattern
|
||||
|
||||
Each module uses:
|
||||
- **Commands** (Write operations)
|
||||
- `CommandBus.execute(command)`
|
||||
- Located in `application/commands/`
|
||||
- Handlers in `application/commands/{command}/`
|
||||
|
||||
- **Queries** (Read operations)
|
||||
- `QueryBus.execute(query)`
|
||||
- Located in `application/queries/`
|
||||
- Handlers in `application/queries/{query}/`
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
// In controller
|
||||
const result = await this.commandBus.execute(
|
||||
new CreateListingCommand(userId, ...)
|
||||
);
|
||||
|
||||
const profile = await this.queryBus.execute(
|
||||
new GetProfileQuery(userId)
|
||||
);
|
||||
```
|
||||
|
||||
### Guards & Interceptors
|
||||
|
||||
- `JwtAuthGuard` - Validates JWT token
|
||||
- `LocalAuthGuard` - Email/password validation
|
||||
- `RolesGuard` - Role-based access control
|
||||
- `QuotaGuard` - Subscription quota enforcement
|
||||
- `FileValidationPipe` - File upload validation
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Starting the API
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Generate Prisma client
|
||||
pnpm db:generate
|
||||
|
||||
# Run migrations
|
||||
pnpm db:migrate:dev
|
||||
|
||||
# Seed data (users, listings, etc.)
|
||||
pnpm db:seed
|
||||
|
||||
# Start API (and Web)
|
||||
pnpm dev
|
||||
|
||||
# API will be available at:
|
||||
# http://localhost:3001/api/v1
|
||||
# Swagger UI: http://localhost:3001/api/v1/docs
|
||||
```
|
||||
|
||||
### With Docker
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
# Services: PostgreSQL, Redis, Typesense, MinIO, API, Web
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 K6 Load Testing Recommendations
|
||||
|
||||
### Key Endpoints to Test
|
||||
|
||||
1. **Authentication** (High priority)
|
||||
- Register: `POST /auth/register`
|
||||
- Login: `POST /auth/login`
|
||||
- Refresh: `POST /auth/refresh`
|
||||
- Profile: `GET /auth/profile` (authenticated)
|
||||
|
||||
2. **Listings** (High priority)
|
||||
- Create: `POST /listings` (quota-gated)
|
||||
- Search: `GET /listings` (public, high volume)
|
||||
- Detail: `GET /listings/:id` (public, high volume)
|
||||
|
||||
3. **Search** (High priority)
|
||||
- Full-text: `GET /search?q=...` (public, high volume)
|
||||
- Geo: `GET /search/geo?lat=...&lng=...` (public, high volume)
|
||||
|
||||
4. **Payments** (Medium priority)
|
||||
- Create: `POST /payments` (authenticated)
|
||||
- List: `GET /payments` (authenticated)
|
||||
- Webhook: `POST /payments/callback/:provider` (unthrottled)
|
||||
|
||||
5. **Admin Endpoints** (Medium priority, restricted)
|
||||
- Moderate listings: `PATCH /listings/:id/moderate`
|
||||
- List pending: `GET /listings/pending`
|
||||
- Verify KYC: `PATCH /auth/kyc`
|
||||
- Reindex: `POST /search/reindex`
|
||||
|
||||
### K6 Script Structure
|
||||
|
||||
```javascript
|
||||
import http from 'k6/http';
|
||||
import { check, group, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = 'http://localhost:3001/api/v1';
|
||||
|
||||
// Stage-based load: ramp up → sustained → ramp down
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '2m', target: 100 }, // Ramp up
|
||||
{ duration: '5m', target: 100 }, // Sustained
|
||||
{ duration: '2m', target: 0 }, // Ramp down
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500', 'p(99)<1000'],
|
||||
http_req_failed: ['rate<0.1'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function() {
|
||||
// Test scenarios here
|
||||
}
|
||||
```
|
||||
|
||||
### Data Generation Tips
|
||||
|
||||
- Use test fixture users from Playwright tests
|
||||
- Leverage Prisma seed data (districts, property types)
|
||||
- Generate realistic search queries
|
||||
- Test with various geo coordinates (Ho Chi Minh City: ~10.77°N, 106.70°E)
|
||||
|
||||
---
|
||||
|
||||
## 📁 File Locations Quick Reference
|
||||
|
||||
```
|
||||
apps/api/
|
||||
├── src/
|
||||
│ ├── main.ts # API entry point (port 3001)
|
||||
│ ├── app.module.ts # Root module
|
||||
│ └── modules/
|
||||
│ ├── auth/
|
||||
│ │ ├── presentation/controllers/auth.controller.ts
|
||||
│ │ ├── presentation/dto/
|
||||
│ │ │ ├── login.dto.ts
|
||||
│ │ │ ├── register.dto.ts
|
||||
│ │ │ ├── refresh-token.dto.ts
|
||||
│ │ │ └── verify-kyc.dto.ts
|
||||
│ │ ├── application/commands/
|
||||
│ │ ├── application/queries/
|
||||
│ │ ├── infrastructure/services/token.service.ts
|
||||
│ │ └── domain/
|
||||
│ ├── listings/
|
||||
│ │ ├── presentation/controllers/listings.controller.ts
|
||||
│ │ ├── presentation/dto/
|
||||
│ │ │ ├── create-listing.dto.ts
|
||||
│ │ │ ├── search-listings.dto.ts
|
||||
│ │ │ ├── update-listing-status.dto.ts
|
||||
│ │ │ └── moderate-listing.dto.ts
|
||||
│ │ └── ...
|
||||
│ ├── payments/
|
||||
│ │ ├── presentation/controllers/payments.controller.ts
|
||||
│ │ ├── presentation/dto/
|
||||
│ │ │ ├── create-payment.dto.ts
|
||||
│ │ │ ├── list-transactions.dto.ts
|
||||
│ │ │ └── refund-payment.dto.ts
|
||||
│ │ └── ...
|
||||
│ ├── search/
|
||||
│ │ ├── presentation/controllers/search.controller.ts
|
||||
│ │ ├── presentation/dto/
|
||||
│ │ │ ├── search-properties.dto.ts
|
||||
│ │ │ └── geo-search.dto.ts
|
||||
│ │ └── ...
|
||||
│ └── ...
|
||||
│
|
||||
└── package.json # Dependencies, scripts
|
||||
|
||||
e2e/
|
||||
├── api/ # Playwright API tests
|
||||
│ ├── auth-register.spec.ts
|
||||
│ ├── auth-refresh.spec.ts
|
||||
│ └── ...
|
||||
├── web/ # Playwright web tests
|
||||
├── fixtures.ts # Test data generators
|
||||
├── global-setup.ts # DB setup before tests
|
||||
└── global-teardown.ts # DB cleanup after tests
|
||||
|
||||
playwright.config.ts # Playwright config
|
||||
.github/workflows/
|
||||
├── ci.yml # Lint → typecheck → test → build
|
||||
├── e2e.yml # Playwright E2E
|
||||
├── security.yml # Security scanning
|
||||
└── deploy.yml # Production deployment
|
||||
|
||||
.env.example # Environment variable template
|
||||
.env.test # Test database connection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Useful Links & References
|
||||
|
||||
- **API Swagger Docs**: `http://localhost:3001/api/v1/docs`
|
||||
- **Project Root Docs**: `CLAUDE.md`
|
||||
- **Existing Analysis**: `CODEBASE_ANALYSIS.md`, `EXPLORATION_REPORT.md`
|
||||
- **Frontend Docs**: `docs/audits/FRONTEND_EXPLORATION.md`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary for K6 Implementation
|
||||
|
||||
**No existing K6 setup** — you have a clean slate!
|
||||
|
||||
**Key endpoints** identified across:
|
||||
- Auth (register, login, refresh, profile)
|
||||
- Listings (create, search, detail, moderate)
|
||||
- Search (full-text, geo)
|
||||
- Payments (create, callback, list, refund)
|
||||
- Admin (moderate, KYC, reindex)
|
||||
|
||||
**Rate limits** to consider:
|
||||
- Auth: 5/hour per endpoint
|
||||
- Payments callback: 20/min
|
||||
- Others: No limit (except quota guards on create operations)
|
||||
|
||||
**Infrastructure ready**:
|
||||
- Turbo monorepo for dependency management
|
||||
- PostgreSQL + PostGIS for spatial data
|
||||
- Typesense for search indexing
|
||||
- Redis for caching
|
||||
- MinIO for media storage
|
||||
- Prometheus metrics endpoint
|
||||
|
||||
**Tests can be integrated** into CI/CD pipeline via `.github/workflows/` (suggested: new `load-test.yml`)
|
||||
|
||||
659
docs/load-testing/K6_QUICK_START.md
Normal file
659
docs/load-testing/K6_QUICK_START.md
Normal file
@@ -0,0 +1,659 @@
|
||||
# K6 Load Testing — Quick Start Guide
|
||||
|
||||
Get started with K6 load tests for GoodGo Platform API in minutes.
|
||||
|
||||
---
|
||||
|
||||
## 1️⃣ Installation
|
||||
|
||||
### macOS
|
||||
```bash
|
||||
brew install k6
|
||||
```
|
||||
|
||||
### Linux
|
||||
```bash
|
||||
apt-get install k6
|
||||
```
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
docker pull grafana/k6:latest
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
```bash
|
||||
k6 version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2️⃣ Setup Test Environment
|
||||
|
||||
### Start API & Database
|
||||
```bash
|
||||
# Terminal 1: Start all services
|
||||
pnpm dev
|
||||
|
||||
# Terminal 2: Seed test database (if needed)
|
||||
pnpm db:seed
|
||||
```
|
||||
|
||||
### Verify API is Running
|
||||
```bash
|
||||
curl http://localhost:3001/api/v1/docs
|
||||
# Should return Swagger UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3️⃣ Create Your First K6 Test
|
||||
|
||||
### Simple Search Load Test
|
||||
|
||||
Create file: `load-tests/search.k6.js`
|
||||
|
||||
```javascript
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001/api/v1';
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '1m', target: 50 }, // Ramp up to 50 users
|
||||
{ duration: '2m', target: 50 }, // Stay at 50 users
|
||||
{ duration: '1m', target: 0 }, // Ramp down
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500', 'p(99)<1000'],
|
||||
http_req_failed: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function() {
|
||||
// Full-text search
|
||||
const searchRes = http.get(
|
||||
`${BASE_URL}/search?q=chung cu&page=1&perPage=20`
|
||||
);
|
||||
|
||||
check(searchRes, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
'has results': (r) => r.body.includes('items'),
|
||||
});
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Geo search
|
||||
const geoRes = http.get(
|
||||
`${BASE_URL}/search/geo?lat=10.7769&lng=106.7009&radiusKm=5`
|
||||
);
|
||||
|
||||
check(geoRes, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||||
});
|
||||
|
||||
sleep(2);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4️⃣ Run Your First Test
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
k6 run load-tests/search.k6.js
|
||||
```
|
||||
|
||||
### With Custom Base URL
|
||||
```bash
|
||||
BASE_URL=http://localhost:3001/api/v1 k6 run load-tests/search.k6.js
|
||||
```
|
||||
|
||||
### With Docker
|
||||
```bash
|
||||
docker run -i grafana/k6 run - < load-tests/search.k6.js
|
||||
```
|
||||
|
||||
### Output
|
||||
```
|
||||
/\ |‾‾| /‾‾/‾‾ |‾‾| /‾‾/‾‾
|
||||
/ \ | |/ / | |/ /
|
||||
/ \ | ( | (
|
||||
/ \ | |\ \ | |\ \
|
||||
/ \ |__| \_/\_/|__| \_/\_/
|
||||
|
||||
execution: local
|
||||
script: load-tests/search.k6.js
|
||||
output: -
|
||||
|
||||
scenarios: (100.00%) 1 local scenario, 50 max VUs, 4m30s max duration
|
||||
|
||||
✓ checks........................ 100%
|
||||
✓ http_req_duration............. 96% ✓ 0 ✗ 12
|
||||
✓ http_req_failed............... 0%
|
||||
|
||||
data_received........: 512 kB (1.8 kB/s)
|
||||
data_sent..............: 45 kB (250 B/s)
|
||||
http_reqs..............: 120 (0.44/s)
|
||||
iteration_duration.....: 4s
|
||||
iterations.............: 60 (0.22/s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5️⃣ Authentication Test
|
||||
|
||||
Create file: `load-tests/auth.k6.js`
|
||||
|
||||
```javascript
|
||||
import http from 'k6/http';
|
||||
import { check } from 'k6';
|
||||
import { Counter, Trend } from 'k6/metrics';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001/api/v1';
|
||||
const loginCounter = new Counter('login_success');
|
||||
const loginDuration = new Trend('login_duration');
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '30s', target: 10 }, // 10 users
|
||||
{ duration: '1m', target: 10 }, // Hold for 1 min
|
||||
{ duration: '30s', target: 0 }, // Ramp down
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<300', 'p(99)<500'],
|
||||
},
|
||||
};
|
||||
|
||||
// Generate unique phone for each user
|
||||
function generatePhone() {
|
||||
const timestamp = new Date().getTime();
|
||||
const random = Math.floor(Math.random() * 1000000);
|
||||
return `09${String(timestamp + random).slice(-8)}`;
|
||||
}
|
||||
|
||||
export default function() {
|
||||
const phone = generatePhone();
|
||||
const password = 'TestPass123!';
|
||||
const fullName = 'Load Test User';
|
||||
|
||||
// Register
|
||||
const registerRes = http.post(`${BASE_URL}/auth/register`, {
|
||||
phone,
|
||||
password,
|
||||
fullName,
|
||||
});
|
||||
|
||||
check(registerRes, {
|
||||
'register status is 201': (r) => r.status === 201,
|
||||
'has tokens': (r) => r.json().accessToken && r.json().refreshToken,
|
||||
});
|
||||
|
||||
// Login (same credentials)
|
||||
const loginRes = http.post(`${BASE_URL}/auth/login`, {
|
||||
phone,
|
||||
password,
|
||||
});
|
||||
|
||||
loginDuration.add(loginRes.timings.duration);
|
||||
|
||||
check(loginRes, {
|
||||
'login status is 201': (r) => r.status === 201,
|
||||
'login response time < 300ms': (r) => r.timings.duration < 300,
|
||||
});
|
||||
|
||||
if (loginRes.status === 201) {
|
||||
loginCounter.add(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Run Auth Test
|
||||
```bash
|
||||
k6 run load-tests/auth.k6.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6️⃣ Listing Creation Test (Authenticated)
|
||||
|
||||
Create file: `load-tests/listings.k6.js`
|
||||
|
||||
```javascript
|
||||
import http from 'k6/http';
|
||||
import { check, group } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001/api/v1';
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '30s', target: 5 }, // 5 users (quota limited)
|
||||
{ duration: '1m30s', target: 5 },
|
||||
{ duration: '30s', target: 0 },
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<1000'],
|
||||
},
|
||||
};
|
||||
|
||||
function createAuthenticatedSession() {
|
||||
// Generate unique user
|
||||
const phone = `09${Math.random().toString().slice(2, 10)}`;
|
||||
const password = 'TestPass123!';
|
||||
const fullName = 'Listing Agent';
|
||||
|
||||
// Register user
|
||||
const registerRes = http.post(`${BASE_URL}/auth/register`, {
|
||||
phone,
|
||||
password,
|
||||
fullName,
|
||||
});
|
||||
|
||||
if (registerRes.status !== 201) {
|
||||
console.error('Registration failed:', registerRes.body);
|
||||
return null;
|
||||
}
|
||||
|
||||
return registerRes.json();
|
||||
}
|
||||
|
||||
export default function() {
|
||||
const session = createAuthenticatedSession();
|
||||
if (!session?.accessToken) return;
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
group('Create Listing', () => {
|
||||
const listingData = {
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5500000000',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ 3PN view sông Sài Gòn - Load Test',
|
||||
description: 'Căn hộ cao cấp 3 phòng ngủ, nội thất đầy đủ, view trực diện sông Sài Gòn.',
|
||||
address: '208 Nguyễn Hữu Cảnh',
|
||||
ward: 'Phường 22',
|
||||
district: 'Bình Thạnh',
|
||||
city: 'Hồ Chí Minh',
|
||||
latitude: 10.7942,
|
||||
longitude: 106.7219,
|
||||
areaM2: 85.5,
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
floor: 15,
|
||||
totalFloors: 30,
|
||||
yearBuilt: 2020,
|
||||
legalStatus: 'Sổ hồng',
|
||||
amenities: ['Hồ bơi', 'Gym', 'Sân chơi trẻ em'],
|
||||
};
|
||||
|
||||
const res = http.post(`${BASE_URL}/listings`, listingData, {
|
||||
headers,
|
||||
});
|
||||
|
||||
check(res, {
|
||||
'status is 201': (r) => r.status === 201,
|
||||
'has listing id': (r) => r.json().id !== undefined,
|
||||
'response time < 1s': (r) => r.timings.duration < 1000,
|
||||
});
|
||||
|
||||
if (res.status === 201) {
|
||||
const listingId = res.json().id;
|
||||
|
||||
// Get listing details
|
||||
group('Get Listing Details', () => {
|
||||
const detailRes = http.get(`${BASE_URL}/listings/${listingId}`);
|
||||
check(detailRes, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'title matches': (r) => r.json().title === listingData.title,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Run Listing Test
|
||||
```bash
|
||||
k6 run load-tests/listings.k6.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7️⃣ Payment Test
|
||||
|
||||
Create file: `load-tests/payments.k6.js`
|
||||
|
||||
```javascript
|
||||
import http from 'k6/http';
|
||||
import { check, group } from 'k6';
|
||||
import { randomString } from 'https://jslib.k6.io/k6-utils/1.1.0/index.js';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001/api/v1';
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '30s', target: 10 },
|
||||
{ duration: '1m', target: 10 },
|
||||
{ duration: '30s', target: 0 },
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<1000'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function() {
|
||||
// Create session first
|
||||
const phone = `09${Math.random().toString().slice(2, 10)}`;
|
||||
const password = 'TestPass123!';
|
||||
|
||||
const registerRes = http.post(`${BASE_URL}/auth/register`, {
|
||||
phone,
|
||||
password,
|
||||
fullName: 'Payment Test User',
|
||||
});
|
||||
|
||||
if (registerRes.status !== 201) return;
|
||||
|
||||
const { accessToken } = registerRes.json();
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
group('Create Payment', () => {
|
||||
const paymentRes = http.post(`${BASE_URL}/payments`, {
|
||||
provider: 'VNPAY',
|
||||
type: 'LISTING_FEE',
|
||||
amountVND: 500000,
|
||||
description: 'Load test payment',
|
||||
returnUrl: 'http://localhost:3000/payment/return',
|
||||
idempotencyKey: `load-test-${randomString(16)}`,
|
||||
}, {
|
||||
headers,
|
||||
});
|
||||
|
||||
check(paymentRes, {
|
||||
'status is 201': (r) => r.status === 201,
|
||||
'has paymentUrl': (r) => r.json().paymentUrl !== undefined,
|
||||
'response time < 1s': (r) => r.timings.duration < 1000,
|
||||
});
|
||||
|
||||
if (paymentRes.status === 201) {
|
||||
const paymentId = paymentRes.json().id;
|
||||
|
||||
// Check payment status
|
||||
group('Get Payment Status', () => {
|
||||
const statusRes = http.get(`${BASE_URL}/payments/${paymentId}`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
check(statusRes, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'has status field': (r) => r.json().status !== undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Run Payment Test
|
||||
```bash
|
||||
k6 run load-tests/payments.k6.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8️⃣ Run All Tests with Results
|
||||
|
||||
Create file: `load-tests/all-scenarios.k6.js`
|
||||
|
||||
```javascript
|
||||
import http from 'k6/http';
|
||||
import { check, group, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001/api/v1';
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '1m', target: 50 }, // Ramp up
|
||||
{ duration: '3m', target: 50 }, // Sustain
|
||||
{ duration: '1m', target: 0 }, // Ramp down
|
||||
],
|
||||
thresholds: {
|
||||
'http_req_duration{scenario:search}': ['p(95)<500'],
|
||||
'http_req_duration{scenario:auth}': ['p(95)<300'],
|
||||
'http_req_failed': ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function() {
|
||||
// Scenario 1: Public Search (60% of load)
|
||||
group('Search Scenario', () => {
|
||||
const res = http.get(`${BASE_URL}/search?q=chung cu&page=1&perPage=20`, {
|
||||
tags: { scenario: 'search' },
|
||||
});
|
||||
check(res, { 'status 200': (r) => r.status === 200 });
|
||||
sleep(1);
|
||||
});
|
||||
|
||||
// Scenario 2: Auth (20% of load)
|
||||
if (Math.random() < 0.2) {
|
||||
group('Auth Scenario', () => {
|
||||
const res = http.post(`${BASE_URL}/auth/login`, {
|
||||
phone: '0912345678',
|
||||
password: 'TestPass123!',
|
||||
}, {
|
||||
tags: { scenario: 'auth' },
|
||||
});
|
||||
check(res, { 'status 201 or 401': (r) => r.status === 201 || r.status === 401 });
|
||||
sleep(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Scenario 3: Geo Search (20% of load)
|
||||
if (Math.random() < 0.2) {
|
||||
group('Geo Search Scenario', () => {
|
||||
const res = http.get(
|
||||
`${BASE_URL}/search/geo?lat=10.77&lng=106.70&radiusKm=5`,
|
||||
{ tags: { scenario: 'geo' } }
|
||||
);
|
||||
check(res, { 'status 200': (r) => r.status === 200 });
|
||||
sleep(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Run with Summary
|
||||
```bash
|
||||
k6 run load-tests/all-scenarios.k6.js \
|
||||
--vus=50 \
|
||||
--duration=5m \
|
||||
--summary-export=summary.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9️⃣ Generate Reports
|
||||
|
||||
### JSON Report
|
||||
```bash
|
||||
k6 run load-tests/search.k6.js --summary-export=report.json
|
||||
```
|
||||
|
||||
### Upload to Grafana Cloud
|
||||
```bash
|
||||
k6 run load-tests/search.k6.js \
|
||||
--out cloud
|
||||
# Requires: k6 login or K6_CLOUD_TOKEN env var
|
||||
```
|
||||
|
||||
### Generate HTML Report (with extension)
|
||||
```bash
|
||||
k6 run load-tests/search.k6.js \
|
||||
--out csv=results.csv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔟 CI Integration
|
||||
|
||||
Create: `.github/workflows/load-test.yml`
|
||||
|
||||
```yaml
|
||||
name: Load Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # Daily at 2 AM
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
load-test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgis/postgis:16
|
||||
env:
|
||||
POSTGRES_DB: goodgo_test
|
||||
POSTGRES_USER: goodgo
|
||||
POSTGRES_PASSWORD: test_secret
|
||||
options: --health-cmd pg_isready --health-interval 10s
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup database
|
||||
env:
|
||||
DATABASE_URL: postgresql://goodgo:test_secret@localhost:5432/goodgo_test
|
||||
run: |
|
||||
pnpm db:migrate:deploy
|
||||
pnpm db:seed
|
||||
|
||||
- name: Start API
|
||||
env:
|
||||
DATABASE_URL: postgresql://goodgo:test_secret@localhost:5432/goodgo_test
|
||||
run: pnpm --filter @goodgo/api run build &
|
||||
|
||||
- name: Install K6
|
||||
run: sudo apt-get install -y k6
|
||||
|
||||
- name: Wait for API
|
||||
run: |
|
||||
for i in {1..30}; do
|
||||
curl http://localhost:3001/api/v1/docs && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Run Load Tests
|
||||
run: k6 run load-tests/search.k6.js --summary-export=results.json
|
||||
|
||||
- name: Upload Results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: k6-results
|
||||
path: results.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Common K6 Checks
|
||||
|
||||
```javascript
|
||||
// HTTP Status
|
||||
check(res, { 'status 200': (r) => r.status === 200 });
|
||||
|
||||
// Response Time
|
||||
check(res, { 'response < 500ms': (r) => r.timings.duration < 500 });
|
||||
|
||||
// JSON Content
|
||||
check(res, { 'has id': (r) => r.json().id });
|
||||
|
||||
// Body Contains
|
||||
check(res, { 'body contains': (r) => r.body.includes('text') });
|
||||
|
||||
// Multiple Conditions
|
||||
check(res, {
|
||||
'status is 2xx': (r) => r.status >= 200 && r.status < 300,
|
||||
'response time acceptable': (r) => r.timings.duration < 1000,
|
||||
});
|
||||
|
||||
// Response Headers
|
||||
check(res, {
|
||||
'content-type is json': (r) => r.headers['Content-Type'] === 'application/json',
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Debugging
|
||||
|
||||
### Verbose Output
|
||||
```bash
|
||||
k6 run -v load-tests/search.k6.js
|
||||
```
|
||||
|
||||
### Show Request/Response
|
||||
```bash
|
||||
k6 run --http-debug=full load-tests/search.k6.js
|
||||
```
|
||||
|
||||
### Limit to Single VU
|
||||
```bash
|
||||
k6 run --vus=1 --iterations=1 load-tests/search.k6.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
1. **Read Full Guide**: `K6_LOAD_TESTING_GUIDE.md`
|
||||
2. **Check Endpoints**: `K6_ENDPOINTS_SUMMARY.md`
|
||||
3. **Explore K6 Docs**: https://k6.io/docs
|
||||
4. **Community Scripts**: https://github.com/grafana/k6-templates
|
||||
|
||||
---
|
||||
|
||||
## ✅ Troubleshooting
|
||||
|
||||
### "connection refused"
|
||||
- Ensure API is running: `pnpm dev`
|
||||
- Check port: 3001
|
||||
- Verify BASE_URL: `http://localhost:3001/api/v1`
|
||||
|
||||
### "rate limit exceeded"
|
||||
- Auth endpoints: 5/hour limit
|
||||
- Spread requests or use different test data
|
||||
- Check test database has seed data
|
||||
|
||||
### "insufficient credits" (payments)
|
||||
- Payments require authenticated user
|
||||
- Create user session first in test
|
||||
- Use test provider credentials
|
||||
|
||||
### "timeout"
|
||||
- Increase K6 timeout in options
|
||||
- Check API logs for errors
|
||||
- Reduce number of VUs initially
|
||||
|
||||
---
|
||||
|
||||
404
docs/load-testing/K6_README.md
Normal file
404
docs/load-testing/K6_README.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# K6 Load Testing Documentation for GoodGo Platform
|
||||
|
||||
Complete guide to understanding and implementing K6 load tests for the GoodGo Platform API.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Files
|
||||
|
||||
This directory contains three comprehensive guides for K6 load testing:
|
||||
|
||||
### 1. **K6_LOAD_TESTING_GUIDE.md** (Primary Reference)
|
||||
Comprehensive exploration of the GoodGo Platform API structure for load testing.
|
||||
|
||||
**Contents:**
|
||||
- API module structure (auth, listings, payments, search)
|
||||
- Detailed endpoint documentation with HTTP methods, rate limits, and auth requirements
|
||||
- Complete DTO specifications with request/response body shapes
|
||||
- Database and environment configuration reference
|
||||
- Existing test setup (Playwright, Vitest, CI/CD)
|
||||
- Architecture patterns (CQRS, DDD)
|
||||
- File location quick reference
|
||||
- K6 implementation recommendations
|
||||
|
||||
**When to use:** Deep dives into specific endpoints, understanding authentication flows, checking environment variables
|
||||
|
||||
### 2. **K6_ENDPOINTS_SUMMARY.md** (Quick Reference)
|
||||
Condensed endpoint reference with data shapes for immediate lookup.
|
||||
|
||||
**Contents:**
|
||||
- All endpoints in table format (method, path, auth, rate limit)
|
||||
- Authentication module (register, login, refresh, profile)
|
||||
- Listings module (CRUD, moderation, media upload)
|
||||
- Payments module (create, list, callbacks, refund)
|
||||
- Search module (full-text, geo)
|
||||
- Request/response body examples (JSON)
|
||||
- K6 test scenarios (search, auth, listings, payments, webhooks)
|
||||
- Rate limits summary
|
||||
- Authentication flow examples (cookies vs tokens)
|
||||
|
||||
**When to use:** Quick lookup of endpoint details, copy-paste example payloads, understanding rate limits
|
||||
|
||||
### 3. **K6_QUICK_START.md** (Executable Examples)
|
||||
Step-by-step guide with ready-to-run K6 scripts and setup instructions.
|
||||
|
||||
**Contents:**
|
||||
- Installation instructions (macOS, Linux, Docker)
|
||||
- Environment setup (starting API, seeding database)
|
||||
- Five runnable K6 scripts:
|
||||
- Search load test (public, high volume)
|
||||
- Auth load test (rate-limited registration)
|
||||
- Listing creation (authenticated, quota-gated)
|
||||
- Payment processing (authenticated)
|
||||
- All scenarios combined
|
||||
- CI integration with GitHub Actions
|
||||
- Report generation options (JSON, Grafana Cloud, CSV)
|
||||
- Common K6 checks and patterns
|
||||
- Debugging and troubleshooting
|
||||
|
||||
**When to use:** Getting started quickly, running tests immediately, setting up CI/CD
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start (3 Minutes)
|
||||
|
||||
### 1. Install K6
|
||||
```bash
|
||||
brew install k6 # macOS
|
||||
# or
|
||||
apt-get install k6 # Linux
|
||||
```
|
||||
|
||||
### 2. Start API & Database
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm db:generate
|
||||
pnpm db:migrate:dev
|
||||
pnpm db:seed
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 3. Run a Load Test
|
||||
```bash
|
||||
# Copy this from K6_QUICK_START.md Step 3
|
||||
k6 run load-tests/search.k6.js
|
||||
```
|
||||
|
||||
### 4. View Results
|
||||
K6 prints a summary to console. For more detailed reports, see K6_QUICK_START.md section on report generation.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Test Scenarios Implemented
|
||||
|
||||
| Scenario | File | Focus | VUs | Duration | Key Endpoints |
|
||||
|----------|------|-------|-----|----------|--------------|
|
||||
| Search Load | `load-tests/search.k6.js` | Public search performance | 50 | 4m | `GET /search`, `GET /search/geo` |
|
||||
| Authentication | `load-tests/auth.k6.js` | Auth throughput & rate limits | 10 | 2m | `POST /auth/register`, `POST /auth/login` |
|
||||
| Listing Creation | `load-tests/listings.k6.js` | Authenticated listing CRUD | 5 | 2m | `POST /listings`, `GET /listings/:id` |
|
||||
| Payments | `load-tests/payments.k6.js` | Payment initiation & status | 10 | 2m | `POST /payments`, `GET /payments/:id` |
|
||||
| Combined | `load-tests/all-scenarios.k6.js` | Realistic mixed load | 50 | 5m | Multiple endpoints |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication Methods
|
||||
|
||||
### Option 1: Cookie-Based (Recommended for Browser-Like Tests)
|
||||
```javascript
|
||||
const loginRes = http.post(`${BASE_URL}/auth/login`, { phone, password });
|
||||
// Cookies automatically managed by K6
|
||||
const profileRes = http.get(`${BASE_URL}/auth/profile`);
|
||||
```
|
||||
|
||||
### Option 2: Bearer Token (Recommended for API-Only Tests)
|
||||
```javascript
|
||||
const loginRes = http.post(`${BASE_URL}/auth/login`, { phone, password });
|
||||
const { accessToken } = loginRes.json();
|
||||
const headers = { Authorization: `Bearer ${accessToken}` };
|
||||
const profileRes = http.get(`${BASE_URL}/auth/profile`, { headers });
|
||||
```
|
||||
|
||||
See K6_ENDPOINTS_SUMMARY.md for full examples.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Key Endpoints by Priority
|
||||
|
||||
### High Priority (Core Functionality)
|
||||
| Endpoint | Priority | Why |
|
||||
|----------|----------|-----|
|
||||
| `GET /search` | ⭐⭐⭐ | Public, high-volume query |
|
||||
| `GET /search/geo` | ⭐⭐⭐ | Geospatial, frequently used |
|
||||
| `GET /listings` | ⭐⭐⭐ | Public search/filter |
|
||||
| `GET /listings/:id` | ⭐⭐⭐ | Detail page load |
|
||||
| `POST /auth/login` | ⭐⭐ | User session creation |
|
||||
| `POST /auth/register` | ⭐⭐ | Rate-limited, important |
|
||||
|
||||
### Medium Priority (Feature-Specific)
|
||||
| Endpoint | Priority | Why |
|
||||
|----------|----------|-----|
|
||||
| `POST /listings` | ⭐⭐ | Quota-gated, authenticated |
|
||||
| `POST /payments` | ⭐⭐ | External integrations |
|
||||
| `GET /payments` | ⭐⭐ | User transaction history |
|
||||
| `POST /payments/callback/:provider` | ⭐⭐ | Webhook handler, critical |
|
||||
|
||||
### Low Priority (Admin/Specialized)
|
||||
| Endpoint | Priority | Why |
|
||||
|----------|----------|-----|
|
||||
| `PATCH /listings/:id/moderate` | ⭐ | Admin-only |
|
||||
| `GET /listings/pending` | ⭐ | Admin-only |
|
||||
| `POST /search/reindex` | ⭐ | Admin-only, scheduled |
|
||||
|
||||
---
|
||||
|
||||
## 📍 API Structure at a Glance
|
||||
|
||||
```
|
||||
API Base: http://localhost:3001/api/v1
|
||||
|
||||
Modules:
|
||||
├── /auth # User authentication & profiles
|
||||
├── /listings # Property CRUD & moderation
|
||||
├── /search # Full-text & geo search
|
||||
├── /payments # Payment processing & webhooks
|
||||
├── /subscriptions # Plans & quotas (not focused for load tests)
|
||||
├── /admin # Admin operations (low priority for load tests)
|
||||
└── /analytics # Market data (low priority for load tests)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database Configuration
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
DATABASE_URL=postgresql://goodgo:password@localhost:5432/goodgo
|
||||
REDIS_URL=redis://localhost:6379
|
||||
TYPESENSE_HOST=localhost
|
||||
TYPESENSE_PORT=8108
|
||||
```
|
||||
|
||||
### Test Environment (CI)
|
||||
```bash
|
||||
DATABASE_URL=postgresql://goodgo:goodgo_test_secret@localhost:5432/goodgo_test
|
||||
REDIS_URL=redis://localhost:6379
|
||||
TYPESENSE_HOST=localhost
|
||||
TYPESENSE_PORT=8108
|
||||
```
|
||||
|
||||
See K6_LOAD_TESTING_GUIDE.md for full environment variables.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Rate Limits
|
||||
|
||||
Respect these limits in your load tests:
|
||||
|
||||
| Endpoint | Limit | Window | Action on Exceeded |
|
||||
|----------|-------|--------|-------------------|
|
||||
| `/auth/register` | 5 | per hour | Returns 429 |
|
||||
| `/auth/login` | 5 | per hour | Returns 429 |
|
||||
| `/auth/refresh` | 5 | per hour | Returns 429 |
|
||||
| `/payments/callback/*` | 20 | per minute | Returns 429 |
|
||||
| All others | None | N/A | Quota gates apply for writes |
|
||||
|
||||
**K6 Handling:**
|
||||
```javascript
|
||||
check(res, {
|
||||
'status not rate limited': (r) => r.status !== 429,
|
||||
'status success or expected': (r) => [200, 201, 400, 404].includes(r.status),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Recommended Test Structure
|
||||
|
||||
```
|
||||
load-tests/
|
||||
├── search.k6.js # High-volume public search
|
||||
├── auth.k6.js # Authentication flow with rate limit handling
|
||||
├── listings.k6.js # Authenticated listing creation
|
||||
├── payments.k6.js # Payment processing
|
||||
├── all-scenarios.k6.js # Combined realistic mix
|
||||
├── helpers/
|
||||
│ ├── data-generators.js # Generate test data (users, listings)
|
||||
│ ├── auth-flows.js # Reusable login/register functions
|
||||
│ └── assertions.js # Custom check functions
|
||||
└── config.js # Base URL, env, thresholds
|
||||
```
|
||||
|
||||
Example helper structure provided in K6_QUICK_START.md.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Integration with Existing Tests
|
||||
|
||||
### Complement, Don't Replace
|
||||
|
||||
K6 is for **load testing** (performance under concurrent load).
|
||||
Existing tests serve different purposes:
|
||||
|
||||
| Test Type | Tool | Purpose | When |
|
||||
|-----------|------|---------|------|
|
||||
| Unit Tests | Vitest | Verify function logic | During development |
|
||||
| E2E Tests | Playwright | Verify user flows work | Before deployment |
|
||||
| Load Tests | K6 | Verify performance at scale | Scheduled, on-demand |
|
||||
|
||||
### Running All Tests
|
||||
|
||||
```bash
|
||||
# Unit tests (API only)
|
||||
pnpm test
|
||||
|
||||
# E2E tests (API + Web)
|
||||
pnpm test:e2e
|
||||
|
||||
# Load tests (new)
|
||||
k6 run load-tests/search.k6.js
|
||||
|
||||
# All in sequence
|
||||
pnpm test && pnpm test:e2e && k6 run load-tests/all-scenarios.k6.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 CI/CD Integration
|
||||
|
||||
### GitHub Actions Workflow
|
||||
|
||||
Create `.github/workflows/load-test.yml` (template in K6_QUICK_START.md section 🔟):
|
||||
|
||||
```bash
|
||||
# Runs on schedule (daily at 2 AM)
|
||||
# Or manually via workflow_dispatch
|
||||
# Reports results as artifacts
|
||||
```
|
||||
|
||||
### Manual Reporting
|
||||
|
||||
```bash
|
||||
# Export JSON
|
||||
k6 run load-tests/search.k6.js --summary-export=results.json
|
||||
|
||||
# View CSV (with extension)
|
||||
k6 run load-tests/search.k6.js --out csv=results.csv
|
||||
|
||||
# Upload to Grafana Cloud
|
||||
K6_CLOUD_TOKEN=xxx k6 run load-tests/search.k6.js --out cloud
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Cross-Reference Guide
|
||||
|
||||
### Looking for...?
|
||||
|
||||
| Need | Find in |
|
||||
|------|----------|
|
||||
| All endpoint URLs & methods | K6_ENDPOINTS_SUMMARY.md |
|
||||
| Request/response JSON shapes | K6_ENDPOINTS_SUMMARY.md (📊 Key Data Shapes) |
|
||||
| DTOs & validation rules | K6_LOAD_TESTING_GUIDE.md (Controllers & DTOs) |
|
||||
| Rate limit specifics | K6_ENDPOINTS_SUMMARY.md (📌 Important Rate Limits) |
|
||||
| Authentication flows | K6_ENDPOINTS_SUMMARY.md (🔗 Authentication Flow for K6) |
|
||||
| Database variables | K6_LOAD_TESTING_GUIDE.md (🗄️ Database & Environment) |
|
||||
| Ready-to-run scripts | K6_QUICK_START.md (Steps 3-8️⃣) |
|
||||
| CI/CD setup | K6_QUICK_START.md (Step 🔟) |
|
||||
| Troubleshooting | K6_QUICK_START.md (✅ Troubleshooting) |
|
||||
| Architecture details | K6_LOAD_TESTING_GUIDE.md (📊 Architecture Patterns) |
|
||||
| File locations | K6_LOAD_TESTING_GUIDE.md (📁 File Locations Quick Reference) |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Common Tasks
|
||||
|
||||
### Task: Load Test Search Endpoint
|
||||
1. Read: K6_ENDPOINTS_SUMMARY.md (🔍 Search section)
|
||||
2. Use: K6_QUICK_START.md (Step 3️⃣ - Search Load Test)
|
||||
3. Run: `k6 run load-tests/search.k6.js`
|
||||
|
||||
### Task: Understand Payment Flow
|
||||
1. Read: K6_LOAD_TESTING_GUIDE.md (💳 PAYMENTS MODULE)
|
||||
2. Check: K6_ENDPOINTS_SUMMARY.md (💳 Payments section)
|
||||
3. Use: K6_QUICK_START.md (Step 7️⃣ - Payment Test)
|
||||
|
||||
### Task: Add New Endpoint to Load Tests
|
||||
1. Find endpoint in: K6_LOAD_TESTING_GUIDE.md or K6_ENDPOINTS_SUMMARY.md
|
||||
2. Get data shape from: K6_ENDPOINTS_SUMMARY.md (📊 Key Data Shapes)
|
||||
3. Check auth from: K6_LOAD_TESTING_GUIDE.md (each module section)
|
||||
4. Implement using examples in: K6_QUICK_START.md
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Checklist
|
||||
|
||||
Before running load tests, verify:
|
||||
|
||||
- [ ] API running: `pnpm dev` (port 3001)
|
||||
- [ ] Database seeded: `pnpm db:seed`
|
||||
- [ ] K6 installed: `k6 version`
|
||||
- [ ] Can reach API: `curl http://localhost:3001/api/v1/docs`
|
||||
- [ ] ENV variables set: `JWT_SECRET`, `CORS_ORIGINS`, etc.
|
||||
- [ ] Load test file exists: `load-tests/*.k6.js`
|
||||
- [ ] Test data available: Check seed in `prisma/seed.ts`
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support & References
|
||||
|
||||
### Internal Documentation
|
||||
- **Full Architecture**: K6_LOAD_TESTING_GUIDE.md
|
||||
- **Endpoint Reference**: K6_ENDPOINTS_SUMMARY.md
|
||||
- **Getting Started**: K6_QUICK_START.md
|
||||
|
||||
### External Resources
|
||||
- **K6 Official Docs**: https://k6.io/docs
|
||||
- **K6 API Reference**: https://k6.io/docs/javascript-api
|
||||
- **K6 Community**: https://community.k6.io
|
||||
- **K6 Examples**: https://github.com/grafana/k6-templates
|
||||
|
||||
### Project Files
|
||||
- **API Controllers**: `apps/api/src/modules/*/presentation/controllers/`
|
||||
- **DTOs**: `apps/api/src/modules/*/presentation/dto/`
|
||||
- **E2E Tests**: `e2e/api/`
|
||||
- **Seed Data**: `prisma/seed.ts`
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learning Path
|
||||
|
||||
### Beginner (30 minutes)
|
||||
1. Read K6_QUICK_START.md (Steps 1-4)
|
||||
2. Install K6
|
||||
3. Run: `k6 run load-tests/search.k6.js`
|
||||
|
||||
### Intermediate (1-2 hours)
|
||||
1. Read K6_ENDPOINTS_SUMMARY.md
|
||||
2. Understand auth flows
|
||||
3. Run auth test: `k6 run load-tests/auth.k6.js`
|
||||
4. Run listing test: `k6 run load-tests/listings.k6.js`
|
||||
|
||||
### Advanced (2-4 hours)
|
||||
1. Read K6_LOAD_TESTING_GUIDE.md completely
|
||||
2. Review controller implementations in source
|
||||
3. Create custom load test script
|
||||
4. Set up CI/CD with GitHub Actions (K6_QUICK_START.md Step 🔟)
|
||||
5. Generate and analyze reports
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- **No existing K6 setup** — These docs provide complete guidance
|
||||
- **Three complementary docs** — Explore different docs for different needs
|
||||
- **Executable examples** — K6_QUICK_START.md scripts work as-is
|
||||
- **Rate limits matter** — Consider them in test design
|
||||
- **Quota gates** — Some operations (listings, payments) are gated by subscription
|
||||
- **Test data** — Use seed data or generate unique test users per VU
|
||||
- **Production ready** — Guides follow K6 best practices
|
||||
|
||||
---
|
||||
|
||||
Generated: 2026-04-09
|
||||
Last Updated: K6_QUICK_START.md latest
|
||||
|
||||
Reference in New Issue
Block a user