Compare commits

..

1 Commits

Author SHA1 Message Date
Zhi
8cacfb360a update HarborForge.Frontend.Test to latest 2026-03-13 11:25:15 +00:00
10 changed files with 58 additions and 503 deletions

View File

@@ -1,14 +0,0 @@
# HarborForge Test Environment Variables
# Default port values
# Wizard service
WIZARD_PORT=8080
# MySQL service
MYSQL_PORT=3306
# Backend service
BACKEND_PORT=8000
# Frontend service
FRONTEND_PORT=3000

4
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "HarborForge.Backend.Test"]
path = HarborForge.Backend.Test
url = https://git.hangman-lab.top/zhi/HarborForge.Backend.Test.git
url = https://zhi:rT5Wjw24mV4all38fIoNQfl2@git.hangman-lab.top/zhi/HarborForge.Backend.Test.git
[submodule "HarborForge.Frontend.Test"]
path = HarborForge.Frontend.Test
url = https://git.hangman-lab.top/zhi/HarborForge.Frontend.Test.git
url = https://zhi:rT5Wjw24mV4all38fIoNQfl2@git.hangman-lab.top/zhi/HarborForge.Frontend.Test.git

135
README.md
View File

@@ -1,136 +1 @@
# HarborForge.Test
Integration / end-to-end test harness for the HarborForge platform. It
spins up the full stack (MySQL, AbstractWizard, HarborForge.Backend,
HarborForge.Frontend) in Docker and runs the backend and frontend test
suites against it.
Part of the [HarborForge](../README.md) platform.
## Layout
```
HarborForge.Test/
├── run-test.sh # Quick frontend E2E run (no rebuild)
├── run-test-frontend.sh # Full frontend E2E run (rebuild + optional port expose)
├── cleanup.sh # Tear down containers / wizard config volume
├── docker-compose-frontend.yml # Internal-only test stack
├── docker-compose-frontend-expose.yml# Same stack with ports bound to 127.0.0.1
├── .env.TEST # Default service ports
├── HarborForge.Backend.Test/ # git submodule — backend pytest suite
└── HarborForge.Frontend.Test/ # git submodule — frontend Playwright suite
```
The two test suites are git submodules (see `.gitmodules`). After cloning,
initialize them:
```bash
git submodule update --init --recursive
```
## What It Tests
### Backend — `HarborForge.Backend.Test` (pytest)
A standalone pytest suite that imports the backend code from
`../HarborForge.Backend/` via `tests/conftest.py` and runs against an
in-memory SQLite database for fast, isolated unit/integration tests.
Configured by `pyproject.toml` (`testpaths = ["tests"]`, verbose,
short tracebacks).
Coverage spans auth/JWT, users, projects, milestones, tasks, comments,
roles/permissions, the milestone and task state machines
(`test_milestone_actions.py`, `test_task_transitions.py`), proposals
(`test_propose.py`), the monitor endpoint, and miscellaneous endpoints
(notifications, activity log, API keys, dashboard).
Run it standalone:
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r HarborForge.Backend.Test/requirements.txt
cd HarborForge.Backend.Test && pytest # or: pytest -v, pytest tests/test_auth.py
```
See `HarborForge.Backend.Test/README.md` for the full test breakdown.
### Frontend — `HarborForge.Frontend.Test` (Playwright)
A Playwright (`@playwright/test`) end-to-end suite that drives the running
frontend through a browser. Specs in `tests/` cover the setup wizard,
projects editor, milestones, tasks, role editor and proposals
(`wizard.spec.ts`, `project-editor.spec.ts`, `milestone.spec.ts`,
`task.spec.ts`, `role-editor.spec.ts`, `propose.spec.ts`,
`proposal-essential.spec.ts`). Its Docker image runs a helper proxy
(`server/proxy.mjs`) alongside `npx playwright test`.
> Note: the frontend's own Vitest unit tests live in the
> `HarborForge.Frontend` repo; this harness exercises the frontend through
> Playwright against the full Docker stack.
## Docker Test Stack
`docker-compose-frontend.yml` defines five services on a private
`test-network` bridge — `mysql` (MySQL 8, tmpfs storage), `wizard`
(AbstractWizard), `backend` (HarborForge.Backend), `frontend`
(HarborForge.Frontend), and `test` (the Playwright runner image built from
`HarborForge.Frontend.Test/`). Service ports default from `.env.TEST`
(`WIZARD_PORT=8080`, `MYSQL_PORT=3306`, `BACKEND_PORT=8000`,
`FRONTEND_PORT=3000`).
- `docker-compose-frontend.yml` — services are reachable only on the
internal network (wizard alone is bound to `127.0.0.1`).
- `docker-compose-frontend-expose.yml` — same stack, but mysql, wizard,
backend and frontend ports are also bound to `127.0.0.1` for debugging.
## Running the Tests
### Quick run (no rebuild)
```bash
./run-test.sh
```
Brings the stack up from `docker-compose-frontend.yml`, waits (up to ~60s)
for the frontend to answer HTTP 200, runs the `test` service once, then
tears everything down with `down -v`.
### Full run (rebuild, optional port exposure)
```bash
./run-test-frontend.sh # rebuild all images, run, auto-cleanup
./run-test-frontend.sh --expose-port on # use the *-expose.yml file, keep services up
./run-test-frontend.sh --expose-port off # default: cleanup after the run
```
It loads `.env.TEST`, rebuilds the frontend (with build arg
`VITE_API_BASE=http://backend:8000`), backend and test-runner images with
`--no-cache`, starts the stack, waits for the frontend, then runs the
`test` service with `WORKERS=1`. With `--expose-port on` it uses
`docker-compose-frontend-expose.yml` and leaves the stack running for
inspection.
### Cleanup
```bash
./cleanup.sh
```
Stops and removes containers/networks (keeps images) and drops the
`harborforgetest_wizard_config` volume. The run scripts also call
`docker compose ... down -v` themselves on completion (except when port
exposure is on).
## CI Notes
- Both run scripts use `set -e` and propagate the test container's exit
code, so they are CI-friendly: a non-zero `TEST_EXIT_CODE` fails the job.
- `run-test-frontend.sh` suppresses build/startup output unless a step
fails, in which case it prints the tail of that step's log.
- Builds use `--no-cache` and remove prior `harborforge-test-*:dev` images
to guarantee a clean stack each run; MySQL uses `tmpfs` so no state
persists between runs.
- Ensure submodules are initialized
(`git submodule update --init --recursive`) before invoking the scripts
in CI.

