feat: QA test suite (12 stages, 205 tests) + Chrome DevTools MCP config

- Add tests/ directory with 12 bash/curl/jq test stages covering all API endpoints
- Add tests/lib/common.sh shared library with assertions and helpers
- Add tests/run-all.sh orchestrator script
- Update CLAUDE.md with test data reference and DevTools MCP docs
- Increase dev rate limit to 5000 for test suite runs
- Configure Chrome DevTools MCP in project settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
delta-lynx-89e8
2026-02-23 06:17:16 -08:00
parent 167e5a9a8b
commit c0123ac776
17 changed files with 1376 additions and 55 deletions

View File

@@ -4,10 +4,22 @@
"type": "http", "type": "http",
"url": "https://mcp.figma.com/mcp" "url": "https://mcp.figma.com/mcp"
}, },
"chrome-devtools": { "devtools": {
"type": "stdio", "type": "stdio",
"command": "npx", "command": "/opt/homebrew/bin/chrome-devtools-mcp",
"args": ["-y", "chrome-devtools-mcp@latest", "--no-usage-statistics", "--isolated"] "args": [
"--no-usage-statistics",
"--no-performance-crux",
"--isolated",
"--chromeArg=--user-data-dir=/tmp/chrome-mcp-profile",
"--chromeArg=--no-first-run",
"--chromeArg=--disable-background-networking",
"--chromeArg=--disable-sync",
"--chromeArg=--disable-translate"
],
"env": {
"CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS": "1"
}
} }
} }
} }

152
CLAUDE.md
View File

@@ -1,68 +1,118 @@
# Marketplace Project Notes # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Full-stack marketplace application with two domains: a **classified listings marketplace** (buy/sell items) and a **rental platform** (apartments, houses, cars, bicycles). Includes admin/moderation tools, Stripe payments, real-time chat via Socket.io, and subscription tiers.
## Architecture
- **Monorepo** using npm workspaces (`client/` and `server/`)
- **Client**: React 19 + TypeScript + Vite + Tailwind CSS 4 (port 5173)
- **Server**: Express + TypeScript + Prisma ORM + PostgreSQL (port 3000)
- **Real-time**: Socket.io for messaging and notifications
- **Payments**: Stripe (listings, rentals, subscriptions, promotions)
- **Auth**: JWT access tokens (15min) + httpOnly refresh token cookies (7 days), bcrypt passwords
- **Validation**: Zod schemas on server (`server/src/validators/`), TypeScript on client
- **State management**: React Context API (AuthContext), no Redux/Zustand
- **Client path alias**: `@/*` maps to `client/src/*`
## Commands
### Development
```bash
docker start marketplace-postgres # Start PostgreSQL (required)
npm run dev # Run client + server together
npm run dev:client # Client only (port 5173)
npm run dev:server # Server only (port 3000)
```
### Database
```bash
npm run db:generate --workspace=server # Regenerate Prisma client
npm run db:push --workspace=server # Push schema to DB (no migration)
npm run db:migrate --workspace=server # Create and apply migration
npm run db:seed --workspace=server # Seed test data
npm run db:studio --workspace=server # Open Prisma Studio GUI
```
Schema location: `server/prisma/schema.prisma`
### Build & Lint
```bash
npm run build # Build both client and server
npm run lint # ESLint (client only)
```
### Testing (QA Suite)
```bash
bash tests/run-all.sh # Run all 12 stages locally
bash tests/stage-02-auth.sh # Run single stage
BASE_URL=https://marketplace.173.212.212.157.sslip.io bash tests/run-all.sh # Against production
```
Tests location: `tests/` — 12 stage scripts, `tests/lib/common.sh` shared library. Requires: bash, curl, jq.
### Seed Data
| Email | ID | Role | Landlord | Key data |
|-------|-----|------|----------|----------|
| alice.chen@example.com | user-alice | SUPER_ADMIN | No | Listings 01,06,11. Offers, bookings, PRO subscription |
| bob.martinez@example.com | user-bob | ADMIN | No | Listings 08,10,15 |
| carol.nguyen@example.com | user-carol | MODERATOR | No | Listings 02,05,12. Blocked user-david |
| david.kim@example.com | user-david | USER | Yes (verified) | Rentals 01,02,06,07. Listings 04,07,13 |
| eva.johnson@example.com | user-eva | USER | Yes | Rentals 03,04,05. Listings 03,09,14 |
All passwords: `password123`
**Key IDs**: listings `listing-01`..`listing-15` | rentals `rental-01`..`rental-07` | bookings `booking-01`..`booking-04` | offers `offer-01`..`offer-10`
## Database: PostgreSQL via Docker ## Database: PostgreSQL via Docker
Container name: `marketplace-postgres` Container: `marketplace-postgres` | Image: `postgres:17-alpine` | Volume: `marketplace-pgdata`
Image: `postgres:17-alpine`
Volume: `marketplace-pgdata` (persistent local data)
### Start database Connection string: `postgresql://marketplace:marketplace_dev@localhost:5432/marketplace`
If container was deleted, recreate:
```bash ```bash
docker start marketplace-postgres docker run -d --name marketplace-postgres \
-e POSTGRES_USER=marketplace -e POSTGRES_PASSWORD=marketplace_dev -e POSTGRES_DB=marketplace \
-p 5432:5432 -v marketplace-pgdata:/var/lib/postgresql/data \
--restart unless-stopped postgres:17-alpine
``` ```
### Stop database (frees memory) ## Key Server Patterns
```bash
docker stop marketplace-postgres
```
### Check status - **Routes**: `server/src/routes/` — RESTful endpoints, admin routes nested under `routes/admin/`
```bash - **Middleware**: `authenticate` (required auth), `optionalAuth` (anonymous OK), `requireRole` (role check), `checkBanned`, `validate` (Zod)
docker ps -f name=marketplace-postgres - **Prisma client**: imported from `server/src/config/database.ts`
``` - **Environment config**: `server/src/config/env.ts`
- **File uploads**: Multer to `server/uploads/`, served statically
- **User roles**: USER, MODERATOR, ADMIN, SUPER_ADMIN — role hierarchy enforced via `requireRole` middleware
### Connection string ## Key Client Patterns
```
postgresql://marketplace:marketplace_dev@localhost:5432/marketplace
```
### If container was deleted, recreate: - **Router**: `client/src/router.tsx` — React Router v7 with `createBrowserRouter`
```bash - **Auth guards**: `<RequireAuth>` wraps protected routes, `<RequireRole>` checks roles
docker run -d \ - **API client**: `client/src/api/client.ts` — fetch wrapper that handles auth headers and token refresh
--name marketplace-postgres \ - **UI components**: `client/src/components/ui/` — Button, Input, Modal, Card, Badge, DataTable, etc.
-e POSTGRES_USER=marketplace \ - **Pages organized by domain**: root pages, `pages/admin/`, `pages/landlord/`
-e POSTGRES_PASSWORD=marketplace_dev \
-e POSTGRES_DB=marketplace \
-p 5432:5432 \
-v marketplace-pgdata:/var/lib/postgresql/data \
--restart unless-stopped \
postgres:17-alpine
```
Data persists in the `marketplace-pgdata` Docker volume even if the container is removed. ## Chrome DevTools MCP (Browser Testing)
## Running the app Chrome DevTools MCP is configured for automated browser testing. It launches an isolated, telemetry-free Chrome instance.
1. Start database: `docker start marketplace-postgres` - **Config location**: `~/.claude.json``mcpServers.chrome-devtools` (NOT `~/.claude/settings.json`)
2. Server: `npm run dev:server` (port 3000) - **Binary**: `/opt/homebrew/bin/chrome-devtools-mcp` (installed globally via npm)
3. Client: `npm run dev:client` (port 5173) - **Chrome profile**: `/tmp/chrome-mcp-profile` (isolated, no Google account)
4. Or both: `npm run dev` - **Key flags**: `--isolated` (auto-launches Chrome), `--disable-sync`, `--disable-background-networking`
- **Usage**: Available automatically when Claude Code starts. Use `take_snapshot`, `click`, `fill`, `navigate_page`, `take_screenshot` etc.
- **If MCP won't start**: Kill zombie processes (`pkill -f chrome-devtools-mcp`), remove lock (`rm /tmp/chrome-mcp-profile/SingletonLock`), restart Claude Code
## Seed data ## Key Env Vars (server/.env)
```bash - `DATABASE_URL` — PostgreSQL connection string
cd server && npx tsx prisma/seed.ts - `JWT_SECRET` / `JWT_REFRESH_SECRET` — Token signing
```
Test users (all password: `password123`):
- alice.chen@example.com
- bob.smith@example.com
- carol.jones@example.com
- david.wilson@example.com
- eva.martinez@example.com
## Key env vars (server/.env)
- `DATABASE_URL` — PostgreSQL connection
- `GOOGLE_MAPS_API_KEY` — Location autocomplete - `GOOGLE_MAPS_API_KEY` — Location autocomplete
- `STRIPE_SECRET_KEY` / `STRIPE_PUBLISHABLE_KEY` — Payments (optional for dev) - `STRIPE_SECRET_KEY` / `STRIPE_PUBLISHABLE_KEY` — Payments (optional for dev)
- `CLIENT_URL` — CORS origin (default: http://localhost:5173)

View File

@@ -49,7 +49,7 @@ app.use('/api/rental-payments/webhook', express.raw({ type: 'application/json' }
app.use(express.json()); app.use(express.json());
// Rate limiting // Rate limiting
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 50 }); const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: env.NODE_ENV === 'production' ? 50 : 5000 });
app.use('/api/auth/login', authLimiter); app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter); app.use('/api/auth/register', authLimiter);

