diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 985ad91..f82eba2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ on: jobs: test: runs-on: ubuntu-latest - timeout-minutes: 90 + timeout-minutes: 120 steps: - uses: actions/checkout@v4 @@ -50,7 +50,8 @@ jobs: - name: Wait for services run: ./scripts/wait-ready.sh env: - WAIT_TIMEOUT: 180 + WAIT_TIMEOUT: 300 + WAIT_SERVICES: api,b1admin - name: Initialize database run: docker compose exec -T api npm run initdb @@ -80,33 +81,30 @@ jobs: - name: Install B1Admin dependencies working-directory: services/B1Admin - run: npm install - - - name: Install B1App dependencies - working-directory: services/B1App - run: npm install + run: npm install --legacy-peer-deps - name: Install Playwright browsers (B1Admin) working-directory: services/B1Admin run: npx playwright install --with-deps chromium - - name: Install Playwright browsers (B1App) - working-directory: services/B1App - run: npx playwright install --with-deps chromium - - name: Run B1Admin E2E tests + id: b1admin-tests working-directory: services/B1Admin - run: npx playwright test + run: | + npx playwright test --reporter=list 2>&1 | tee /tmp/playwright-output.log + exit ${PIPESTATUS[0]} env: BASE_URL: http://localhost:3101 CI: true - - name: Run B1App E2E tests - working-directory: services/B1App - run: npx playwright test - env: - BASE_URL: http://localhost:3301 - CI: true + - name: Show test summary + if: always() + run: | + echo "=== Test Summary ===" + tail -5 /tmp/playwright-output.log 2>/dev/null || echo "No output" + echo "" + echo "=== Failed Tests ===" + grep "✘" /tmp/playwright-output.log 2>/dev/null | grep -v "retry" | head -60 || echo "None" - name: Upload Playwright report (B1Admin) if: always() @@ -116,14 +114,6 @@ jobs: path: services/B1Admin/playwright-report/ retention-days: 14 - - name: Upload Playwright report (B1App) - if: always() - uses: actions/upload-artifact@v4 - with: - name: playwright-report-b1app - path: services/B1App/playwright-report/ - retention-days: 14 - - name: Dump logs on failure if: failure() run: docker compose logs --tail=100 diff --git a/docker-compose.yml b/docker-compose.yml index 07d29df..b83a7ea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,6 +67,8 @@ services: - "8087:8087" volumes: - ./services/Api/src:/app/src:cached + - ./services/Api/tools:/app/tools:cached + - ./services/Api/drizzle:/app/drizzle:cached - ./services/Api/jest.integration.config.cjs:/app/jest.integration.config.cjs:cached - ./services/Api/tsconfig.test.json:/app/tsconfig.test.json:cached - api_node_modules:/app/node_modules diff --git a/docker/api/Dockerfile.prod b/docker/api/Dockerfile.prod index 40e1e7b..c075783 100644 --- a/docker/api/Dockerfile.prod +++ b/docker/api/Dockerfile.prod @@ -18,6 +18,7 @@ RUN npm ci --omit=dev COPY --from=builder /app/dist ./dist COPY services/Api/config/ ./config/ COPY services/Api/tools/ ./tools/ +COPY services/Api/drizzle/ ./drizzle/ COPY docker/api/initdb.sh ./initdb.sh EXPOSE 8084 8087 CMD ["node", "dist/index.js"] diff --git a/docker/api/initdb.sh b/docker/api/initdb.sh index 4c34caf..c94640d 100644 --- a/docker/api/initdb.sh +++ b/docker/api/initdb.sh @@ -1,6 +1,7 @@ #!/bin/sh # B1Stack database initialisation script. -# Runs all SQL scripts in tools/dbScripts/ against the bundled database instance. +# Applies drizzle-kit migration SQL files from drizzle/// and +# loads stored procedures + demo data from tools/dbScripts/. # # Supports both MySQL and PostgreSQL based on DB_DIALECT env var. # @@ -20,6 +21,7 @@ set -e DIALECT="${DB_DIALECT:-mysql}" +MODULES="membership attendance content giving messaging doing" if [ "$DIALECT" = "postgres" ] || [ "$DIALECT" = "postgresql" ] || [ "$DIALECT" = "pg" ]; then # ─── PostgreSQL mode ───────────────────────────────────────────────────── @@ -27,6 +29,7 @@ if [ "$DIALECT" = "postgres" ] || [ "$DIALECT" = "postgresql" ] || [ "$DIALECT" USER="${PG_USER:-b1stack}" PORT="${PG_PORT:-5432}" DATABASE="${PG_DATABASE:-b1stack}" + DRIZZLE_DIALECT="postgresql" if [ -z "$PG_PASSWORD" ]; then echo "ERROR: PG_PASSWORD is required" >&2 @@ -49,21 +52,23 @@ if [ "$DIALECT" = "postgres" ] || [ "$DIALECT" = "postgresql" ] || [ "$DIALECT" done echo "PostgreSQL is ready." - for module in membership attendance content giving messaging doing reporting; do - dir="/app/tools/dbScripts/$module" - [ -d "$dir" ] || continue + for module in $MODULES; do echo "Initialising module: $module" - # Ensure schema exists and set search_path + # Ensure schema exists $CMD -c "CREATE SCHEMA IF NOT EXISTS $module" 2>/dev/null || true - for sql_file in "$dir"/*.sql; do - [ -f "$sql_file" ] || continue - # Skip MySQL stored procedure files on PG - if grep -qi "DELIMITER\|CREATE PROCEDURE\|CREATE DEFINER" "$sql_file" 2>/dev/null; then - echo " Skipping MySQL procedure: $(basename "$sql_file")" - continue - fi - PGOPTIONS="-c search_path=$module,public" $CMD -f "$sql_file" 2>&1 | grep -v "NOTICE" || true - done + + # Apply drizzle migration SQL files (CREATE TABLE statements) + migration_dir="/app/drizzle/$DRIZZLE_DIALECT/$module" + if [ -d "$migration_dir" ]; then + for sql_file in "$migration_dir"/*.sql; do + [ -f "$sql_file" ] || continue + echo " Migration: $(basename "$sql_file")" + PGOPTIONS="-c search_path=$module,public" $CMD -f "$sql_file" 2>&1 | grep -v "NOTICE" || true + done + fi + + # Skip stored procedures on PG (handled in application code) + echo " done." done @@ -72,6 +77,7 @@ else HOST="${MYSQL_HOST:-b1stack-mysql}" USER="${MYSQL_USER:-b1stack}" PORT="${MYSQL_PORT:-3306}" + DRIZZLE_DIALECT="mysql" if [ -z "$MYSQL_PASSWORD" ]; then echo "ERROR: MYSQL_PASSWORD is required" >&2 @@ -93,14 +99,37 @@ else done echo "MySQL is ready." - for module in membership attendance content giving messaging doing reporting; do - dir="/app/tools/dbScripts/$module" - [ -d "$dir" ] || continue + for module in $MODULES; do echo "Initialising module: $module" - for sql_file in "$dir"/*.sql; do - [ -f "$sql_file" ] || continue - $CMD --force "$module" < "$sql_file" 2>&1 | grep -v "Warning" || true - done + # Ensure database exists + $CMD -e "CREATE DATABASE IF NOT EXISTS \`$module\`" 2>&1 | grep -v "Warning" || true + + # Apply drizzle migration SQL files (CREATE TABLE statements) + migration_dir="/app/drizzle/$DRIZZLE_DIALECT/$module" + if [ -d "$migration_dir" ]; then + for sql_file in "$migration_dir"/*.sql; do + [ -f "$sql_file" ] || continue + echo " Migration: $(basename "$sql_file")" + $CMD --force "$module" < "$sql_file" 2>&1 | grep -v "Warning" || true + done + fi + + # Load stored procedures (MySQL only) + scripts_dir="/app/tools/dbScripts/$module" + if [ -d "$scripts_dir" ]; then + for sql_file in "$scripts_dir"/*.sql; do + [ -f "$sql_file" ] || continue + base=$(basename "$sql_file") + # Only load stored procedure files (skip demo data) + case "$base" in + cleanup.sql|deleteForChurch.sql|updateConversationStats.sql) + echo " Procedure: $base" + $CMD --force "$module" < "$sql_file" 2>&1 | grep -v "Warning" || true + ;; + esac + done + fi + echo " done." done fi diff --git a/load-tests/seed/large-church-pg.sql b/load-tests/seed/large-church-pg.sql new file mode 100644 index 0000000..7429a1b --- /dev/null +++ b/load-tests/seed/large-church-pg.sql @@ -0,0 +1,106 @@ +-- ============================================================ +-- B1Stack Demo Seed — Large Church (~5,000 members) — PostgreSQL +-- ============================================================ +-- Represents a large multi-service church: +-- 5,000 members across 1,800 households +-- 5 services (3 weekend + midweek + satellite campus) +-- 80 small groups / ministries +-- +-- Separate church record (CHU00000002) keeps large-church data +-- isolated from the small-church seed (CHU00000001). +-- +-- Run: +-- kubectl exec -n b1-postgres b1pg-pg-1 -- psql -U app membership \ +-- < load-tests/seed/large-church-pg.sql +-- ============================================================ + +-- ── Church record ─────────────────────────────────────────────────────────── +INSERT INTO churches (id, name, "subDomain", "registrationDate", address1, city, state, zip, country) +VALUES ('CHU00000002', 'Crossroads Fellowship', 'crossroads', NOW(), '1000 Church Blvd', 'Columbus', 'OH', '43215', 'US') +ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name; + +-- ── Households (1,800 for ~5,000 members, avg ~2.8 per household) ────────── +INSERT INTO households (id, "churchId", name) +SELECT + LPAD('LHH' || i::text, 11, '0'), + 'CHU00000002', + 'Household ' || i +FROM generate_series(1, 1800) AS i +ON CONFLICT (id) DO NOTHING; + +-- ── People (5,000 members) ────────────────────────────────────────────────── +INSERT INTO people ( + id, "churchId", "householdId", "firstName", "lastName", "displayName", + email, "membershipStatus", gender, removed +) +SELECT + LPAD('LPER' || i::text, 11, '0'), + 'CHU00000002', + LPAD('LHH' || LEAST(FLOOR((i - 1) / 2.78)::int + 1, 1800)::text, 11, '0'), + (ARRAY['James','Mary','John','Patricia','Robert','Jennifer', + 'Michael','Linda','William','Barbara','David','Susan', + 'Richard','Jessica','Joseph','Sarah','Thomas','Karen', + 'Charles','Lisa'])[1 + (i % 20)], + (ARRAY['Smith','Johnson','Williams','Brown','Jones','Garcia', + 'Miller','Davis','Wilson','Moore','Taylor','Anderson', + 'Thomas','Jackson','White','Harris','Martin','Thompson', + 'Wood','Martinez'])[1 + (i % 20)], + (ARRAY['James','Mary','John','Patricia','Robert','Jennifer', + 'Michael','Linda','William','Barbara','David','Susan', + 'Richard','Jessica','Joseph','Sarah','Thomas','Karen', + 'Charles','Lisa'])[1 + (i % 20)] + || ' ' || + (ARRAY['Smith','Johnson','Williams','Brown','Jones','Garcia', + 'Miller','Davis','Wilson','Moore','Taylor','Anderson', + 'Thomas','Jackson','White','Harris','Martin','Thompson', + 'Wood','Martinez'])[1 + (i % 20)], + CASE WHEN i % 3 = 0 THEN 'member' || i || '@crossroads.church' ELSE NULL END, + (ARRAY['Member','Regular Attender','Visitor','Member'])[1 + (i % 4)], + CASE WHEN i % 2 = 0 THEN 'Male' ELSE 'Female' END, + false +FROM generate_series(1, 5000) AS i +ON CONFLICT (id) DO NOTHING; + +-- ── Services (3 weekend + midweek + satellite campus) ─────────────────────── +-- services table is in the attendance schema +INSERT INTO attendance.services (id, "churchId", name) +VALUES + ('LSVC0000001', 'CHU00000002', 'Saturday Evening'), + ('LSVC0000002', 'CHU00000002', 'Sunday 8am'), + ('LSVC0000003', 'CHU00000002', 'Sunday 10:30am'), + ('LSVC0000004', 'CHU00000002', 'Sunday 10:30am East Campus'), + ('LSVC0000005', 'CHU00000002', 'Wednesday Night') +ON CONFLICT (id) DO NOTHING; + +-- ── 80 small groups / ministries ──────────────────────────────────────────── +INSERT INTO groups (id, "churchId", "categoryName", name, "trackAttendance", about) +SELECT + LPAD('LGRP' || i::text, 11, '0'), + 'CHU00000002', + (ARRAY['Young Adults','Married Couples','Senior Fellowship','Youth', + 'Mens','Womens','Prayer','Children','Outreach','Bible Study'])[1 + (i % 10)], + (ARRAY['Young Adults','Married Couples','Senior Fellowship','Youth', + 'Mens','Womens','Prayer','Children','Outreach','Bible Study'])[1 + (i % 10)] + || ' Group ' || i, + CASE WHEN i % 3 = 0 THEN false ELSE true END, + 'Fellowship group number ' || i +FROM generate_series(1, 80) AS i +ON CONFLICT (id) DO NOTHING; + +-- ── Group memberships (~2,000 members across 80 groups, avg 25/group) ───── +INSERT INTO "groupMembers" (id, "churchId", "groupId", "personId", leader, "joinDate") +SELECT + LPAD('LGM' || i::text, 11, '0'), + 'CHU00000002', + LPAD('LGRP' || (1 + (i % 80))::text, 11, '0'), + LPAD('LPER' || i::text, 11, '0'), + CASE WHEN i % 25 = 0 THEN true ELSE false END, + CURRENT_DATE - (RANDOM() * 365)::int +FROM generate_series(1, 2000) AS i +ON CONFLICT (id) DO NOTHING; + +SELECT 'Large church seed complete' AS status, + (SELECT COUNT(*) FROM people WHERE "churchId"='CHU00000002') AS people, + (SELECT COUNT(*) FROM households WHERE "churchId"='CHU00000002') AS households, + (SELECT COUNT(*) FROM groups WHERE "churchId"='CHU00000002') AS grps, + (SELECT COUNT(*) FROM "groupMembers" WHERE "churchId"='CHU00000002') AS members; diff --git a/repos.yaml b/repos.yaml index 42ef6d3..6dab39b 100644 --- a/repos.yaml +++ b/repos.yaml @@ -34,7 +34,7 @@ target: aggregated merges: - upstream main - - origin feat/playwright-e2e-tests + - origin feat/playwright-e2e-clean ./services/B1App: remotes: @@ -43,6 +43,7 @@ target: aggregated merges: - upstream main + - origin fix/missing-vendor-css - origin feat/playwright-e2e-tests ./services/LessonsApi: diff --git a/scripts/wait-ready.sh b/scripts/wait-ready.sh index 15d2e97..6ea518e 100755 --- a/scripts/wait-ready.sh +++ b/scripts/wait-ready.sh @@ -4,12 +4,33 @@ set -euo pipefail TIMEOUT=${WAIT_TIMEOUT:-120} INTERVAL=3 -NAMES=("Api" "B1Admin" "B1App") -URLS=("http://localhost:8084" "http://localhost:3101" "http://localhost:3301") +ALL_NAMES=("api" "b1admin" "b1app") +ALL_URLS=("http://localhost:8084" "http://localhost:3101" "http://localhost:3301") +ALL_LABELS=("Api" "B1Admin" "B1App") if [ "${1:-}" = "--full" ]; then - NAMES=("Api" "B1Admin" "B1App" "LessonsApi" "AskApi") - URLS=("http://localhost:8084/membership/churches" "http://localhost:3101" "http://localhost:3301" "http://localhost:8090" "http://localhost:8097") + ALL_NAMES=("api" "b1admin" "b1app" "lessonsapi" "askapi") + ALL_URLS=("http://localhost:8084/membership/churches" "http://localhost:3101" "http://localhost:3301" "http://localhost:8090" "http://localhost:8097") + ALL_LABELS=("Api" "B1Admin" "B1App" "LessonsApi" "AskApi") +fi + +# WAIT_SERVICES env var: comma-separated list of services to wait for (default: all) +NAMES=() +URLS=() +if [ -n "${WAIT_SERVICES:-}" ]; then + IFS=',' read -ra FILTER <<< "$WAIT_SERVICES" + for f in "${FILTER[@]}"; do + f=$(echo "$f" | tr '[:upper:]' '[:lower:]' | xargs) + for idx in "${!ALL_NAMES[@]}"; do + if [ "${ALL_NAMES[$idx]}" = "$f" ]; then + NAMES+=("${ALL_LABELS[$idx]}") + URLS+=("${ALL_URLS[$idx]}") + fi + done + done +else + NAMES=("${ALL_LABELS[@]}") + URLS=("${ALL_URLS[@]}") fi total=${#NAMES[@]}