View File

@@ -1,17 +0,0 @@
#!/bin/bash
# Cleanup script for HarborForge Test
# Removes containers and networks, but keeps images
set -e
COMPOSE_FILE="docker-compose-frontend.yml"
echo "🧹 Cleaning up HarborForge Test containers..."
# Stop and remove containers, networks (keep images)
docker compose -f "$COMPOSE_FILE" down
# Also remove the wizard config volume
docker volume rm harborforgetest_wizard_config 2>/dev/null || true
echo "✅ Cleanup complete!"

View File

@@ -1,116 +0,0 @@
services:
mysql:
image: mysql:8.0
container_name: harborforge-test-mysql
restart: "no"
tmpfs:
- /var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-harborforge_root}
MYSQL_DATABASE: ${MYSQL_DATABASE:-harborforge}
MYSQL_USER: ${MYSQL_USER:-harborforge}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-harborforge_pass}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "127.0.0.1:${MYSQL_PORT:-3306}:3306"
networks:
- test-network
wizard:
build:
context: ../AbstractWizard
dockerfile: Dockerfile
image: harborforge-test-wizard:dev
container_name: harborforge-test-wizard
user: 0:0
restart: "no"
volumes:
- wizard_config:/config
environment:
CONFIG_DIR: /config
LISTEN_ADDR: "0.0.0.0:${WIZARD_PORT:-8080}"
MAX_BACKUPS: "5"
CORS_ORIGINS: http://frontend:${FRONTEND_PORT:-3000},http://127.0.0.1:${FRONTEND_PORT:-3000},http://localhost:${FRONTEND_PORT:-3000}
ports:
- "127.0.0.1:${WIZARD_PORT:-8080}:${WIZARD_PORT:-8080}"
networks:
- test-network
backend:
build:
context: ../HarborForge.Backend
dockerfile: Dockerfile
image: harborforge-test-backend:dev
container_name: harborforge-test-backend
restart: "no"
volumes:
- wizard_config:/config:ro
environment:
CONFIG_DIR: /config
CONFIG_FILE: harborforge.json
SECRET_KEY: ${SECRET_KEY:-change_me_in_production}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
DATABASE_URL: mysql+pymysql://harborforge:harborforge_pass@mysql:${MYSQL_PORT:-3306}/harborforge
ports:
- "127.0.0.1:${BACKEND_PORT:-8000}:${BACKEND_PORT:-8000}"
depends_on:
mysql:
condition: service_healthy
networks:
- test-network
frontend:
build:
context: ../HarborForge.Frontend
dockerfile: Dockerfile
args:
VITE_WIZARD_PORT: ${WIZARD_PORT:-8080}
image: harborforge-test-frontend:dev
container_name: harborforge-test-frontend
restart: "no"
environment:
VITE_API_BASE_URL: http://backend:${BACKEND_PORT:-8000}
VITE_WIZARD_PORT: ${WIZARD_PORT:-8080}
FRONTEND_DEV_MODE: ${FRONTEND_DEV_MODE:-1}
NODE_ENV: development
ports:
- "127.0.0.1:${FRONTEND_PORT:-3000}:${FRONTEND_PORT:-3000}"
depends_on:
- wizard
- backend
networks:
- test-network
test:
build:
context: ./HarborForge.Frontend.Test
dockerfile: Dockerfile
image: harborforge-test-runner:dev
container_name: harborforge-test-runner
restart: "no"
environment:
# Use internal service name for test to reach frontend
BASE_URL: http://127.0.0.1:${FRONTEND_PORT:-3000}
FRONTEND_URL: http://127.0.0.1:${FRONTEND_PORT:-3000}
WEB_SERVER_URL: http://127.0.0.1:${FRONTEND_PORT:-3000}
WIZARD_URL: http://127.0.0.1:${WIZARD_PORT:-8080}/wizard
WIZARD_API_URL: http://127.0.0.1:${WIZARD_PORT:-8080}
WIZARD_HOST: wizard
WIZARD_PORT: ${WIZARD_PORT:-8080}
BACKEND_URL: http://127.0.0.1:${BACKEND_PORT:-8000}
BACKEND_HOST: backend
FRONTEND_HOST: frontend
CHROME_DEBUGGING_PORT: 9222
networks:
- test-network
volumes:
wizard_config:
networks:
test-network:
driver: bridge

