Testing Guide

v0.15.1 — Manual Testing Guide

Step-by-step instructions to verify match result email notifications locally and with a real Resend key.

Prerequisites

supabase start — local Supabase running
supabase db reset — fresh seed data applied
npm run dev (in apps/admin) — admin app on http://localhost:3000
supabase functions serve send-match-email --env-file /tmp/edge-fn-env — Edge Function running

1. Setup

Start local services

cd /workspaces/dev1

supabase start

supabase db reset

cd apps/admin && npm run dev &

Create Edge Function env file

cat > /tmp/edge-fn-env <<'EOF'

RESEND_API_KEY=re_test_XXXXXXXX # Get from resend.com (free tier)

WEB_APP_URL=http://localhost:3002

EOF

Start Edge Function

supabase functions serve send-match-email --env-file /tmp/edge-fn-env

Seed test API key (needed for v1 endpoints)

curl -s -X POST "http://127.0.0.1:54321/rest/v1/api_keys" \

-H "Content-Type: application/json" \

-H "apikey: $(supabase status | grep 'Secret' | awk '{print $NF}')" \

-H "Authorization: Bearer $(supabase status | grep 'Secret' | awk '{print $NF}')" \

-H "Prefer: return=minimal" \

-d '{

"facility_id": "00000000-0000-0000-0000-000000000001",

"key_hash": "50f535341eba98533273917efc99b525dbab3524752d77593446f99666eac6a3",

"key_prefix": "sk_live_test",

"name": "Manual Test Key",

"scopes": ["*"],

"is_active": true

}'

Test Scenarios (6)

T1

Edge Function — direct call, all 5 event types

Call the Edge Function directly with curl to verify data fetching, recipient logic, and email construction for each event type.

Get your local keys

# These are printed by "supabase status"

ANON_KEY="sb_publishable_..."

SERVICE_KEY="sb_secret_..."

T1a: result_submitted (excludes submitter)

curl -s -X POST "http://127.0.0.1:54321/functions/v1/send-match-email" \

-H "Content-Type: application/json" \

-H "apikey: $ANON_KEY" \

-H "Authorization: Bearer $SERVICE_KEY" \

-d '{

"event_type": "result_submitted",

"result_id": "eeeeeeee-0000-0000-0000-000000000001",

"actor_user_id": "aaaaaaaa-0000-0000-0000-000000000001"

}'

Expected: 3 recipients (Maria, Priya, Tom) — Carlos excluded as submitter.

T1b: result_confirmed (all participants)

curl -s -X POST "http://127.0.0.1:54321/functions/v1/send-match-email" \

-H "Content-Type: application/json" \

-H "apikey: $ANON_KEY" \

-H "Authorization: Bearer $SERVICE_KEY" \

-d '{

"event_type": "result_confirmed",

"result_id": "eeeeeeee-0000-0000-0000-000000000001",

"actor_user_id": "aaaaaaaa-0000-0000-0000-000000000002"

}'

Expected: 4 recipients (Carlos, Maria, Priya, Tom) — no exclusion.

T1c: result_auto_confirmed (no actor, all participants)

curl -s -X POST "http://127.0.0.1:54321/functions/v1/send-match-email" \

-H "Content-Type: application/json" \

-H "apikey: $ANON_KEY" \

-H "Authorization: Bearer $SERVICE_KEY" \

-d '{

"event_type": "result_auto_confirmed",

"result_id": "eeeeeeee-0000-0000-0000-000000000001"

}'

Expected: 4 recipients — no actor to exclude.

T1d: result_disputed (excludes disputer)

curl -s -X POST "http://127.0.0.1:54321/functions/v1/send-match-email" \

-H "Content-Type: application/json" \

-H "apikey: $ANON_KEY" \

-H "Authorization: Bearer $SERVICE_KEY" \

-d '{

"event_type": "result_disputed",

"result_id": "eeeeeeee-0000-0000-0000-000000000001",

