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:
@@ -4,10 +4,22 @@
|
||||
"type": "http",
|
||||
"url": "https://mcp.figma.com/mcp"
|
||||
},
|
||||
"chrome-devtools": {
|
||||
"devtools": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "chrome-devtools-mcp@latest", "--no-usage-statistics", "--isolated"]
|
||||
"command": "/opt/homebrew/bin/chrome-devtools-mcp",
|
||||
"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
152
CLAUDE.md
@@ -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
|
||||
|
||||
Container name: `marketplace-postgres`
|
||||
Image: `postgres:17-alpine`
|
||||
Volume: `marketplace-pgdata` (persistent local data)
|
||||
Container: `marketplace-postgres` | Image: `postgres:17-alpine` | Volume: `marketplace-pgdata`
|
||||
|
||||
### Start database
|
||||
Connection string: `postgresql://marketplace:marketplace_dev@localhost:5432/marketplace`
|
||||
|
||||
If container was deleted, recreate:
|
||||
```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)
|
||||
```bash
|
||||
docker stop marketplace-postgres
|
||||
```
|
||||
## Key Server Patterns
|
||||
|
||||
### Check status
|
||||
```bash
|
||||
docker ps -f name=marketplace-postgres
|
||||
```
|
||||
- **Routes**: `server/src/routes/` — RESTful endpoints, admin routes nested under `routes/admin/`
|
||||
- **Middleware**: `authenticate` (required auth), `optionalAuth` (anonymous OK), `requireRole` (role check), `checkBanned`, `validate` (Zod)
|
||||
- **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
|
||||
```
|
||||
postgresql://marketplace:marketplace_dev@localhost:5432/marketplace
|
||||
```
|
||||
## Key Client Patterns
|
||||
|
||||
### If container was deleted, recreate:
|
||||
```bash
|
||||
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
|
||||
```
|
||||
- **Router**: `client/src/router.tsx` — React Router v7 with `createBrowserRouter`
|
||||
- **Auth guards**: `<RequireAuth>` wraps protected routes, `<RequireRole>` checks roles
|
||||
- **API client**: `client/src/api/client.ts` — fetch wrapper that handles auth headers and token refresh
|
||||
- **UI components**: `client/src/components/ui/` — Button, Input, Modal, Card, Badge, DataTable, etc.
|
||||
- **Pages organized by domain**: root pages, `pages/admin/`, `pages/landlord/`
|
||||
|
||||
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`
|
||||
2. Server: `npm run dev:server` (port 3000)
|
||||
3. Client: `npm run dev:client` (port 5173)
|
||||
4. Or both: `npm run dev`
|
||||
- **Config location**: `~/.claude.json` → `mcpServers.chrome-devtools` (NOT `~/.claude/settings.json`)
|
||||
- **Binary**: `/opt/homebrew/bin/chrome-devtools-mcp` (installed globally via npm)
|
||||
- **Chrome profile**: `/tmp/chrome-mcp-profile` (isolated, no Google account)
|
||||
- **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
|
||||
cd server && npx tsx prisma/seed.ts
|
||||
```
|
||||
|
||||
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
|
||||
- `DATABASE_URL` — PostgreSQL connection string
|
||||
- `JWT_SECRET` / `JWT_REFRESH_SECRET` — Token signing
|
||||
- `GOOGLE_MAPS_API_KEY` — Location autocomplete
|
||||
- `STRIPE_SECRET_KEY` / `STRIPE_PUBLISHABLE_KEY` — Payments (optional for dev)
|
||||
- `CLIENT_URL` — CORS origin (default: http://localhost:5173)
|
||||
|
||||
@@ -49,7 +49,7 @@ app.use('/api/rental-payments/webhook', express.raw({ type: 'application/json' }
|
||||
app.use(express.json());
|
||||
|
||||
// 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/register', authLimiter);
|
||||
|
||||
|
||||
133
tests/lib/common.sh
Normal file
133
tests/lib/common.sh
Normal 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
66
tests/run-all.sh
Executable 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
|
||||
24
tests/stage-01-infrastructure.sh
Executable file
24
tests/stage-01-infrastructure.sh
Executable 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
76
tests/stage-02-auth.sh
Executable 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
102
tests/stage-03-public.sh
Executable 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
98
tests/stage-04-user-crud.sh
Executable 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
78
tests/stage-05-listings.sh
Executable 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
96
tests/stage-06-offers.sh
Executable 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
156
tests/stage-07-rentals.sh
Executable 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
66
tests/stage-08-subscriptions.sh
Executable 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"
|
||||
80
tests/stage-09-chat-notifications.sh
Executable file
80
tests/stage-09-chat-notifications.sh
Executable 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
138
tests/stage-10-admin.sh
Executable 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
102
tests/stage-11-edge-cases.sh
Executable 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
44
tests/stage-12-client-pages.sh
Executable 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"
|
||||
Reference in New Issue
Block a user