View File

@@ -2,9 +2,7 @@ services:
mysql:
image: mysql:8.0
container_name: harborforge-test-mysql
restart: "no"
tmpfs:
- /var/lib/mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-harborforge_root}
MYSQL_DATABASE: ${MYSQL_DATABASE:-harborforge}
@@ -15,6 +13,11 @@ services:
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
networks:
- test-network
@@ -25,17 +28,21 @@ services:
image: harborforge-test-wizard:dev
container_name: harborforge-test-wizard
user: 0:0
restart: "no"
restart: unless-stopped
volumes:
- wizard_config:/config
environment:
CONFIG_DIR: /config
LISTEN_ADDR: "0.0.0.0:${WIZARD_PORT:-8080}"
LISTEN_ADDR: "0.0.0.0:8080"
MAX_BACKUPS: "5"
# Internal network only
CORS_ORIGINS: http://frontend:${FRONTEND_PORT:-3000}
CORS_ORIGINS: http://frontend:3000
ports:
- "127.0.0.1:${WIZARD_PORT:-8080}:${WIZARD_PORT:-8080}"
- "${WIZARD_PORT:-18080}:8080"
deploy:
resources:
limits:
cpus: '0.1'
memory: 64M
networks:
- test-network
@@ -45,18 +52,29 @@ services:
dockerfile: Dockerfile
image: harborforge-test-backend:dev
container_name: harborforge-test-backend
restart: "no"
volumes:
- wizard_config:/config:ro
restart: unless-stopped
environment:
CONFIG_DIR: /config
CONFIG_FILE: harborforge.json
SECRET_KEY: ${SECRET_KEY:-change_me_in_production}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
DATABASE_URL: mysql+pymysql://harborforge:harborforge_pass@mysql:${MYSQL_PORT:-3306}/harborforge
DATABASE_URL: mysql+pymysql://harborforge:harborforge_pass@mysql:3306/harborforge
volumes:
- wizard_config:/config:ro
depends_on:
mysql:
condition: service_healthy
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
- test-network
@@ -65,16 +83,25 @@ services:
context: ../HarborForge.Frontend
dockerfile: Dockerfile
args:
VITE_WIZARD_PORT: ${WIZARD_PORT:-8080}
VITE_WIZARD_PORT: 8080
image: harborforge-test-frontend:dev
container_name: harborforge-test-frontend
restart: "no"
restart: unless-stopped
environment:
# Use internal service name
VITE_API_BASE_URL: http://backend:${BACKEND_PORT:-8000}
VITE_WIZARD_PORT: ${WIZARD_PORT:-8080}
FRONTEND_DEV_MODE: ${FRONTEND_DEV_MODE:-1}
NODE_ENV: development
VITE_API_BASE_URL: http://backend:8000
depends_on:
- wizard
- backend
deploy:
resources:
limits:
cpus: '0.25'
memory: 128M
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
networks:
- test-network
@@ -86,18 +113,20 @@ services:
container_name: harborforge-test-runner
restart: "no"
environment:
BASE_URL: http://frontend:${FRONTEND_PORT:-3000}
WEB_SERVER_URL: http://frontend:${FRONTEND_PORT:-3000}
WIZARD_URL: http://wizard:${WIZARD_PORT:-8080}/wizard
WIZARD_API_URL: http://wizard:${WIZARD_PORT:-8080}
WIZARD_HOST: wizard
WIZARD_PORT: ${WIZARD_PORT:-8080}
BASE_URL: http://frontend:3000
WEB_SERVER_URL: http://frontend:3000
CHROME_DEBUGGING_PORT: 9222
depends_on:
frontend:
condition: service_healthy
backend:
condition: service_healthy
networks:
- test-network
volumes:
wizard_config:
driver: local
networks:
test-network:

