Files
goodgo-platform/K6_LOAD_TESTING_GUIDE.md
Ho Ngoc Hai a1a44ef8fb docs: add project documentation — changelog, QA tracker, audit reports, and guides
Add comprehensive project documentation including changelog, QA tracker,
code quality audit, implementation guide, K6 load testing guide, frontend
exploration notes, and file mapping reference.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 09:44:53 +07:00

22 KiB

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)

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

{
  phone: string        // Required, example: "0901234567"
  password: string     // Required, example: "P@ssw0rd!"
}

RegisterDto

{
  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

{
  refreshToken?: string  // Optional if using cookie
}

VerifyKycDto

{
  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

{
  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

{
  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

{
  status: string,
  moderationNotes?: string,
}

ModerateListingDto

{
  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

{
  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

{
  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

{
  status?: string,
  limit?: number,
  offset?: number,
}

RefundPaymentDto

{
  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

{
  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
}
{
  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

{
  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

# 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

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

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

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)

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:

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

# 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

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

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

  • API Swagger Docs: http://localhost:3001/api/v1/docs
  • Project Root Docs: CLAUDE.md
  • Existing Analysis: CODEBASE_ANALYSIS.md, EXPLORATION_REPORT.md
  • Frontend Docs: 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)