"actor_user_id": "aaaaaaaa-0000-0000-0000-000000000002"

}'

Expected: 3 recipients (Carlos, Priya, Tom) — Maria excluded as disputer.

T1e: result_admin_resolved (all participants)

curl -s -X POST "http://127.0.0.1:54321/functions/v1/send-match-email" \

-H "Content-Type: application/json" \

-H "apikey: $ANON_KEY" \

-H "Authorization: Bearer $SERVICE_KEY" \

-d '{

"event_type": "result_admin_resolved",

"result_id": "eeeeeeee-0000-0000-0000-000000000001"

}'

Expected: 4 recipients — no exclusion.

Expected: Each call returns { sent: N, results: [...] }. With re_test_ key: sent=0 and status='failed' (expected — test keys don't send). With real key: sent=N and status='sent', and emails arrive in recipients' inboxes.

T2

Edge Function — input validation

Verify invalid payloads are rejected with proper error messages.

T2a: Missing result_id

curl -s -X POST "http://127.0.0.1:54321/functions/v1/send-match-email" \

-H "Content-Type: application/json" \

-H "apikey: $ANON_KEY" \

-H "Authorization: Bearer $SERVICE_KEY" \

-d '{"event_type": "result_submitted"}'

Expected: 400 — "event_type and result_id are required"

T2b: Invalid event_type

curl -s -X POST "http://127.0.0.1:54321/functions/v1/send-match-email" \

-H "Content-Type: application/json" \

-H "apikey: $ANON_KEY" \

-H "Authorization: Bearer $SERVICE_KEY" \

-d '{"event_type": "invalid", "result_id": "test"}'

Expected: 400 — "Unknown event_type: invalid"

T2c: Nonexistent result

curl -s -X POST "http://127.0.0.1:54321/functions/v1/send-match-email" \

-H "Content-Type: application/json" \

-H "apikey: $ANON_KEY" \

-H "Authorization: Bearer $SERVICE_KEY" \

-d '{"event_type": "result_submitted", "result_id": "00000000-0000-0000-0000-000000000000"}'

Expected: 404 — "Result not found"

Expected: Proper HTTP status codes and error messages for each case.

T3

API route integration — submit, confirm, dispute

Test the full flow through the admin API routes to verify the Edge Function is called as fire-and-forget.

Get a JWT token (sign in as Maria — participant in match 009)

MARIA_TOKEN=$(curl -s -X POST "http://127.0.0.1:54321/auth/v1/token?grant_type=password" \

-H "apikey: $ANON_KEY" \

-H "Content-Type: application/json" \

-d '{"email": "maria@demo.local", "password": "demo-password-123"}' \

| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

T3a: Submit result (match 009 — full, no existing result)

curl -s -X POST "http://localhost:3000/api/v1/matches/cccccccc-0000-0000-0000-000000000009/results" \

-H "Content-Type: application/json" \

-H "Authorization: Bearer $MARIA_TOKEN" \

-H "X-API-Key: sk_live_testkey1234567890abcdefgh" \

-d '{

"team_a_player_ids": ["aaaaaaaa-0000-0000-0000-000000000001", "aaaaaaaa-0000-0000-0000-000000000002"],

"team_b_player_ids": ["aaaaaaaa-0000-0000-0000-000000000005", "aaaaaaaa-0000-0000-0000-000000000007"],

"score": [{"team_a": 6, "team_b": 4}, {"team_a": 6, "team_b": 3}],

"winner": "team_a"

}'

Expected: 201 with result object. Check Edge Function logs for "[SEND-MATCH-EMAIL] result_submitted" log line. Verify: API response returns instantly (not blocked by email). Edge Function log shows correct recipient count.

T3b: Confirm result (sign in as Carlos — non-submitter participant)

CARLOS_TOKEN=$(curl -s -X POST "http://127.0.0.1:54321/auth/v1/token?grant_type=password" \

-H "apikey: $ANON_KEY" \

-H "Content-Type: application/json" \

-d '{"email": "carlos@demo.local", "password": "demo-password-123"}' \

| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

curl -s -X POST "http://localhost:3000/api/v1/matches/cccccccc-0000-0000-0000-000000000009/results/confirm" \

-H "Content-Type: application/json" \

-H "Authorization: Bearer $CARLOS_TOKEN" \

-H "X-API-Key: sk_live_testkey1234567890abcdefgh"

Expected: { "confirmed": true, "result_id": "..." }. Edge Function log shows "result_confirmed".

T3c: Dispute result (use match 010 — submit as Maria, dispute as Jake)

# Submit for match 010 first

curl -s -X POST "http://localhost:3000/api/v1/matches/cccccccc-0000-0000-0000-000000000010/results" \

-H "Content-Type: application/json" \

-H "Authorization: Bearer $MARIA_TOKEN" \

-H "X-API-Key: sk_live_testkey1234567890abcdefgh" \

-d '{

"team_a_player_ids": ["aaaaaaaa-0000-0000-0000-000000000002"],

"team_b_player_ids": ["aaaaaaaa-0000-0000-0000-000000000003"],

"score": [{"team_a": 6, "team_b": 4}, {"team_a": 6, "team_b": 3}],

"winner": "team_a"

}'

# Now dispute as Jake

JAKE_TOKEN=$(curl -s -X POST "http://127.0.0.1:54321/auth/v1/token?grant_type=password" \

-H "apikey: $ANON_KEY" \

-H "Content-Type: application/json" \

-d '{"email": "jake@demo.local", "password": "demo-password-123"}' \

| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

curl -s -X POST "http://localhost:3000/api/v1/matches/cccccccc-0000-0000-0000-000000000010/results/dispute" \

-H "Content-Type: application/json" \

-H "Authorization: Bearer $JAKE_TOKEN" \

-H "X-API-Key: sk_live_testkey1234567890abcdefgh" \

-d '{

"proposed_score": [{"team_a": 4, "team_b": 6}, {"team_a": 3, "team_b": 6}],

"proposed_winner": "team_b",

"reason": "Score was entered incorrectly"

}'

Expected: 201 with dispute object. Edge Function log shows "result_disputed".

Expected: All API responses return the same shape as before (no regressions). Edge Function logs show email attempts for each event.

T4

Admin resolve endpoint

Verify admin resolve triggers result_admin_resolved email.

Resolve the disputed result (match 016 — already in admin_review status)

curl -s -X POST "http://localhost:3000/api/admin/matches/cccccccc-0000-0000-0000-000000000016/results/resolve" \

-H "Content-Type: application/json" \

-d '{

"score": [{"team_a": 6, "team_b": 4}, {"team_a": 6, "team_b": 3}],

"winner": "team_a"

}'