View File

@@ -1,143 +0,0 @@
#!/bin/bash
# Run frontend test with optional port exposure
# Usage: ./run-test-frontend.sh [--expose-port {on|off}]
# Default:
# --expose-port off: Auto cleanup after test
set -e
EXPOSE_PORT="off"
COMPOSE_FILE="docker-compose-frontend.yml"
# Load environment variables from .env.TEST if exists
if [ -f ".env.TEST" ]; then
echo "📋 Loading .env.TEST..."
set -a
source .env.TEST
set +a
fi
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--expose-port)
EXPOSE_PORT="$2"
shift 2
;;
--expose-port=*)
EXPOSE_PORT="${1#*=}"
shift
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [--expose-port {on|off}]"
exit 1
;;
esac
done
# Validate expose-port value
if [[ "$EXPOSE_PORT" != "on" && "$EXPOSE_PORT" != "off" ]]; then
echo "Error: --expose-port must be 'on' or 'off'"
exit 1
fi
# Select compose file based on expose-port
if [[ "$EXPOSE_PORT" == "on" ]]; then
COMPOSE_FILE="docker-compose-frontend-expose.yml"
echo "🔌 Port exposure: ON (services will keep running)"
else
echo "🔌 Port exposure: OFF (auto cleanup after test)"
fi
echo "📦 Using compose file: $COMPOSE_FILE"
run_quiet() {
local label="$1"
shift
local log_file
log_file=$(mktemp)
if "$@" >"$log_file" 2>&1; then
rm -f "$log_file"
return 0
fi
echo "${label} failed"
echo "--- ${label} log ---"
tail -n 200 "$log_file"
rm -f "$log_file"
return 1
}
# Clean any previous containers first
echo "🧹 Cleaning up previous containers..."
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
# Build frontend with correct API base URL (force no cache, remove image first)
echo "🔨 Building frontend..."
docker rmi harborforge-test-frontend:dev >/dev/null 2>&1 || true
run_quiet "frontend build" docker compose -f "$COMPOSE_FILE" build --no-cache --build-arg VITE_API_BASE=http://backend:8000 frontend
# Build backend (force no cache, remove image first)
echo "🔨 Building backend..."
docker rmi harborforge-test-backend:dev >/dev/null 2>&1 || true
run_quiet "backend build" docker compose -f "$COMPOSE_FILE" build --no-cache backend
# Build test runner (force no cache, remove image first)
echo "🔨 Building test runner..."
docker rmi harborforge-test-runner:dev >/dev/null 2>&1 || true
run_quiet "test runner build" docker compose -f "$COMPOSE_FILE" build --no-cache test
# Start services
echo "📦 Starting services..."
run_quiet "service startup" docker compose -f "$COMPOSE_FILE" up -d
# Wait for frontend to be ready
echo "⏳ Waiting for services..."
MAX_RETRIES=30
RETRY_COUNT=0
until docker run --rm --network harborforgetest_test-network curlimages/curl -s -o /dev/null -w "%{http_code}" http://frontend:3000/ 2>/dev/null | grep -q "200" || [ $RETRY_COUNT -eq $MAX_RETRIES ]; do
echo " Waiting for frontend... ($RETRY_COUNT/$MAX_RETRIES)"
sleep 2
RETRY_COUNT=$((RETRY_COUNT+1))
done
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
echo "❌ Frontend failed to start"
docker compose -f "$COMPOSE_FILE" logs
docker compose -f "$COMPOSE_FILE" down -v
exit 1
fi
echo "✅ Services ready!"
# Run test using the image default CMD so proxy startup stays inside Frontend.Test Dockerfile
echo "🧪 Running test..."
docker compose -f "$COMPOSE_FILE" run --rm -e WORKERS=1 test
TEST_EXIT_CODE=$?
# Cleanup decision based on expose-port
if [[ "$EXPOSE_PORT" == "on" ]]; then
echo ""
echo "🔌 Port exposure is ON - keeping services running!"
echo " Use './run-test-frontend.sh --expose-port on' to cleanup"
echo " Or manually: docker compose -f $COMPOSE_FILE down -v"
echo ""
if [ $TEST_EXIT_CODE -eq 0 ]; then
echo "✅ Test passed!"
else
echo "❌ Test failed with exit code: $TEST_EXIT_CODE"
fi
else
echo ""
echo "🧹 Cleaning up containers and volumes..."
docker compose -f "$COMPOSE_FILE" down -v
if [ $TEST_EXIT_CODE -eq 0 ]; then
echo "✅ Test passed!"
else
echo "❌ Test failed with exit code: $TEST_EXIT_CODE"
fi
fi
exit $TEST_EXIT_CODE

