Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 16 additions & 26 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 90
timeout-minutes: 120

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docker/api/Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
71 changes: 50 additions & 21 deletions docker/api/initdb.sh
Original file line number Diff line number Diff line change
@@ -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/<dialect>/<module>/ and
# loads stored procedures + demo data from tools/dbScripts/.
#
# Supports both MySQL and PostgreSQL based on DB_DIALECT env var.
#
Expand All @@ -20,13 +21,15 @@
set -e

DIALECT="${DB_DIALECT:-mysql}"
MODULES="membership attendance content giving messaging doing"

if [ "$DIALECT" = "postgres" ] || [ "$DIALECT" = "postgresql" ] || [ "$DIALECT" = "pg" ]; then
# ─── PostgreSQL mode ─────────────────────────────────────────────────────
HOST="${PG_HOST:-b1stack-pg}"
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
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down
106 changes: 106 additions & 0 deletions load-tests/seed/large-church-pg.sql
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion repos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
target: aggregated
merges:
- upstream main
- origin feat/playwright-e2e-tests
- origin feat/playwright-e2e-clean

./services/B1App:
remotes:
Expand All @@ -43,6 +43,7 @@
target: aggregated
merges:
- upstream main
- origin fix/missing-vendor-css
- origin feat/playwright-e2e-tests

./services/LessonsApi:
Expand Down
29 changes: 25 additions & 4 deletions scripts/wait-ready.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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[@]}
Expand Down
Loading