Expected: { "resolved": true, "result_id": "..." }. Edge Function log shows "result_admin_resolved".

Expected: Resolve returns success. Edge Function log shows all 4 participants notified (no exclusion for admin resolve).

T5

pg_net — auto-confirm triggers email

Verify the PostgreSQL auto-confirm function calls the Edge Function via pg_net.

Set app.settings so pg_net knows the Supabase URL

docker exec -i $(docker ps --filter "name=supabase_db" --format "{{.Names}}") psql -U postgres -c "

ALTER DATABASE postgres SET app.settings.supabase_url = 'http://kong:8000';

ALTER DATABASE postgres SET app.settings.service_role_key = 'sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz';

ALTER DATABASE postgres SET app.settings.anon_key = 'sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH';

"

Force-expire a pending result's deadline

docker exec -i $(docker ps --filter "name=supabase_db" --format "{{.Names}}") psql -U postgres -c "

UPDATE match_results

SET confirmation_deadline = now() - interval '1 hour'

WHERE id = 'eeeeeeee-0000-0000-0000-000000000002';

"

Call auto-confirm manually

docker exec -i $(docker ps --filter "name=supabase_db" --format "{{.Names}}") psql -U postgres -c "

SELECT auto_confirm_match_results();

"

Note: This may fail with an Elo numeric overflow error (pre-existing issue — player_ratings.level is numeric(4,2) which can't hold 1500). This is NOT related to v0.15.1.

Alternative: test pg_net directly

docker exec -i $(docker ps --filter "name=supabase_db" --format "{{.Names}}") psql -U postgres -c "

SELECT net.http_post(

url := 'http://kong:8000/functions/v1/send-match-email',

body := '{"event_type": "result_auto_confirmed", "result_id": "eeeeeeee-0000-0000-0000-000000000001"}'::jsonb,

headers := '{"Content-Type": "application/json", "apikey": "sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH", "Authorization": "Bearer sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz"}'::jsonb

);

"

Check pg_net response

sleep 2

docker exec -i $(docker ps --filter "name=supabase_db" --format "{{.Names}}") psql -U postgres -c "

SELECT id, status_code, content FROM net._http_response ORDER BY id DESC LIMIT 1;

"

Expected: status_code = 200, content shows { "sent": N, "results": [...] }

Expected: pg_net successfully reaches the Edge Function from within PostgreSQL. Response logged in net._http_response with status 200.

T6

Real email delivery (requires Resend account)

Verify actual emails arrive in inboxes using a real Resend API key.

Get a free Resend API key

1. Sign up at https://resend.com (free tier = 100 emails/day)

2. Go to API Keys and create a new key

3. You'll get a key like re_abc123...

Update Edge Function env

cat > /tmp/edge-fn-env <<'EOF'

RESEND_API_KEY=re_YOUR_REAL_KEY_HERE

WEB_APP_URL=http://localhost:3002

EOF

# Restart the Edge Function

supabase functions serve send-match-email --env-file /tmp/edge-fn-env

Note on "from" address

Resend free tier only allows sending from onboarding@resend.dev unless you verify a domain.

You'll need to temporarily change the "from" in the Edge Function, OR verify your domain in Resend.

Quick test: send to your own email

You can insert a test profile with your real email and run a test:

# Update one of the demo users' email to yours

docker exec -i $(docker ps --filter "name=supabase_db" --format "{{.Names}}") psql -U postgres -c "

UPDATE customers SET email = 'your-real-email@example.com'

WHERE id = 'aaaaaaaa-0000-0000-0000-000000000002';

"

Then run test T1b (result_confirmed) — you should receive the email.

What to check in the email

- Subject line matches the event type

- Facility name in header ("Sunset Padel Club")

- "Hi [FirstName]," greeting

- Score displayed correctly (e.g., "6-3, 6-4")

- Winner shows "Team A" or "Team B"

- Sport, court, and date are correct

- "View Match" button links to the correct URL

- Footer says "Sent by [Facility] via Setpoint"

Expected: Real emails arrive in inbox with correct formatting, branding, and CTA link.

Seed Data Reference

Matches with results (from seed)

Match IDSportStatusResult StatusResult ID
...0014padelcompletedconfirmedeeee...0001
...0015pickleballcompletedpending_revieweeee...0002
...0016padelcompletedadmin_revieweeee...0003

Matches without results (for submission tests)

Match IDSportStatusPlayers
...0009padelfullCarlos, Maria, Alex, Tom
...0010tennisfullMaria, Jake
...0011padelfull(check match_players)

Demo users

User IDNameEmailPassword
...0001Carlos Ruizcarlos@demo.localdemo-password-123
...0002Maria Santosmaria@demo.localdemo-password-123
...0003Jake Thompsonjake@demo.localdemo-password-123
...0006Priya Patelpriya@demo.localdemo-password-123
...0007Tom Wilsontom@demo.localdemo-password-123

API Key for v1 endpoints

Key: sk_live_testkey1234567890abcdefgh

(must be seeded — see Setup section above)