View File

@@ -1,49 +0,0 @@
#!/bin/bash
# Run test and cleanup afterwards
set -e
COMPOSE_FILE="docker-compose-frontend.yml"
echo "🚀 Running HarborForge Test..."
# Clean any previous containers first
docker compose -f "$COMPOSE_FILE" down 2>/dev/null || true
# Start services
echo "📦 Starting services..."
docker compose -f "$COMPOSE_FILE" up -d
# Wait for frontend to be ready (run curl inside docker network)
echo "⏳ Waiting for services..."
MAX_RETRIES=30
RETRY_COUNT=0
until docker run --rm --network harborforgetest_test-network curlimages/curl -s -o /dev/null -w "%{http_code}" http://frontend:3000/ 2>/dev/null | grep -q "200" || [ $RETRY_COUNT -eq $MAX_RETRIES ]; do
echo " Waiting for frontend... ($RETRY_COUNT/$MAX_RETRIES)"
sleep 2
RETRY_COUNT=$((RETRY_COUNT+1))
done
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
echo "❌ Frontend failed to start"
docker compose -f "$COMPOSE_FILE" logs
docker compose -f "$COMPOSE_FILE" down -v
exit 1
fi
echo "✅ Services ready!"
# Run test
docker compose -f "$COMPOSE_FILE" run --rm test
TEST_EXIT_CODE=$?
echo ""
echo "🧹 Cleaning up containers and volumes..."
docker compose -f "$COMPOSE_FILE" down -v
if [ $TEST_EXIT_CODE -eq 0 ]; then
echo "✅ Test passed!"
else
echo "❌ Test failed with exit code: $TEST_EXIT_CODE"
fi
exit $TEST_EXIT_CODE