133
tests/lib/common.sh Normal file
View File

@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# tests/lib/common.sh — Shared testing utilities
BASE_URL="${BASE_URL:-http://localhost:3000}"
CLIENT_URL="${CLIENT_URL:-http://localhost:5173}"
PASS_COUNT=0
FAIL_COUNT=0
SKIP_COUNT=0
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# --- HTTP helpers ---
api_call() {
local method="$1" path="$2" token="$3" data="$4"
local args=(-s -w '\n%{http_code}' -X "$method")
[[ -n "$token" ]] && args+=(-H "Authorization: Bearer $token")
[[ -n "$data" ]] && args+=(-H "Content-Type: application/json" -d "$data")
curl "${args[@]}" "${BASE_URL}${path}" 2>/dev/null
}
get_body() { echo "$1" | sed '$ d'; }
get_status() { echo "$1" | tail -n1; }
# --- Login helper (returns token, retries on rate limit) ---
login_as() {
local email="$1" password="${2:-password123}"
local result body status attempt
for attempt in 1 2 3 4 5; do
result=$(api_call POST "/api/auth/login" "" "{\"email\":\"$email\",\"password\":\"$password\"}")
status=$(get_status "$result")
if [[ "$status" == "429" || "$status" == "500" || "$status" == "503" ]]; then
sleep "$attempt"
continue
fi
body=$(get_body "$result")
echo "$body" | jq -r '.accessToken // empty' 2>/dev/null
return
done
echo ""
}
# --- Assertions ---
assert_status() {
local name="$1" expected="$2" actual="$3"
if [[ "$expected" == "$actual" ]]; then
printf " ${GREEN}PASS${NC} %s (HTTP %s)\n" "$name" "$actual"
((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} %s (expected %s, got %s)\n" "$name" "$expected" "$actual"
((FAIL_COUNT++))
fi
}
assert_json() {
local name="$1" body="$2" field="$3" expected="$4"
local actual
actual=$(echo "$body" | jq -r "$field" 2>/dev/null)
if [[ "$actual" == "$expected" ]]; then
printf " ${GREEN}PASS${NC} %s (%s = %s)\n" "$name" "$field" "$actual"
((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} %s (%s: expected '%s', got '%s')\n" "$name" "$field" "$expected" "$actual"
((FAIL_COUNT++))
fi
}
assert_json_exists() {
local name="$1" body="$2" field="$3"
local actual
actual=$(echo "$body" | jq -r "$field" 2>/dev/null)
if [[ -n "$actual" && "$actual" != "null" ]]; then
printf " ${GREEN}PASS${NC} %s (%s exists)\n" "$name" "$field"
((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} %s (%s is null/missing)\n" "$name" "$field"
((FAIL_COUNT++))
fi
}
assert_json_array_min() {
local name="$1" body="$2" field="$3" min="${4:-1}"
local length
length=$(echo "$body" | jq "$field | length" 2>/dev/null)
if [[ "$length" -ge "$min" ]] 2>/dev/null; then
printf " ${GREEN}PASS${NC} %s (%s items)\n" "$name" "$length"
((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} %s (expected >= %s items, got %s)\n" "$name" "$min" "$length"
((FAIL_COUNT++))
fi
}
assert_contains() {
local name="$1" body="$2" substring="$3"
if echo "$body" | grep -q "$substring"; then
printf " ${GREEN}PASS${NC} %s (contains '%s')\n" "$name" "$substring"
((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} %s (missing '%s')\n" "$name" "$substring"
((FAIL_COUNT++))
fi
}
skip_test() {
local name="$1" reason="$2"
printf " ${YELLOW}SKIP${NC} %s (%s)\n" "$name" "$reason"
((SKIP_COUNT++))
}
# --- Stage header/summary ---
stage_header() {
echo ""
printf "${CYAN}${BOLD}════════════════════════════════════════${NC}\n"
printf "${CYAN}${BOLD} %s${NC}\n" "$1"
printf "${CYAN}${BOLD}════════════════════════════════════════${NC}\n"
}
print_summary() {
local name="$1"
echo ""
printf "${BOLD}--- %s Results ---${NC}\n" "$name"
printf " ${GREEN}Passed: %d${NC}\n" "$PASS_COUNT"
[[ $FAIL_COUNT -gt 0 ]] && printf " ${RED}Failed: %d${NC}\n" "$FAIL_COUNT"
[[ $SKIP_COUNT -gt 0 ]] && printf " ${YELLOW}Skipped: %d${NC}\n" "$SKIP_COUNT"
printf " Total: %d\n" "$((PASS_COUNT + FAIL_COUNT + SKIP_COUNT))"
[[ $FAIL_COUNT -gt 0 ]] && return 1
return 0
}

66
tests/run-all.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
FAILED_STAGES=()
PASSED_STAGES=()
TOTAL_STAGES=0
run_stage() {
local script="$1" name="$2"
((TOTAL_STAGES++))
sleep 1 # Brief pause between stages to avoid overwhelming the server
if bash "$SCRIPT_DIR/$script"; then
PASSED_STAGES+=("$name")
else
FAILED_STAGES+=("$name")
fi
}
echo ""
printf "${BOLD}╔══════════════════════════════════════════╗${NC}\n"
printf "${BOLD}║ Marketplace QA Test Suite ║${NC}\n"
printf "${BOLD}╚══════════════════════════════════════════╝${NC}\n"
echo " API: ${BASE_URL:-http://localhost:3000}"
echo " Client: ${CLIENT_URL:-http://localhost:5173}"
echo ""
run_stage "stage-01-infrastructure.sh" "Infrastructure"
run_stage "stage-02-auth.sh" "Authentication"
run_stage "stage-03-public.sh" "Public Endpoints"
run_stage "stage-04-user-crud.sh" "User CRUD"
run_stage "stage-05-listings.sh" "Listings Workflow"
run_stage "stage-06-offers.sh" "Offers Workflow"
run_stage "stage-07-rentals.sh" "Rental Workflow"
run_stage "stage-08-subscriptions.sh" "Subscriptions"
run_stage "stage-09-chat-notifications.sh" "Chat & Notifications"
run_stage "stage-10-admin.sh" "Admin Operations"
run_stage "stage-11-edge-cases.sh" "Edge Cases"
# Stage 12 only if client running
CLIENT_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${CLIENT_URL:-http://localhost:5173}" 2>/dev/null)
if [[ "$CLIENT_STATUS" == "200" ]]; then
run_stage "stage-12-client-pages.sh" "Client Pages"
else
printf "\n${YELLOW}SKIP: Client not running — Stage 12 skipped${NC}\n"
fi
# Final report
echo ""
printf "${BOLD}╔══════════════════════════════════════════╗${NC}\n"
printf "${BOLD}║ FINAL RESULTS ║${NC}\n"
printf "${BOLD}╚══════════════════════════════════════════╝${NC}\n"
printf " Stages passed: ${GREEN}%d${NC} / %d\n" "${#PASSED_STAGES[@]}" "$TOTAL_STAGES"
if [[ ${#FAILED_STAGES[@]} -gt 0 ]]; then
printf " ${RED}Failed stages:${NC}\n"
for s in "${FAILED_STAGES[@]}"; do
printf " ${RED}✗ %s${NC}\n" "$s"
done
exit 1
else
printf " ${GREEN}ALL STAGES PASSED${NC}\n"
exit 0
fi

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 1: Infrastructure"
# 1.1 Health check
r=$(api_call GET "/api/health")
assert_status "1.1 Health endpoint" "200" "$(get_status "$r")"
assert_json "1.1 Health status" "$(get_body "$r")" ".status" "ok"
# 1.2 Listings endpoint (proves DB + Prisma)
r=$(api_call GET "/api/listings")
assert_status "1.2 Listings endpoint" "200" "$(get_status "$r")"
# 1.3 Rentals endpoint
r=$(api_call GET "/api/rentals")
assert_status "1.3 Rentals endpoint" "200" "$(get_status "$r")"
# 1.4 Subscription tiers (proves platform config)
r=$(api_call GET "/api/subscriptions/tiers")
assert_status "1.4 Subscription tiers" "200" "$(get_status "$r")"
assert_json_array_min "1.4 Tiers count" "$(get_body "$r")" "." 3
print_summary "Stage 1: Infrastructure"

76
tests/stage-02-auth.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 2: Authentication"
TEST_EMAIL="qa-test-$(date +%s)@example.com"
TEST_USER_ID=""
ALICE_TOKEN=""
cleanup() {
: # test user stays (unique email, no harm)
}
trap cleanup EXIT
# 2.1 Register new user
r=$(api_call POST "/api/auth/register" "" "{\"fullName\":\"QA Test User\",\"email\":\"$TEST_EMAIL\",\"password\":\"TestPass123\"}")
assert_status "2.1 Register valid user" "201" "$(get_status "$r")"
b=$(get_body "$r")
assert_json_exists "2.1 Token returned" "$b" ".accessToken"
TEST_TOKEN=$(echo "$b" | jq -r '.accessToken // empty')
TEST_USER_ID=$(echo "$b" | jq -r '.user.id // empty')
# 2.2 Register duplicate email
r=$(api_call POST "/api/auth/register" "" "{\"fullName\":\"Dup\",\"email\":\"$TEST_EMAIL\",\"password\":\"TestPass123\"}")
assert_status "2.2 Duplicate email" "409" "$(get_status "$r")"
# 2.3 Register missing password
r=$(api_call POST "/api/auth/register" "" "{\"fullName\":\"No Pass\",\"email\":\"nopass@example.com\"}")
assert_status "2.3 Missing password" "400" "$(get_status "$r")"
# 2.4 Register short password
r=$(api_call POST "/api/auth/register" "" "{\"fullName\":\"Short\",\"email\":\"short@example.com\",\"password\":\"123\"}")
assert_status "2.4 Short password" "400" "$(get_status "$r")"
# 2.5 Login valid (Alice - SUPER_ADMIN)
r=$(api_call POST "/api/auth/login" "" '{"email":"alice.chen@example.com","password":"password123"}')
assert_status "2.5 Login Alice" "200" "$(get_status "$r")"
b=$(get_body "$r")
assert_json "2.5 Alice role" "$b" ".user.role" "SUPER_ADMIN"
ALICE_TOKEN=$(echo "$b" | jq -r '.accessToken')
# 2.6 Login wrong password
r=$(api_call POST "/api/auth/login" "" '{"email":"alice.chen@example.com","password":"wrong"}')
assert_status "2.6 Wrong password" "401" "$(get_status "$r")"
# 2.7 Login nonexistent email
r=$(api_call POST "/api/auth/login" "" '{"email":"nobody@example.com","password":"pass"}')
assert_status "2.7 Nonexistent email" "401" "$(get_status "$r")"
# 2.8 GET /me with valid token
r=$(api_call GET "/api/auth/me" "$ALICE_TOKEN")
assert_status "2.8 GET /me valid" "200" "$(get_status "$r")"
assert_json "2.8 Correct user" "$(get_body "$r")" ".user.email" "alice.chen@example.com"
# 2.9 GET /me no token
r=$(api_call GET "/api/auth/me")
assert_status "2.9 GET /me no auth" "401" "$(get_status "$r")"
# 2.10 GET /me garbage token
r=$(api_call GET "/api/auth/me" "invalidtoken123")
assert_status "2.10 GET /me bad token" "401" "$(get_status "$r")"
# 2.11 Logout
r=$(api_call POST "/api/auth/logout" "$TEST_TOKEN")
assert_status "2.11 Logout" "200" "$(get_status "$r")"
# 2.12 Login all seed users
for user in "bob.martinez@example.com:ADMIN" "carol.nguyen@example.com:MODERATOR" "david.kim@example.com:USER" "eva.johnson@example.com:USER"; do
email="${user%%:*}"
role="${user##*:}"
r=$(api_call POST "/api/auth/login" "" "{\"email\":\"$email\",\"password\":\"password123\"}")
assert_status "2.12 Login $email" "200" "$(get_status "$r")"
assert_json "2.12 Role $email" "$(get_body "$r")" ".user.role" "$role"
done
print_summary "Stage 2: Authentication"

102
tests/stage-03-public.sh Executable file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 3: Public Endpoints"
# 3.1 List all listings
r=$(api_call GET "/api/listings")
assert_status "3.1 GET /listings" "200" "$(get_status "$r")"
b=$(get_body "$r")
assert_json_array_min "3.1 Has listings" "$b" ".data" 1
# 3.2 Category filter
r=$(api_call GET "/api/listings?category=ELECTRONICS")
b=$(get_body "$r")
assert_status "3.2 Filter ELECTRONICS" "200" "$(get_status "$r")"
count=$(echo "$b" | jq '[.data[] | select(.category != "ELECTRONICS")] | length')
assert_json "3.2 All are ELECTRONICS" "$b" '[.data[] | select(.category != "ELECTRONICS")] | length' "0"
# 3.3 Search
r=$(api_call GET "/api/listings?search=MacBook")
assert_status "3.3 Search MacBook" "200" "$(get_status "$r")"
b=$(get_body "$r")
total=$(echo "$b" | jq '.total')
if [[ "$total" -ge 1 ]]; then
printf " ${GREEN}PASS${NC} 3.3 Found %s results\n" "$total"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 3.3 Expected >= 1, got %s\n" "$total"; ((FAIL_COUNT++))
fi
# 3.4 Sort by price ascending
r=$(api_call GET "/api/listings?sort=price_asc")
b=$(get_body "$r")
first=$(echo "$b" | jq '.data[0].price')
last=$(echo "$b" | jq '.data[-1].price')
if awk "BEGIN{exit !($first <= $last)}"; then
printf " ${GREEN}PASS${NC} 3.4 Price sort asc (%s <= %s)\n" "$first" "$last"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 3.4 Price sort asc (%s > %s)\n" "$first" "$last"; ((FAIL_COUNT++))
fi
# 3.5 Pagination
r=$(api_call GET "/api/listings?page=1&pageSize=2")
b=$(get_body "$r")
assert_status "3.5 Pagination" "200" "$(get_status "$r")"
len=$(echo "$b" | jq '.data | length')
if [[ "$len" -le 2 ]]; then
printf " ${GREEN}PASS${NC} 3.5 Page size respected (%s items)\n" "$len"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 3.5 Page size not respected (%s items)\n" "$len"; ((FAIL_COUNT++))
fi
# 3.6 Single listing
r=$(api_call GET "/api/listings/listing-01")
assert_status "3.6 GET listing-01" "200" "$(get_status "$r")"
assert_contains "3.6 Has MacBook" "$(get_body "$r")" "MacBook"
# 3.7 Non-existent listing
r=$(api_call GET "/api/listings/nonexistent-id")
assert_status "3.7 404 on missing" "404" "$(get_status "$r")"
# 3.8 List rentals
r=$(api_call GET "/api/rentals")
assert_status "3.8 GET /rentals" "200" "$(get_status "$r")"
assert_json_array_min "3.8 Has rentals" "$(get_body "$r")" ".data" 1
# 3.9 Rental category filter
r=$(api_call GET "/api/rentals?category=APARTMENT")
assert_status "3.9 Filter APARTMENT" "200" "$(get_status "$r")"
# 3.10 Rental search
r=$(api_call GET "/api/rentals?search=Tesla")
b=$(get_body "$r")
assert_status "3.10 Search Tesla" "200" "$(get_status "$r")"
# 3.11 Rental sort + period
r=$(api_call GET "/api/rentals?sort=price_asc&periodType=DAILY")
assert_status "3.11 Rental sort" "200" "$(get_status "$r")"
# 3.12 Single rental
r=$(api_call GET "/api/rentals/rental-01")
assert_status "3.12 GET rental-01" "200" "$(get_status "$r")"
assert_json_exists "3.12 Has landlord" "$(get_body "$r")" ".landlord.fullName"
# 3.13 Availability (public, no auth)
r=$(api_call GET "/api/rentals/rental-01/availability")
assert_status "3.13 Availability" "200" "$(get_status "$r")"
# 3.14 Landlord reviews (public)
r=$(api_call GET "/api/rental-reviews/landlord/user-david")
assert_status "3.14 Landlord reviews" "200" "$(get_status "$r")"
# 3.15 Public user profile
r=$(api_call GET "/api/users/user-alice")
assert_status "3.15 Public profile" "200" "$(get_status "$r")"
assert_json "3.15 Correct name" "$(get_body "$r")" ".fullName" "Alice Chen"
# 3.16 Subscription tiers
r=$(api_call GET "/api/subscriptions/tiers")
assert_status "3.16 Tiers list" "200" "$(get_status "$r")"
assert_json_array_min "3.16 Has 3 tiers" "$(get_body "$r")" "." 3
print_summary "Stage 3: Public Endpoints"

98
tests/stage-04-user-crud.sh Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 4: User CRUD"
EVA_TOKEN=$(login_as "eva.johnson@example.com")
if [[ -z "$EVA_TOKEN" ]]; then echo "FATAL: Cannot login as Eva"; exit 1; fi
ORIGINAL_BIO=""
cleanup() {
# Restore Eva's bio if changed
if [[ -n "$ORIGINAL_BIO" ]]; then
api_call PUT "/api/users/profile" "$EVA_TOKEN" "{\"bio\":\"$ORIGINAL_BIO\"}" >/dev/null 2>&1
fi
# Unblock Bob if blocked
api_call DELETE "/api/users/user-bob/block" "$EVA_TOKEN" >/dev/null 2>&1
}
trap cleanup EXIT
# 4.1 Get own profile
r=$(api_call GET "/api/users/profile" "$EVA_TOKEN")
assert_status "4.1 GET /profile" "200" "$(get_status "$r")"
b=$(get_body "$r")
assert_json "4.1 Correct name" "$b" ".fullName" "Eva Johnson"
ORIGINAL_BIO=$(echo "$b" | jq -r '.bio // ""')
# 4.2 Update bio
r=$(api_call PUT "/api/users/profile" "$EVA_TOKEN" '{"bio":"QA test bio update"}')
assert_status "4.2 Update bio" "200" "$(get_status "$r")"
assert_json "4.2 Bio updated" "$(get_body "$r")" ".bio" "QA test bio update"
# 4.3 Get settings
r=$(api_call GET "/api/users/settings" "$EVA_TOKEN")
assert_status "4.3 GET /settings" "200" "$(get_status "$r")"
# 4.4 Update settings
r=$(api_call PUT "/api/users/settings" "$EVA_TOKEN" '{"showOnlineStatus":false}')
assert_status "4.4 Update settings" "200" "$(get_status "$r")"
# Restore
api_call PUT "/api/users/settings" "$EVA_TOKEN" '{"showOnlineStatus":true}' >/dev/null
# 4.5 Sessions
r=$(api_call GET "/api/users/sessions" "$EVA_TOKEN")
assert_status "4.5 GET /sessions" "200" "$(get_status "$r")"
# 4.6 Change password (use temp user, not seed user — safer for idempotency)
PW_EMAIL="qa-pw-$(date +%s)@example.com"
r=$(api_call POST "/api/auth/register" "" "{\"fullName\":\"PW Test\",\"email\":\"$PW_EMAIL\",\"password\":\"OrigPass123\"}")
PW_TOKEN=$(get_body "$r" | jq -r '.accessToken // empty')
if [[ -n "$PW_TOKEN" ]]; then
r=$(api_call PUT "/api/users/password" "$PW_TOKEN" '{"currentPassword":"OrigPass123","newPassword":"NewPass456"}')
assert_status "4.6 Change password" "200" "$(get_status "$r")"
# Verify login with new password
PW_TOKEN2=$(login_as "$PW_EMAIL" "NewPass456")
if [[ -n "$PW_TOKEN2" ]]; then
printf " ${GREEN}PASS${NC} 4.6 Login with new password works\n"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 4.6 Cannot login with new password\n"; ((FAIL_COUNT++))
fi
else
skip_test "4.6 Change password" "Could not register temp user"
fi
# 4.7 Wrong current password
r=$(api_call PUT "/api/users/password" "$EVA_TOKEN" '{"currentPassword":"wrongpw","newPassword":"anything"}')
s=$(get_status "$r")
if [[ "$s" == "400" || "$s" == "401" ]]; then
printf " ${GREEN}PASS${NC} 4.7 Wrong password rejected (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 4.7 Expected 400/401, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# 4.8 Toggle favorite
r=$(api_call POST "/api/listings/listing-01/favorite" "$EVA_TOKEN")
assert_status "4.8 Toggle favorite" "200" "$(get_status "$r")"
# 4.9 Get favorites
r=$(api_call GET "/api/listings/favorites" "$EVA_TOKEN")
assert_status "4.9 GET /favorites" "200" "$(get_status "$r")"
# 4.10 Block user
r=$(api_call POST "/api/users/user-bob/block" "$EVA_TOKEN")
assert_status "4.10 Block Bob" "200" "$(get_status "$r")"
# 4.11 Cannot block self
r=$(api_call POST "/api/users/user-eva/block" "$EVA_TOKEN")
s=$(get_status "$r")
if [[ "$s" == "400" || "$s" == "409" ]]; then
printf " ${GREEN}PASS${NC} 4.11 Cannot block self (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 4.11 Expected 400/409, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# 4.12 Unblock
r=$(api_call DELETE "/api/users/user-bob/block" "$EVA_TOKEN")
assert_status "4.12 Unblock Bob" "200" "$(get_status "$r")"
print_summary "Stage 4: User CRUD"

78
tests/stage-05-listings.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 5: Listings Workflow"
BOB_TOKEN=$(login_as "bob.martinez@example.com")
ALICE_TOKEN=$(login_as "alice.chen@example.com")
if [[ -z "$BOB_TOKEN" || -z "$ALICE_TOKEN" ]]; then echo "FATAL: Cannot login"; exit 1; fi
LISTING_ID=""
cleanup() {
# Delete test listing if created
if [[ -n "$LISTING_ID" ]]; then
api_call DELETE "/api/listings/$LISTING_ID" "$BOB_TOKEN" >/dev/null 2>&1
fi
}
trap cleanup EXIT
# 5.1 Create listing
r=$(api_call POST "/api/listings" "$BOB_TOKEN" '{"title":"QA Test Listing","description":"A test listing created by automated QA suite","price":49.99,"category":"ELECTRONICS","condition":"LIKE_NEW","location":"Test City, QA"}')
assert_status "5.1 Create listing" "201" "$(get_status "$r")"
b=$(get_body "$r")
LISTING_ID=$(echo "$b" | jq -r '.id // empty')
assert_json "5.1 Status DRAFT" "$b" ".status" "DRAFT"
# 5.2 Activate listing
r=$(api_call POST "/api/listings/$LISTING_ID/activate" "$BOB_TOKEN")
assert_status "5.2 Activate listing" "200" "$(get_status "$r")"
b=$(get_body "$r")
# autoApprove=true → ACTIVE; else PENDING_REVIEW
status=$(echo "$b" | jq -r '.status')
if [[ "$status" == "ACTIVE" || "$status" == "PENDING_REVIEW" ]]; then
printf " ${GREEN}PASS${NC} 5.2 Status is %s\n" "$status"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 5.2 Unexpected status: %s\n" "$status"; ((FAIL_COUNT++))
fi
# 5.3 Get own listings
r=$(api_call GET "/api/listings/mine" "$BOB_TOKEN")
assert_status "5.3 GET /mine" "200" "$(get_status "$r")"
assert_contains "5.3 Contains test listing" "$(get_body "$r")" "QA Test Listing"
# 5.4 Update listing
r=$(api_call PUT "/api/listings/$LISTING_ID" "$BOB_TOKEN" '{"title":"QA Test Listing Updated","price":39.99}')
assert_status "5.4 Update listing" "200" "$(get_status "$r")"
assert_json "5.4 Title updated" "$(get_body "$r")" ".title" "QA Test Listing Updated"
# 5.5 Get single listing
r=$(api_call GET "/api/listings/$LISTING_ID")
assert_status "5.5 GET single" "200" "$(get_status "$r")"
assert_json "5.5 Price updated" "$(get_body "$r")" ".price" "39.99"
# 5.6 Cannot edit someone else's listing
r=$(api_call PUT "/api/listings/$LISTING_ID" "$ALICE_TOKEN" '{"title":"Hacked"}')
assert_status "5.6 Edit other's listing" "403" "$(get_status "$r")"
# 5.7 Cannot delete someone else's listing
r=$(api_call DELETE "/api/listings/$LISTING_ID" "$ALICE_TOKEN")
assert_status "5.7 Delete other's listing" "403" "$(get_status "$r")"
# 5.8 Delete own listing
r=$(api_call DELETE "/api/listings/$LISTING_ID" "$BOB_TOKEN")
assert_status "5.8 Delete own listing" "200" "$(get_status "$r")"
LISTING_ID="" # Cleared for cleanup
# 5.9 Create with missing title
r=$(api_call POST "/api/listings" "$BOB_TOKEN" '{"description":"desc that is long enough","price":10,"category":"OTHER","condition":"NEW","location":"Here"}')
assert_status "5.9 Missing title" "400" "$(get_status "$r")"
# 5.10 Create with negative price
r=$(api_call POST "/api/listings" "$BOB_TOKEN" '{"title":"Bad Price","description":"desc that is long enough","price":-5,"category":"OTHER","condition":"NEW","location":"Here"}')
assert_status "5.10 Negative price" "400" "$(get_status "$r")"
# 5.11 Sold items (Alice has sold items)
r=$(api_call GET "/api/listings/sold" "$ALICE_TOKEN")
assert_status "5.11 GET /sold" "200" "$(get_status "$r")"
print_summary "Stage 5: Listings Workflow"

96
tests/stage-06-offers.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 6: Offers Workflow"
ALICE_TOKEN=$(login_as "alice.chen@example.com")
EVA_TOKEN=$(login_as "eva.johnson@example.com")
DAVID_TOKEN=$(login_as "david.kim@example.com")
if [[ -z "$ALICE_TOKEN" || -z "$EVA_TOKEN" || -z "$DAVID_TOKEN" ]]; then echo "FATAL: Cannot login"; exit 1; fi
# Create a fresh listing for offer tests (so we don't mutate seed data)
TEST_LISTING_ID=""
OFFER_ID=""
OFFER2_ID=""
cleanup() {
# Cancel any pending offers
[[ -n "$OFFER_ID" ]] && api_call DELETE "/api/offers/$OFFER_ID" "$EVA_TOKEN" >/dev/null 2>&1
[[ -n "$OFFER2_ID" ]] && api_call DELETE "/api/offers/$OFFER2_ID" "$EVA_TOKEN" >/dev/null 2>&1
# Delete test listing
[[ -n "$TEST_LISTING_ID" ]] && api_call DELETE "/api/listings/$TEST_LISTING_ID" "$ALICE_TOKEN" >/dev/null 2>&1
}
trap cleanup EXIT
# Setup: create test listing as Alice
r=$(api_call POST "/api/listings" "$ALICE_TOKEN" '{"title":"Offer Test Item","description":"A listing created for testing offers workflow","price":500,"category":"ELECTRONICS","condition":"NEW","location":"QA City"}')
TEST_LISTING_ID=$(get_body "$r" | jq -r '.id // empty')
api_call POST "/api/listings/$TEST_LISTING_ID/activate" "$ALICE_TOKEN" >/dev/null
# 6.1 Create offer (Eva → Alice's listing)
r=$(api_call POST "/api/offers" "$EVA_TOKEN" "{\"amount\":400,\"message\":\"QA test offer\",\"listingId\":\"$TEST_LISTING_ID\"}")
assert_status "6.1 Create offer" "201" "$(get_status "$r")"
b=$(get_body "$r")
OFFER_ID=$(echo "$b" | jq -r '.id // empty')
assert_json "6.1 Status PENDING" "$b" ".status" "PENDING"
# 6.2 Duplicate offer (same buyer, same listing)
r=$(api_call POST "/api/offers" "$EVA_TOKEN" "{\"amount\":450,\"listingId\":\"$TEST_LISTING_ID\"}")
assert_status "6.2 Duplicate offer" "409" "$(get_status "$r")"
# 6.3 Get sent offers
r=$(api_call GET "/api/offers" "$EVA_TOKEN")
assert_status "6.3 GET offers (sent)" "200" "$(get_status "$r")"
# 6.4 Get received offers (as Alice)
r=$(api_call GET "/api/offers" "$ALICE_TOKEN")
assert_status "6.4 GET offers (received)" "200" "$(get_status "$r")"
# 6.5 Counter offer (Alice counters)
r=$(api_call PATCH "/api/offers/$OFFER_ID" "$ALICE_TOKEN" '{"status":"COUNTERED","counterAmount":450}')
assert_status "6.5 Counter offer" "200" "$(get_status "$r")"
assert_json "6.5 Status COUNTERED" "$(get_body "$r")" ".status" "COUNTERED"
# 6.6 Decline counter (Eva declines)
r=$(api_call PATCH "/api/offers/$OFFER_ID" "$EVA_TOKEN" '{"status":"DECLINED"}')
assert_status "6.6 Decline counter" "200" "$(get_status "$r")"
OFFER_ID="" # No longer pending
# 6.7 Self-offer prevention
r=$(api_call POST "/api/offers" "$ALICE_TOKEN" "{\"amount\":100,\"listingId\":\"$TEST_LISTING_ID\"}")
s=$(get_status "$r")
if [[ "$s" == "400" || "$s" == "403" ]]; then
printf " ${GREEN}PASS${NC} 6.7 Self-offer rejected (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 6.7 Expected 400/403, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# 6.8 New offer then accept (full cycle)
r=$(api_call POST "/api/offers" "$EVA_TOKEN" "{\"amount\":480,\"listingId\":\"$TEST_LISTING_ID\"}")
OFFER2_ID=$(get_body "$r" | jq -r '.id // empty')
assert_status "6.8 New offer" "201" "$(get_status "$r")"
r=$(api_call PATCH "/api/offers/$OFFER2_ID" "$ALICE_TOKEN" '{"status":"ACCEPTED"}')
assert_status "6.8 Accept offer" "200" "$(get_status "$r")"
assert_json "6.8 Status ACCEPTED" "$(get_body "$r")" ".status" "ACCEPTED"
OFFER2_ID="" # Completed, no need to cancel
TEST_LISTING_ID="" # Now SOLD, will 404 on delete
# 6.9 Offer on non-active listing (DRAFT)
r=$(api_call POST "/api/offers" "$EVA_TOKEN" '{"amount":300,"listingId":"listing-15"}')
assert_status "6.9 Offer on DRAFT" "400" "$(get_status "$r")"
# 6.10 Blocked user offer: Carol blocked David
r=$(api_call POST "/api/offers" "$DAVID_TOKEN" '{"amount":200,"listingId":"listing-02"}')
s=$(get_status "$r")
if [[ "$s" == "403" || "$s" == "400" ]]; then
printf " ${GREEN}PASS${NC} 6.10 Blocked user rejected (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 6.10 Expected 403/400, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# 6.11 Offer on non-existent listing
r=$(api_call POST "/api/offers" "$EVA_TOKEN" '{"amount":100,"listingId":"fake-listing-id"}')
assert_status "6.11 Non-existent listing" "404" "$(get_status "$r")"
print_summary "Stage 6: Offers Workflow"

156
tests/stage-07-rentals.sh Executable file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 7: Rental Workflow"
DAVID_TOKEN=$(login_as "david.kim@example.com")
ALICE_TOKEN=$(login_as "alice.chen@example.com")
CAROL_TOKEN=$(login_as "carol.nguyen@example.com")
if [[ -z "$DAVID_TOKEN" || -z "$ALICE_TOKEN" || -z "$CAROL_TOKEN" ]]; then echo "FATAL: Cannot login"; exit 1; fi
RENTAL_ID=""
BLOCK_ID=""
BOOKING_ID=""
REVIEW_ID=""
UPGRADED_SUB=""
cleanup() {
[[ -n "$RENTAL_ID" ]] && api_call DELETE "/api/rentals/$RENTAL_ID" "$DAVID_TOKEN" >/dev/null 2>&1
[[ -n "$UPGRADED_SUB" ]] && api_call POST "/api/subscriptions/cancel" "$DAVID_TOKEN" >/dev/null 2>&1
}
trap cleanup EXIT
# Upgrade David to PRO (he has 4 active rentals, BASIC limit is 3)
api_call POST "/api/subscriptions/create" "$DAVID_TOKEN" '{"tier":"PRO"}' >/dev/null 2>&1
UPGRADED_SUB="yes"
# 7.1 Create rental (David is verified landlord)
r=$(api_call POST "/api/rentals" "$DAVID_TOKEN" '{"title":"QA Test Rental Apartment","description":"A test rental listing for automated QA testing suite","category":"APARTMENT","location":"Chicago, IL","dailyPrice":100,"depositAmount":200,"minDays":1,"maxDays":30}')
assert_status "7.1 Create rental" "201" "$(get_status "$r")"
b=$(get_body "$r")
RENTAL_ID=$(echo "$b" | jq -r '.id // empty')
assert_json "7.1 Status DRAFT" "$b" ".status" "DRAFT"
# 7.2 Activate → PENDING_REVIEW (rentalAutoApprove=false)
r=$(api_call POST "/api/rentals/$RENTAL_ID/activate" "$DAVID_TOKEN")
assert_status "7.2 Activate rental" "200" "$(get_status "$r")"
b=$(get_body "$r")
status=$(echo "$b" | jq -r '.status')
if [[ "$status" == "PENDING_REVIEW" || "$status" == "ACTIVE" ]]; then
printf " ${GREEN}PASS${NC} 7.2 Status is %s\n" "$status"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 7.2 Unexpected status: %s\n" "$status"; ((FAIL_COUNT++))
fi
# 7.3 Admin approves (Carol is MODERATOR)
if [[ "$status" == "PENDING_REVIEW" ]]; then
r=$(api_call PATCH "/api/admin/rentals/$RENTAL_ID/approve" "$CAROL_TOKEN")
assert_status "7.3 Admin approve" "200" "$(get_status "$r")"
else
skip_test "7.3 Admin approve" "Already ACTIVE"
fi
# 7.4 Verify active
r=$(api_call GET "/api/rentals/$RENTAL_ID")
assert_status "7.4 GET rental" "200" "$(get_status "$r")"
assert_json "7.4 Status ACTIVE" "$(get_body "$r")" ".status" "ACTIVE"
# 7.5 Add availability block
BLOCK_START="2026-12-25T00:00:00.000Z"
BLOCK_END="2026-12-31T00:00:00.000Z"
r=$(api_call POST "/api/rentals/$RENTAL_ID/availability" "$DAVID_TOKEN" "{\"startDate\":\"$BLOCK_START\",\"endDate\":\"$BLOCK_END\",\"reason\":\"Holiday block\"}")
assert_status "7.5 Add availability block" "201" "$(get_status "$r")"
BLOCK_ID=$(get_body "$r" | jq -r '.id // empty')
# 7.6 Check availability
r=$(api_call GET "/api/rentals/$RENTAL_ID/availability")
assert_status "7.6 GET availability" "200" "$(get_status "$r")"
# 7.7 Remove block
if [[ -n "$BLOCK_ID" ]]; then
r=$(api_call DELETE "/api/rentals/$RENTAL_ID/availability/$BLOCK_ID" "$DAVID_TOKEN")
assert_status "7.7 Remove block" "200" "$(get_status "$r")"
fi
# 7.8 Create booking (Alice books David's rental)
START=$(date -v+60d -u +"%Y-%m-%dT00:00:00.000Z" 2>/dev/null || date -d "+60 days" -u +"%Y-%m-%dT00:00:00.000Z")
END=$(date -v+63d -u +"%Y-%m-%dT00:00:00.000Z" 2>/dev/null || date -d "+63 days" -u +"%Y-%m-%dT00:00:00.000Z")
r=$(api_call POST "/api/bookings" "$ALICE_TOKEN" "{\"rentalListingId\":\"$RENTAL_ID\",\"periodType\":\"DAILY\",\"startDate\":\"$START\",\"endDate\":\"$END\",\"message\":\"QA test booking\"}")
s=$(get_status "$r")
b=$(get_body "$r")
if [[ "$s" == "201" ]]; then
BOOKING_ID=$(echo "$b" | jq -r '.id // empty')
assert_status "7.8 Create booking" "201" "$s"
assert_json "7.8 Status PENDING" "$b" ".status" "PENDING"
else
printf " ${RED}FAIL${NC} 7.8 Create booking (HTTP %s: %s)\n" "$s" "$(echo "$b" | jq -r '.message // empty')"
((FAIL_COUNT++))
fi
# 7.9 Confirm booking (David)
if [[ -n "$BOOKING_ID" ]]; then
r=$(api_call PATCH "/api/bookings/$BOOKING_ID/confirm" "$DAVID_TOKEN")
assert_status "7.9 Confirm booking" "200" "$(get_status "$r")"
assert_json "7.9 Status CONFIRMED" "$(get_body "$r")" ".status" "CONFIRMED"
fi
# 7.10 Complete booking (David)
if [[ -n "$BOOKING_ID" ]]; then
r=$(api_call PATCH "/api/bookings/$BOOKING_ID/complete" "$DAVID_TOKEN")
assert_status "7.10 Complete booking" "200" "$(get_status "$r")"
assert_json "7.10 Status COMPLETED" "$(get_body "$r")" ".status" "COMPLETED"
fi
# 7.11 Create review (Alice reviews)
if [[ -n "$BOOKING_ID" ]]; then
r=$(api_call POST "/api/rental-reviews" "$ALICE_TOKEN" "{\"bookingId\":\"$BOOKING_ID\",\"rating\":4,\"comment\":\"QA test review - good rental\"}")
s=$(get_status "$r")
if [[ "$s" == "201" ]]; then
REVIEW_ID=$(get_body "$r" | jq -r '.id // empty')
assert_status "7.11 Create review" "201" "$s"
else
printf " ${RED}FAIL${NC} 7.11 Create review (HTTP %s)\n" "$s"; ((FAIL_COUNT++))
fi
fi
# 7.12 Landlord responds to review
if [[ -n "$REVIEW_ID" ]]; then
r=$(api_call PATCH "/api/rental-reviews/$REVIEW_ID/respond" "$DAVID_TOKEN" '{"response":"Thanks for the QA review!"}')
assert_status "7.12 Review response" "200" "$(get_status "$r")"
fi
# 7.13 Landlord bookings
r=$(api_call GET "/api/bookings" "$DAVID_TOKEN")
assert_status "7.13 Landlord bookings" "200" "$(get_status "$r")"
# 7.14 Tenant bookings
r=$(api_call GET "/api/bookings" "$ALICE_TOKEN")
assert_status "7.14 Tenant bookings" "200" "$(get_status "$r")"
# 7.15 Payouts
r=$(api_call GET "/api/payouts" "$DAVID_TOKEN")
assert_status "7.15 GET /payouts" "200" "$(get_status "$r")"
# 7.16 Rental favorites toggle
r=$(api_call POST "/api/rentals/rental-01/favorite" "$ALICE_TOKEN")
assert_status "7.16 Toggle rental fav" "200" "$(get_status "$r")"
# 7.17 Cannot book own rental
r=$(api_call POST "/api/bookings" "$DAVID_TOKEN" "{\"rentalListingId\":\"rental-01\",\"periodType\":\"DAILY\",\"startDate\":\"$START\",\"endDate\":\"$END\"}")
s=$(get_status "$r")
if [[ "$s" == "400" || "$s" == "403" ]]; then
printf " ${GREEN}PASS${NC} 7.17 Cannot book own rental (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 7.17 Expected 400/403, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# 7.18 Pause rental
r=$(api_call POST "/api/rentals/$RENTAL_ID/pause" "$DAVID_TOKEN")
assert_status "7.18 Pause rental" "200" "$(get_status "$r")"
# 7.19 Delete test rental (cleanup)
r=$(api_call DELETE "/api/rentals/$RENTAL_ID" "$DAVID_TOKEN")
assert_status "7.19 Delete rental" "200" "$(get_status "$r")"
RENTAL_ID=""
print_summary "Stage 7: Rental Workflow"

66
tests/stage-08-subscriptions.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 8: Subscriptions"
EVA_TOKEN=$(login_as "eva.johnson@example.com")
if [[ -z "$EVA_TOKEN" ]]; then echo "FATAL: Cannot login"; exit 1; fi
ORIGINAL_TIER=""
cleanup() {
# Restore to BASIC if changed
if [[ -n "$ORIGINAL_TIER" && "$ORIGINAL_TIER" != "null" ]]; then
api_call POST "/api/subscriptions/cancel" "$EVA_TOKEN" >/dev/null 2>&1
fi
}
trap cleanup EXIT
# 8.1 Get tiers (public)
r=$(api_call GET "/api/subscriptions/tiers")
assert_status "8.1 GET /tiers" "200" "$(get_status "$r")"
assert_json_array_min "8.1 Has tiers" "$(get_body "$r")" "." 3
# 8.2 Get current subscription
r=$(api_call GET "/api/subscriptions/current" "$EVA_TOKEN")
assert_status "8.2 GET /current" "200" "$(get_status "$r")"
b=$(get_body "$r")
assert_json_exists "8.2 Has tierConfig" "$b" ".tierConfig"
ORIGINAL_TIER=$(echo "$b" | jq -r '.subscription.tier // empty')
# 8.3 Upgrade to PRO
r=$(api_call POST "/api/subscriptions/create" "$EVA_TOKEN" '{"tier":"PRO"}')
assert_status "8.3 Upgrade to PRO" "200" "$(get_status "$r")"
# 8.4 Verify current is PRO
r=$(api_call GET "/api/subscriptions/current" "$EVA_TOKEN")
assert_json "8.4 Tier is PRO" "$(get_body "$r")" ".subscription.tier" "PRO"
# 8.5 Duplicate tier
r=$(api_call POST "/api/subscriptions/create" "$EVA_TOKEN" '{"tier":"PRO"}')
s=$(get_status "$r")
if [[ "$s" == "400" || "$s" == "409" ]]; then
printf " ${GREEN}PASS${NC} 8.5 Duplicate tier rejected (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 8.5 Expected 400/409, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# 8.6 Cancel subscription
r=$(api_call POST "/api/subscriptions/cancel" "$EVA_TOKEN")
assert_status "8.6 Cancel subscription" "200" "$(get_status "$r")"
ORIGINAL_TIER="" # Restored
# 8.7 Invalid tier
r=$(api_call POST "/api/subscriptions/create" "$EVA_TOKEN" '{"tier":"INVALID"}')
assert_status "8.7 Invalid tier" "400" "$(get_status "$r")"
# 8.8 Verify back to BASIC after cancel
r=$(api_call GET "/api/subscriptions/current" "$EVA_TOKEN")
b=$(get_body "$r")
tier=$(echo "$b" | jq -r '.subscription.tier // .tierConfig.tier // empty')
if [[ "$tier" == "BASIC" || -z "$tier" ]]; then
printf " ${GREEN}PASS${NC} 8.8 Back to BASIC (tier=%s)\n" "${tier:-none}"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 8.8 Expected BASIC, got %s\n" "$tier"; ((FAIL_COUNT++))
fi
print_summary "Stage 8: Subscriptions"

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 9: Chat & Notifications"
ALICE_TOKEN=$(login_as "alice.chen@example.com")
BOB_TOKEN=$(login_as "bob.martinez@example.com")
DAVID_TOKEN=$(login_as "david.kim@example.com")
if [[ -z "$ALICE_TOKEN" || -z "$BOB_TOKEN" ]]; then echo "FATAL: Cannot login"; exit 1; fi
CONV_ID=""
cleanup() {
[[ -n "$CONV_ID" ]] && api_call DELETE "/api/chat/conversations/$CONV_ID" "$ALICE_TOKEN" >/dev/null 2>&1
}
trap cleanup EXIT
# 9.1 Create conversation (Alice → Bob)
r=$(api_call POST "/api/chat/conversations" "$ALICE_TOKEN" '{"recipientId":"user-bob","message":"QA test message from Alice"}')
assert_status "9.1 Create conversation" "200" "$(get_status "$r")"
b=$(get_body "$r")
CONV_ID=$(echo "$b" | jq -r '.id // .conversation.id // empty')
# 9.2 Get conversations
r=$(api_call GET "/api/chat/conversations" "$ALICE_TOKEN")
assert_status "9.2 GET conversations" "200" "$(get_status "$r")"
# 9.3 Get messages
if [[ -n "$CONV_ID" ]]; then
r=$(api_call GET "/api/chat/conversations/$CONV_ID/messages" "$ALICE_TOKEN")
assert_status "9.3 GET messages" "200" "$(get_status "$r")"
fi
# 9.4 Bob sees conversation
r=$(api_call GET "/api/chat/conversations" "$BOB_TOKEN")
assert_status "9.4 Bob's conversations" "200" "$(get_status "$r")"
# 9.5 Bob replies (reuses same conversation)
r=$(api_call POST "/api/chat/conversations" "$BOB_TOKEN" '{"recipientId":"user-alice","message":"QA reply from Bob"}')
assert_status "9.5 Bob replies" "200" "$(get_status "$r")"
# 9.6 Cannot message self
r=$(api_call POST "/api/chat/conversations" "$ALICE_TOKEN" '{"recipientId":"user-alice","message":"self"}')
s=$(get_status "$r")
if [[ "$s" == "400" || "$s" == "403" ]]; then
printf " ${GREEN}PASS${NC} 9.6 Cannot message self (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 9.6 Expected 400/403, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# 9.7 Blocked user messaging (Carol blocked David)
if [[ -n "$DAVID_TOKEN" ]]; then
r=$(api_call POST "/api/chat/conversations" "$DAVID_TOKEN" '{"recipientId":"user-carol","message":"blocked test"}')
s=$(get_status "$r")
if [[ "$s" == "403" || "$s" == "400" ]]; then
printf " ${GREEN}PASS${NC} 9.7 Blocked user rejected (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 9.7 Expected 403/400, got %s\n" "$s"; ((FAIL_COUNT++))
fi
fi
# 9.8 Get notifications
r=$(api_call GET "/api/notifications" "$ALICE_TOKEN")
assert_status "9.8 GET /notifications" "200" "$(get_status "$r")"
# 9.9 Unread count
r=$(api_call GET "/api/notifications/unread-count" "$ALICE_TOKEN")
assert_status "9.9 Unread count" "200" "$(get_status "$r")"
# 9.10 Mark all read
r=$(api_call PATCH "/api/notifications/read-all" "$ALICE_TOKEN")
assert_status "9.10 Mark all read" "200" "$(get_status "$r")"
# 9.11 Delete test conversation
if [[ -n "$CONV_ID" ]]; then
r=$(api_call DELETE "/api/chat/conversations/$CONV_ID" "$ALICE_TOKEN")
assert_status "9.11 Delete conversation" "200" "$(get_status "$r")"
CONV_ID=""
fi
print_summary "Stage 9: Chat & Notifications"

138
tests/stage-10-admin.sh Executable file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 10: Admin Operations"
ALICE_TOKEN=$(login_as "alice.chen@example.com") # SUPER_ADMIN
BOB_TOKEN=$(login_as "bob.martinez@example.com") # ADMIN
CAROL_TOKEN=$(login_as "carol.nguyen@example.com") # MODERATOR
DAVID_TOKEN=$(login_as "david.kim@example.com") # USER
if [[ -z "$ALICE_TOKEN" || -z "$BOB_TOKEN" || -z "$CAROL_TOKEN" || -z "$DAVID_TOKEN" ]]; then
echo "FATAL: Cannot login all users"; exit 1
fi
cleanup() {
# Restore David's role to USER if changed
api_call PATCH "/api/admin/users/user-david/role" "$ALICE_TOKEN" '{"role":"USER"}' >/dev/null 2>&1
# Unban David if banned
api_call POST "/api/admin/users/user-david/unban" "$BOB_TOKEN" >/dev/null 2>&1
}
trap cleanup EXIT
# --- Stats ---
# 10.1 Moderator can see stats
r=$(api_call GET "/api/admin/stats" "$CAROL_TOKEN")
assert_status "10.1 Stats (moderator)" "200" "$(get_status "$r")"
assert_json_exists "10.1 totalUsers" "$(get_body "$r")" ".totalUsers"
# 10.2 Stats/listings (moderator)
r=$(api_call GET "/api/admin/stats/listings" "$CAROL_TOKEN")
assert_status "10.2 Stats/listings" "200" "$(get_status "$r")"
# 10.3 Stats/revenue (moderator → denied, need admin)
r=$(api_call GET "/api/admin/stats/revenue" "$CAROL_TOKEN")
assert_status "10.3 Revenue denied for mod" "403" "$(get_status "$r")"
# 10.4 Stats/revenue (admin)
r=$(api_call GET "/api/admin/stats/revenue" "$BOB_TOKEN")
assert_status "10.4 Revenue (admin)" "200" "$(get_status "$r")"
# 10.5 Stats/users (admin)
r=$(api_call GET "/api/admin/stats/users" "$BOB_TOKEN")
assert_status "10.5 Stats/users" "200" "$(get_status "$r")"
# --- Users ---
# 10.6 List users
r=$(api_call GET "/api/admin/users" "$CAROL_TOKEN")
assert_status "10.6 List users" "200" "$(get_status "$r")"
# 10.7 Search users
r=$(api_call GET "/api/admin/users?search=alice" "$CAROL_TOKEN")
assert_status "10.7 Search users" "200" "$(get_status "$r")"
# 10.8 User detail
r=$(api_call GET "/api/admin/users/user-david" "$CAROL_TOKEN")
assert_status "10.8 User detail" "200" "$(get_status "$r")"
# 10.9 Change role (SUPER_ADMIN only)
r=$(api_call PATCH "/api/admin/users/user-david/role" "$ALICE_TOKEN" '{"role":"MODERATOR"}')
assert_status "10.9 Change role" "200" "$(get_status "$r")"
# Restore
api_call PATCH "/api/admin/users/user-david/role" "$ALICE_TOKEN" '{"role":"USER"}' >/dev/null
# 10.10 Cannot change own role
r=$(api_call PATCH "/api/admin/users/user-alice/role" "$ALICE_TOKEN" '{"role":"USER"}')
s=$(get_status "$r")
if [[ "$s" == "400" || "$s" == "403" ]]; then
printf " ${GREEN}PASS${NC} 10.10 Cannot change own role (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 10.10 Expected 400/403, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# 10.11 Ban user (admin)
r=$(api_call POST "/api/admin/users/user-david/ban" "$BOB_TOKEN" '{"reason":"QA test ban"}')
assert_status "10.11 Ban David" "200" "$(get_status "$r")"
# 10.12 Unban
r=$(api_call POST "/api/admin/users/user-david/unban" "$BOB_TOKEN")
assert_status "10.12 Unban David" "200" "$(get_status "$r")"
# 10.13 Cannot ban SUPER_ADMIN
r=$(api_call POST "/api/admin/users/user-alice/ban" "$BOB_TOKEN" '{"reason":"test"}')
s=$(get_status "$r")
if [[ "$s" == "403" || "$s" == "400" ]]; then
printf " ${GREEN}PASS${NC} 10.13 Cannot ban super admin (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 10.13 Expected 403/400, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# --- Listings ---
# 10.14 Admin listings
r=$(api_call GET "/api/admin/listings" "$CAROL_TOKEN")
assert_status "10.14 Admin listings" "200" "$(get_status "$r")"
# 10.15 Moderation queue
r=$(api_call GET "/api/admin/moderation/queue" "$CAROL_TOKEN")
assert_status "10.15 Mod queue" "200" "$(get_status "$r")"
# --- Reports ---
# 10.16 Reports list
r=$(api_call GET "/api/admin/reports" "$CAROL_TOKEN")
assert_status "10.16 Reports list" "200" "$(get_status "$r")"
# --- Settings ---
# 10.17 Get settings (admin)
r=$(api_call GET "/api/admin/settings" "$BOB_TOKEN")
assert_status "10.17 GET settings" "200" "$(get_status "$r")"
# 10.18 Update settings (super_admin)
r=$(api_call PATCH "/api/admin/settings" "$ALICE_TOKEN" '{"listingFee":5}')
assert_status "10.18 Update settings" "200" "$(get_status "$r")"
# --- Rentals ---
# 10.19 Admin rentals
r=$(api_call GET "/api/admin/rentals" "$CAROL_TOKEN")
assert_status "10.19 Admin rentals" "200" "$(get_status "$r")"
# 10.20 Rental stats
r=$(api_call GET "/api/admin/rentals/stats" "$CAROL_TOKEN")
assert_status "10.20 Rental stats" "200" "$(get_status "$r")"
# --- Role guard ---
# 10.21 Regular user cannot access admin
r=$(api_call GET "/api/admin/stats" "$DAVID_TOKEN")
assert_status "10.21 User denied admin" "403" "$(get_status "$r")"
# 10.22 Admin bookings
r=$(api_call GET "/api/admin/bookings" "$CAROL_TOKEN")
assert_status "10.22 Admin bookings" "200" "$(get_status "$r")"
# 10.23 Admin payments (admin only)
r=$(api_call GET "/api/admin/payments" "$BOB_TOKEN")
assert_status "10.23 Admin payments" "200" "$(get_status "$r")"
# 10.24 Moderation logs (admin only)
r=$(api_call GET "/api/admin/moderation/logs" "$BOB_TOKEN")
assert_status "10.24 Mod logs" "200" "$(get_status "$r")"
print_summary "Stage 10: Admin Operations"

102
tests/stage-11-edge-cases.sh Executable file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 11: Edge Cases & Validation"
ALICE_TOKEN=$(login_as "alice.chen@example.com")
EVA_TOKEN=$(login_as "eva.johnson@example.com")
BOB_TOKEN=$(login_as "bob.martinez@example.com")
if [[ -z "$ALICE_TOKEN" || -z "$EVA_TOKEN" ]]; then echo "FATAL: Cannot login"; exit 1; fi
# Register a temp user for ban test
TEMP_EMAIL="qa-ban-$(date +%s)@example.com"
r=$(api_call POST "/api/auth/register" "" "{\"fullName\":\"Ban Test\",\"email\":\"$TEMP_EMAIL\",\"password\":\"TestPass123\"}")
TEMP_TOKEN=$(get_body "$r" | jq -r '.accessToken // empty')
TEMP_USER_ID=$(get_body "$r" | jq -r '.user.id // empty')
cleanup() {
# Unban temp user
[[ -n "$TEMP_USER_ID" ]] && api_call POST "/api/admin/users/$TEMP_USER_ID/unban" "$BOB_TOKEN" >/dev/null 2>&1
}
trap cleanup EXIT
# 11.1 Expired/garbage token
r=$(api_call GET "/api/auth/me" "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ0ZXN0IiwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAwMDAwMDF9.invalid")
assert_status "11.1 Expired token" "401" "$(get_status "$r")"
# 11.2 Malformed JSON body
r=$(curl -s -w '\n%{http_code}' -X POST "${BASE_URL}/api/auth/login" -H "Content-Type: application/json" -d "not valid json" 2>/dev/null)
s=$(get_status "$r")
if [[ "$s" == "400" || "$s" == "500" ]]; then
printf " ${GREEN}PASS${NC} 11.2 Malformed JSON rejected (HTTP %s)\n" "$s"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 11.2 Expected 400/500, got %s\n" "$s"; ((FAIL_COUNT++))
fi
# 11.3 Missing required fields
r=$(api_call POST "/api/listings" "$ALICE_TOKEN" '{}')
assert_status "11.3 Empty listing body" "400" "$(get_status "$r")"
# 11.4 Invalid category
r=$(api_call POST "/api/listings" "$ALICE_TOKEN" '{"title":"Bad","description":"At least 10 chars here","price":10,"category":"INVALID","condition":"NEW","location":"X"}')
assert_status "11.4 Invalid category" "400" "$(get_status "$r")"
# 11.5 Invalid condition
r=$(api_call POST "/api/listings" "$ALICE_TOKEN" '{"title":"Bad","description":"At least 10 chars here","price":10,"category":"OTHER","condition":"BROKEN","location":"X"}')
assert_status "11.5 Invalid condition" "400" "$(get_status "$r")"
# 11.6 Negative price
r=$(api_call POST "/api/listings" "$ALICE_TOKEN" '{"title":"Bad","description":"At least 10 chars here","price":-10,"category":"OTHER","condition":"NEW","location":"X"}')
assert_status "11.6 Negative price" "400" "$(get_status "$r")"
# 11.7 Offer on non-existent listing
r=$(api_call POST "/api/offers" "$EVA_TOKEN" '{"amount":100,"listingId":"fake-listing-xyz"}')
assert_status "11.7 Offer on fake listing" "404" "$(get_status "$r")"
# 11.8 Edit another user's listing
r=$(api_call PUT "/api/listings/listing-01" "$EVA_TOKEN" '{"title":"Hacked"}')
assert_status "11.8 Edit other listing" "403" "$(get_status "$r")"
# 11.9 Delete another user's listing
r=$(api_call DELETE "/api/listings/listing-01" "$EVA_TOKEN")
assert_status "11.9 Delete other listing" "403" "$(get_status "$r")"
# 11.10 Unauthenticated protected endpoints
for path in "/api/listings" "/api/offers" "/api/bookings" "/api/reports"; do
r=$(api_call POST "$path" "" '{}')
s=$(get_status "$r")
if [[ "$s" == "401" ]]; then
printf " ${GREEN}PASS${NC} 11.10 POST %s → 401\n" "$path"; ((PASS_COUNT++))
else
printf " ${RED}FAIL${NC} 11.10 POST %s expected 401, got %s\n" "$path" "$s"; ((FAIL_COUNT++))
fi
done
# 11.11 Ban user then test login
if [[ -n "$TEMP_USER_ID" ]]; then
api_call POST "/api/admin/users/$TEMP_USER_ID/ban" "$BOB_TOKEN" '{"reason":"QA ban test"}' >/dev/null
r=$(api_call POST "/api/auth/login" "" "{\"email\":\"$TEMP_EMAIL\",\"password\":\"TestPass123\"}")
assert_status "11.11 Banned user login" "403" "$(get_status "$r")"
# Unban
api_call POST "/api/admin/users/$TEMP_USER_ID/unban" "$BOB_TOKEN" >/dev/null
fi
# 11.12 Report creation
r=$(api_call POST "/api/reports" "$EVA_TOKEN" '{"targetType":"LISTING","targetId":"listing-01","reason":"SPAM","description":"QA test report"}')
assert_status "11.12 Create report" "201" "$(get_status "$r")"
# 11.13 Report invalid target
r=$(api_call POST "/api/reports" "$EVA_TOKEN" '{"targetType":"LISTING","targetId":"fake-id","reason":"SPAM"}')
assert_status "11.13 Report fake target" "404" "$(get_status "$r")"
# 11.14 Booking with past dates
START="2020-01-01T00:00:00.000Z"
END="2020-01-03T00:00:00.000Z"
r=$(api_call POST "/api/bookings" "$ALICE_TOKEN" "{\"rentalListingId\":\"rental-01\",\"periodType\":\"DAILY\",\"startDate\":\"$START\",\"endDate\":\"$END\"}")
assert_status "11.14 Past date booking" "400" "$(get_status "$r")"
# 11.15 Booking with end before start
r=$(api_call POST "/api/bookings" "$ALICE_TOKEN" '{"rentalListingId":"rental-01","periodType":"DAILY","startDate":"2027-06-05T00:00:00.000Z","endDate":"2027-06-01T00:00:00.000Z"}')
assert_status "11.15 End before start" "400" "$(get_status "$r")"
print_summary "Stage 11: Edge Cases"

44
tests/stage-12-client-pages.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
stage_header "Stage 12: Client Pages"
# Check if client is running
r=$(curl -s -o /dev/null -w "%{http_code}" "$CLIENT_URL" 2>/dev/null)
if [[ "$r" != "200" ]]; then
echo " Client not running at $CLIENT_URL — skipping all tests"
exit 0
fi
check_page() {
local name="$1" path="$2" expected="${3:-200}"
local status
status=$(curl -s -o /dev/null -w "%{http_code}" "${CLIENT_URL}${path}" 2>/dev/null)
assert_status "$name" "$expected" "$status"
}
# Public pages
check_page "12.1 Homepage" "/"
check_page "12.2 Login" "/login"
check_page "12.3 Signup" "/signup"
check_page "12.4 About" "/about"
check_page "12.5 Privacy" "/privacy"
check_page "12.6 Terms" "/terms"
check_page "12.7 Help" "/help"
check_page "12.8 Contact" "/contact"
check_page "12.9 Rentals" "/rentals"
check_page "12.10 Listing detail" "/listings/listing-01"
check_page "12.11 Rental detail" "/rentals/rental-01"
# SPA routes (all return 200 — HTML shell, auth handled client-side)
check_page "12.12 Dashboard" "/dashboard"
check_page "12.13 Admin" "/admin"
check_page "12.14 Landlord" "/landlord"
check_page "12.15 Sell" "/sell"
check_page "12.16 Settings" "/dashboard/settings"
# API proxy through client
r=$(curl -s -o /dev/null -w "%{http_code}" "${CLIENT_URL}/api/listings" 2>/dev/null)
assert_status "12.17 API proxy" "200" "$r"
print_summary "Stage 12: Client Pages"