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:
Ho Ngoc Hai
2026-04-13 12:09:14 +07:00
parent ccfc176e40
commit b93c28fa01
38 changed files with 252 additions and 412 deletions

View 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`
---

View 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`)

View 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
---

View 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