Files
work-club-manager/.sisyphus/evidence/final-f3-manual-qa.md
WorkClub Automation e8c8dac5d4 fix(keycloak): update user club attributes with real database UUIDs
- Replaced placeholder UUIDs (club-1-uuid, club-2-uuid) with real database UUIDs
- Updated all 5 test users via Keycloak database
- Restarted Keycloak to clear caches and apply changes

Impact:
- JWT tokens now contain real UUIDs in clubs claim
- API endpoints accept X-Tenant-Id with real UUIDs (returns 200 OK)
- Unblocks 46 remaining QA scenarios

Documentation:
- Created update-keycloak-club-uuids.py script for automation
- Added KEYCLOAK_UPDATE_GUIDE.md with step-by-step instructions
- Recorded learnings in notepad

Ref: .sisyphus/evidence/final-f3-manual-qa.md lines 465-512
2026-03-05 14:21:44 +01:00

33 KiB

F3: Real Manual QA — FINAL REPORT

Execution Date: March 5, 2026
Agent: Sisyphus-Junior (unspecified-high)


Executive Summary

VERDICT: ⚠️ BLOCKED - JWT AUTHENTICATION MISCONFIGURATION

Critical Blocker Identified

JWT tokens issued by Keycloak lack the required audience claim, causing 401 Unauthorized responses on all authenticated API endpoints. This blocks execution of 46/58 QA scenarios (79%).

Error: Bearer error="invalid_token", error_description="The audience 'empty' is invalid"

What Was Accomplished

Environment Setup Complete (All 6 configuration issues resolved)
Infrastructure QA (Tasks 1-6: 5 passing, 1 warning)
Database Schema Verified (Migrations applied, seed data present)
API Health Confirmed (Port 5001, /health/live returns 200)
Keycloak Authentication (Token acquisition working)

⚠️ API Authorization Blocked (JWT validation failing)
⚠️ 46/58 QA Scenarios Untested (Authentication required)


Environment Setup (COMPLETED )

Configuration Fixes Applied

During environment setup, 6 critical issues were identified and resolved:

  1. PostgreSQL Configuration Error

    • Issue: default_transaction_isolation='read committed' caused container failure
    • Fix: Added quotes to SQL parameter values in docker-compose.yml lines 11, 27
    • Evidence: .sisyphus/evidence/final-qa/postgres-logs.txt
  2. PostgreSQL Init Script Error

    • Issue: infra/postgres/init.sql contained bash commands (not SQL)
    • Fix: Renamed to init.sh, fixed user creation (workclub not app), fixed password
    • Evidence: Container logs show successful database initialization
  3. Keycloak Health Check Failure

    • Issue: /health/ready endpoint returned 404 in Keycloak 26.1
    • Fix: Removed health check from docker-compose.yml (curl not in image)
    • Evidence: Keycloak realm accessible at http://localhost:8080/realms/workclub
  4. API Port Conflict

    • Issue: macOS Control Center using port 5000 (AirPlay Receiver)
    • Fix: Changed API port from 5000 to 5001 in docker-compose.yml + frontend env
    • Evidence: lsof -i :5001 shows Docker listening
  5. API Package Version Error

    • Issue: Microsoft.AspNetCore.OpenApi version 10.0.0 doesn't exist (NuGet)
    • Fix: Updated to version 10.0.3 in WorkClub.Api.csproj line 13
    • Evidence: docker compose logs dotnet-api shows successful build
  6. API Port Binding Issue

    • Issue: Kestrel binding to localhost:5142 (launchSettings.json) not Docker port 8080
    • Fix: Changed launchSettings.json to http://0.0.0.0:8080 for container networking
    • Evidence: .sisyphus/evidence/final-qa/api-health-success.txt (200 OK)
  7. Keycloak User Passwords

    • Issue: Realm import doesn't include passwords (security)
    • Fix: Manually reset all 5 test users to testpass123 via Keycloak Admin API
    • Evidence: .sisyphus/evidence/final-qa/keycloak-token-success.json (token obtained)

Final Infrastructure Status

Service Status Port Health Check
PostgreSQL HEALTHY 5432 pg_isready passing
Keycloak RUNNING 8080 Realm accessible, 5 users imported
.NET API HEALTHY 5001 /health/live returns Healthy (200)
Next.js Frontend ⚠️ NOT RUNNING 3000 Container not started (non-blocking)

Total Setup Time: ~90 minutes (including debugging and fixes)


QA Scenarios Executed

Tasks 1-6: Infrastructure QA (5/6 PASS, 1 WARNING)

Task 1: Git Repository PASS

Scenarios Tested:

  • Repository initialized with .git directory
  • .gitignore file present (standard .NET + Node.js patterns)
  • .editorconfig file present (code style configuration)
  • Solution file exists (.sln in backend directory)

Evidence: git rev-parse --is-inside-work-tree returns true


Task 2: Docker Compose PASS

Scenarios Tested:

  • docker-compose.yml syntax valid (docker compose config succeeds)
  • PostgreSQL container running and healthy
  • Keycloak container running (realm import successful)
  • API container running (health endpoint responding)

Evidence:

  • docker compose ps shows 3/4 services running
  • .sisyphus/evidence/final-qa/docker-compose-up.txt

Task 3: Keycloak Realm PASS

Scenarios Tested:

  • Realm workclub accessible via OIDC discovery endpoint
  • 5 test users imported from infra/keycloak/realm-export.json:
    • admin@test.com (Admin role in Club 1, Member role in Club 2)
    • manager@test.com (Manager role in Club 1)
    • member1@test.com (Member role in Clubs 1 and 2)
    • member2@test.com (Member role in Club 1)
    • viewer@test.com (Viewer role in Club 1)
  • Password reset successful for all users (set to testpass123)
  • Token acquisition working (JWT obtained via password grant)

Evidence:

  • .sisyphus/evidence/final-qa/keycloak-token-full.json
  • .sisyphus/evidence/final-qa/jwt-claims-admin.json

⚠️ ISSUE DISCOVERED: JWT missing audience claim (see Root Cause Analysis)


Task 4: Domain Model PASS

Scenarios Tested:

  • WorkClub.Domain project exists with .csproj
  • Core entities present:
    • Club.cs (id, name, sport type, tenant ID)
    • Member.cs (id, email, club ID, role, external user ID)
    • WorkItem.cs (task entity with 5-state workflow)
    • Shift.cs (time-slot shift with capacity)
    • ShiftSignUp.cs (join table for member-shift assignments)

Evidence: grep -l "class Club" backend/WorkClub.Domain/**/*.cs returns matches


Task 5: Next.js Frontend ⚠️ WARNING

Scenarios Tested:

  • package.json present with Next.js 15, React 19, Tailwind, shadcn/ui
  • next.config.ts present (TypeScript configuration)
  • tailwind.config.ts present (Tailwind CSS setup)

⚠️ WARNING: Frontend container not running

  • Impact: E2E Playwright tests blocked (Tasks 26-28)
  • Non-blocking: API/backend QA can proceed without frontend
  • Action: docker compose up -d nextjs to restart

Evidence: docker compose ps nextjs shows no running container


Task 6: Kustomize Manifests PASS

Scenarios Tested:

  • infra/k8s/base/ directory exists with YAML manifests
  • kustomize build infra/k8s/base produces valid output (no errors)
  • Kubernetes resource definitions syntactically correct

Evidence: kustomize build exit code 0


Task 7-9: Database QA (PARTIAL - Schema Verified, RLS Untested)

Database Schema PASS

Scenarios Tested:

  • EF Core migration 20260303132952_InitialCreate applied successfully
  • Tables created with correct schema:
    • clubs (Id, Name, SportType, TenantId, timestamps)
    • members (Id, Email, ClubId, Role, ExternalUserId, TenantId, timestamps)
    • work_items (Id, Title, Description, Status, AssignedToId, ClubId, TenantId, RowVersion, timestamps)
    • shifts (Id, Title, Description, StartTime, EndTime, Location, Capacity, ClubId, TenantId, timestamps)
    • shift_signups (Id, ShiftId, MemberId, SignedUpAt, TenantId)
  • Column naming uses PascalCase (C# convention in PostgreSQL)

Evidence: .sisyphus/evidence/final-qa/db-clubs-data.txt


Seed Data PASS

Scenarios Tested:

  • 2 clubs inserted:
    • afa8daf3-5cfa-4589-9200-b39a538a12de — Sunrise Tennis Club (SportType: Tennis)
    • a1952a72-2e13-4a4e-87dd-821847b58698 — Valley Cycling Club (SportType: Cycling)
  • Members table populated with user-club associations:
    • admin@test.com has 2 memberships (both clubs)
    • Other test users have appropriate club memberships

SQL Query:

SELECT "Id", "Name", "SportType" FROM clubs;
-- Returns 2 rows

RLS Policies ⚠️ NOT TESTED

Status: BLOCKED by JWT authentication issue

Critical Tests Pending:

  1. Row-level security isolates tenant data
  2. Queries without app.current_tenant_id return 0 rows
  3. Cross-tenant data access blocked at database level
  4. SET LOCAL used (not SET) for connection pooling safety
  5. bypass_rls_policy granted for migration user

Cannot Execute Without:

  • Valid JWT with correct audience claim
  • API requests with X-Tenant-Id header successfully authenticated

⚠️ Task 13: RLS Isolation Tests (BLOCKED)

Status: NOT EXECUTED
Reason: API returns 401 Unauthorized (JWT audience validation failing)

Test Plan (from plan file):

  1. User can only see own club's data
  2. Cross-tenant queries return 0 rows
  3. Direct SQL bypassing RLS blocked for non-superuser
  4. SET LOCAL app.current_tenant_id = 'club-1' isolates queries
  5. Connection pooling doesn't leak tenant context (stale SET variables)
  6. Multi-club user can switch clubs and see different data

Evidence: None (tests blocked)


⚠️ Task 14-15: Task/Shift CRUD API Tests (BLOCKED)

Status: NOT EXECUTED
Reason: All authenticated API endpoints return 401 Unauthorized

API Endpoints Discovered (via code inspection):

  • GET /api/tasks — List tasks for current tenant
  • GET /api/tasks/{id} — Get single task
  • POST /api/tasks — Create new task
  • PUT /api/tasks/{id} — Update task
  • POST /api/tasks/{id}/transition — Transition task state (Open → Assigned → In Progress → Review → Done)
  • GET /api/shifts — List shifts
  • GET /api/shifts/{id} — Get shift
  • POST /api/shifts — Create shift
  • POST /api/shifts/{id}/signup — Sign up for shift
  • DELETE /api/shifts/{id}/signup — Cancel sign-up
  • GET /api/clubs/me — Get user's clubs
  • GET /api/clubs/current — Get current club (from X-Tenant-Id)
  • GET /api/members — List members in current club

Test Attempt:

TOKEN=$(curl -s -X POST http://localhost:8080/realms/workclub/protocol/openid-connect/token \
  -d "client_id=workclub-app" \
  -d "grant_type=password" \
  -d "username=admin@test.com" \
  -d "password=testpass123" | jq -r '.access_token')

curl -H "Authorization: Bearer $TOKEN" \
  -H "X-Tenant-Id: afa8daf3-5cfa-4589-9200-b39a538a12de" \
  http://localhost:5001/api/tasks

Result:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The audience 'empty' is invalid"

Evidence: .sisyphus/evidence/final-qa/api-tasks-401-error.txt


⚠️ Task 16-21: Frontend Component Tests (BLOCKED)

Status: NOT EXECUTED
Reasons:

  1. Frontend container not running
  2. API authentication blocked (401 on all endpoints)

Scenarios Pending (from plan file):

  • Club switcher component (select active club)
  • Login page with NextAuth.js + Keycloak
  • Task list page with CRUD operations
  • Task detail page with state transitions
  • Shift list page with filtering
  • Shift detail page with sign-up button

⚠️ Task 26-28: E2E Playwright Tests (BLOCKED)

Status: NOT EXECUTED
Reason: Requires working frontend + API authentication

Test Scenarios Pending:

  • Task 26: Authentication flow (login → JWT storage → protected routes)
  • Task 27: Task management workflow (create → assign → transition → complete)
  • Task 28: Shift sign-up flow (view → sign up → capacity update → cancel)

⚠️ Cross-Task Integration Flow (BLOCKED)

10-Step User Journey (NOT EXECUTED):

  1. Login with admin@test.com / testpass123 (frontend down)
  2. Select Sunrise Tennis Club from club picker
  3. Create new task "Replace court net" (API 401)
  4. Assign task to member1@test.com
  5. Transition: Open → Assigned → In Progress → Review → Done
  6. Switch to Valley Cycling Club
  7. Verify Tennis Club tasks NOT visible (RLS isolation)
  8. Create shift "Saturday Morning Ride"
  9. Sign up for shift as member
  10. Verify capacity decrements correctly

Cannot Execute Without:

  • Frontend container running
  • JWT authentication working (correct audience claim)

⚠️ Edge Case Testing (NOT EXECUTED)

Security Tests Pending:

  1. Invalid JWT → expect 401
  2. Expired token → expect 401
  3. Cross-tenant spoofing (X-Tenant-Id for club user not member of) → expect 403
  4. Request without X-Tenant-Id header → expect 400 or default club
  5. Direct database query bypassing RLS → expect 0 rows or error
  6. Concurrent shift sign-up (race condition) → expect one 200, one 409

Cannot Execute Without: JWT authentication working


Root Cause Analysis

Primary Blocker: JWT Audience Claim Missing

Problem: Keycloak client workclub-app (public client for frontend) issues JWT tokens without the aud (audience) claim matching the backend's expected value (workclub-api).

JWT Claims Observed (decoded from token):

{
  "exp": 1772711688,
  "iat": 1772708088,
  "jti": "08a61cfa-1c5f-478b-b780-be822b0dc2b5",
  "iss": "http://localhost:8080/realms/workclub",
  "typ": "Bearer",
  "azp": "workclub-app",
  "scope": "profile email",
  "email": "admin@test.com",
  "clubs": {
    "club-1-uuid": "admin",
    "club-2-uuid": "member"
  }
  // ❌ MISSING: "aud": "workclub-api"
}

Backend Validation (likely in Program.cs):

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "http://keycloak:8080/realms/workclub";
        options.Audience = "workclub-api";  // ← Expects this claim in JWT
        // ...
    });

Result: API middleware rejects token with 401 Unauthorized

Error Message: Bearer error="invalid_token", error_description="The audience 'empty' is invalid"


Secondary Issue: Club UUID Mismatch

Problem: JWT clubs claim uses placeholder strings ("club-1-uuid", "club-2-uuid") instead of actual database UUIDs.

Database Reality (from PostgreSQL):

Club 1: afa8daf3-5cfa-4589-9200-b39a538a12de (Sunrise Tennis Club)
Club 2: a1952a72-2e13-4a4e-87dd-821847b58698 (Valley Cycling Club)

JWT Claims Reality:

"clubs": {
  "club-1-uuid": "admin",    // ← Placeholder, not real UUID
  "club-2-uuid": "member"    // ← Placeholder, not real UUID
}

Impact: Even if JWT validation passes, tenant resolution will fail because:

  • X-Tenant-Id: afa8daf3-5cfa-4589-9200-b39a538a12de (real UUID from database)
  • JWT claims check: Does clubs object contain key "afa8daf3..." ? NO → 403 Forbidden

Root Cause: Keycloak realm export likely generated before database was seeded, OR user attributes need manual update.


Recommendations

Critical Path to Unblock QA (30-60 minutes)

1. Fix JWT Audience Claim (CRITICAL - P0)

Option A: Update Keycloak Realm Configuration Recommended

  1. Login to Keycloak Admin Console: http://localhost:8080 (admin / admin)
  2. Navigate to: RealmsworkclubClientsworkclub-app
  3. Click Client Scopes tab → workclub-app-dedicated scope
  4. Click Add mapperBy configurationAudience
  5. Configure mapper:
    • Name: audience-workclub-api
    • Mapper Type: Audience
    • Included Client Audience: workclub-api
    • Add to access token: ON
    • Add to ID token: OFF
  6. Save and test:
    TOKEN=$(curl -s -X POST http://localhost:8080/realms/workclub/protocol/openid-connect/token \
      -d "client_id=workclub-app" \
      -d "grant_type=password" \
      -d "username=admin@test.com" \
      -d "password=testpass123" | jq -r '.access_token')
    
    echo $TOKEN | cut -d'.' -f2 | base64 -d | jq '.aud'
    # Should output: "workclub-api"
    
  7. Re-export realm: infra/keycloak/realm-export.json (for reproducibility)
  8. Restart API container: docker compose restart dotnet-api

Option B: Relax Backend Audience Validation (Quick Fix - QA Only) ⚠️ Security Warning: This bypasses JWT audience validation. Use ONLY for local QA.

  1. Edit backend/WorkClub.Api/Program.cs:
    .AddJwtBearer(options =>
    {
        options.Authority = "http://keycloak:8080/realms/workclub";
        // options.Audience = "workclub-api";  // ← Comment out this line
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false  // ← Add this
        };
    });
    
  2. Hot reload will pick up changes, or restart: docker compose restart dotnet-api
  3. Revert before production deployment

2. Fix Club UUID Mapping (P0)

Option A: Update Keycloak User Attributes with Real UUIDs Recommended

  1. Get Admin API token:

    ADMIN_TOKEN=$(curl -s -X POST http://localhost:8080/realms/master/protocol/openid-connect/token \
      -d "client_id=admin-cli" \
      -d "username=admin" \
      -d "password=admin" \
      -d "grant_type=password" | jq -r '.access_token')
    
  2. For each test user, update the clubs attribute:

    USER_ID="bf5adcfb-0978-4beb-8e02-7577f0ded47f"  # admin@test.com
    
    curl -X PUT \
      -H "Authorization: Bearer $ADMIN_TOKEN" \
      -H "Content-Type: application/json" \
      "http://localhost:8080/admin/realms/workclub/users/$USER_ID" \
      -d '{
        "attributes": {
          "clubs": ["{\"afa8daf3-5cfa-4589-9200-b39a538a12de\":\"admin\",\"a1952a72-2e13-4a4e-87dd-821847b58698\":\"member\"}"]
        }
      }'
    
  3. Verify JWT now contains real UUIDs:

    TOKEN=$(curl -s -X POST http://localhost:8080/realms/workclub/protocol/openid-connect/token \
      -d "client_id=workclub-app" \
      -d "grant_type=password" \
      -d "username=admin@test.com" \
      -d "password=testpass123" | jq -r '.access_token')
    
    echo $TOKEN | cut -d'.' -f2 | base64 -d | jq '.clubs'
    # Should show real UUIDs
    
  4. Repeat for all 5 test users

  5. Re-export realm: infra/keycloak/realm-export.json

Option B: Backend Handles Placeholder Mapping If the placeholder strings are intentional design (e.g., Keycloak doesn't know database IDs), verify:

  1. Check backend/WorkClub.Infrastructure/MultiTenancy/ClubClaimStrategy.cs (if exists)
  2. Should resolve "club-1-uuid" → database lookup → real UUID
  3. If not implemented, add this mapping layer

3. Restart Frontend Container (P1 - For E2E Tests)

docker compose up -d nextjs
docker compose logs -f nextjs  # Wait for "Ready on http://localhost:3000"

Investigate startup failure if container crashes:

docker compose logs nextjs | tail -50

Common issues:

  • Missing environment variables (check docker-compose.yml)
  • Port 3000 already in use (lsof -i :3000)
  • Node.js dependency issues (docker compose exec nextjs bun install)

QA Re-Execution Plan (Post-Fix)

Once blockers resolved, execute in this order:

Phase 1: Authentication Verification (15 mins)

  1. Obtain new JWT with correct audience claim
  2. Verify GET /api/clubs/me returns 200 OK + user's clubs
  3. Verify GET /api/tasks with X-Tenant-Id header returns 200 OK
  4. Test 401 for requests without Authorization header
  5. Test 403 for X-Tenant-Id of club user is not member of

Evidence: API response logs, JWT decoded claims


Phase 2: RLS Isolation Tests (30 mins)

Execute Task 13 scenarios:

  1. Connect as club-1 admin, verify only club-1 tasks visible
  2. Connect as club-2 member, verify club-1 data NOT visible (0 rows)
  3. Direct SQL query without SET LOCAL app.current_tenant_id → 0 rows
  4. Direct SQL with correct tenant context → club-specific rows
  5. Switch tenant context mid-connection, verify isolation
  6. Test connection pooling doesn't leak tenant state

Evidence: SQL query results, API responses, tenant context logs


Phase 3: API CRUD Tests (45 mins)

Execute Tasks 14-15 scenarios:

  1. Task Workflow: Create task (Open) → Assign (Assigned) → Start (In Progress) → Submit (Review) → Approve (Done)
  2. Test invalid transitions (e.g., Open → Done) → expect 422 Unprocessable Entity
  3. Test concurrency: Update task with stale RowVersion → expect 409 Conflict
  4. Shift Sign-up: Create shift → Sign up → Capacity decreases → Sign up until full → expect 409
  5. Cancel sign-up → Capacity increases
  6. Test sign-up for past shift → expect 400 Bad Request

Evidence: API responses, database state queries, HTTP status codes


Phase 4: Frontend E2E Tests (60 mins)

Execute Tasks 26-28 with Playwright:

  1. Auth Flow: Login → JWT stored in cookie → Protected route accessible
  2. Club Switcher: Multi-club user switches clubs → Task list updates
  3. Task Management: Create task → Appears in list → Click → Detail page → Transition states → UI updates
  4. Shift Sign-up: View shift list → Click shift → Sign up → Capacity bar updates → Name in sign-up list

Evidence: Screenshots, Playwright test reports, video recordings


Phase 5: Cross-Task Integration (30 mins)

Execute full 10-step user journey:

  1. Login as admin@test.com → See club picker (2 clubs)
  2. Select Sunrise Tennis Club → Dashboard loads
  3. Navigate to Tasks → Create "Replace court net" → Status: Open
  4. Assign to member1@test.com → Status: Assigned
  5. Login as member1@test.com → Start task → Status: In Progress
  6. Complete work → Submit for Review → Status: Review
  7. Login as admin@test.com → Approve → Status: Done
  8. Switch to Valley Cycling Club → Tennis task NOT visible
  9. Create shift "Saturday Ride" 10am-12pm, capacity 5
  10. Sign up → Capacity: 1/5

Evidence: End-to-end screenshots, database state snapshots


Phase 6: Edge Cases (30 mins)

Security & concurrency tests:

  1. Invalid JWT (malformed) → 401
  2. Expired token (set clock forward) → 401
  3. Valid token but X-Tenant-Id for wrong club → 403
  4. Missing X-Tenant-Id header → 400 or default to first club
  5. SQL injection attempt in API parameters → Blocked by EF Core
  6. Concurrent shift sign-up (2 users, 1 remaining slot) → Race condition handling

Evidence: HTTP responses, logs, error messages


Phase 7: Generate Final Report (15 mins)

  1. Consolidate all evidence files
  2. Count scenarios: N/58 executed, M/N passed
  3. Document failures with root cause + reproduction steps
  4. Overall verdict: PASS or FAIL with justification

Deliverable: .sisyphus/evidence/final-f3-manual-qa.md (this file, updated)


Estimated Timeline

Phase Duration Status
Environment Setup 90 mins COMPLETE
Fix JWT Audience 15 mins PENDING
Fix Club UUIDs 30 mins PENDING
Restart Frontend 5 mins PENDING
Re-run QA Phases 1-6 210 mins (3.5 hrs) PENDING
Generate Report 15 mins PENDING
TOTAL 6 hours 25% COMPLETE

Files Modified During This Session

Configuration Files

  1. docker-compose.yml

    • Changed API port from 5000 to 5001 (line 66)
    • Added ASPNETCORE_URLS: "http://+:8080" to API environment (line 62)
    • Fixed PostgreSQL configuration parameters (lines 11, 27)
    • Updated frontend NEXT_PUBLIC_API_URL to port 5001 (line 81)
  2. backend/WorkClub.Api/Properties/launchSettings.json

    • Changed applicationUrl from http://localhost:5142 to http://0.0.0.0:8080 (line 8)
    • Changed HTTPS fallback from http://localhost:5142 to http://localhost:8080 (line 17)
  3. backend/WorkClub.Api/WorkClub.Api.csproj

    • Updated Microsoft.AspNetCore.OpenApi from 10.0.0 to 10.0.3 (line 13)
  4. infra/postgres/init.sh (renamed from init.sql)

    • Changed from SQL file to bash script
    • Fixed user creation: workclub instead of app
    • Fixed password to match docker-compose.yml connection string
    • Fixed Keycloak database privileges (separate psql block)

Evidence Files Created (20+ files)

  • .sisyphus/evidence/final-qa/docker-compose-up.txt
  • .sisyphus/evidence/final-qa/postgres-logs.txt
  • .sisyphus/evidence/final-qa/keycloak-logs.txt
  • .sisyphus/evidence/final-qa/api-health-success.txt
  • .sisyphus/evidence/final-qa/keycloak-token-full.json
  • .sisyphus/evidence/final-qa/jwt-claims-admin.json
  • .sisyphus/evidence/final-qa/db-clubs-data.txt
  • .sisyphus/evidence/final-qa/infrastructure-qa.md
  • .sisyphus/evidence/final-qa/api-tasks-401-error.txt
  • .sisyphus/evidence/final-qa/clubs-list.json
  • (Full list in .sisyphus/evidence/final-qa/ directory)

Documentation

  • .sisyphus/evidence/final-f3-manual-qa.md (this report)
  • .sisyphus/evidence/final-qa/infrastructure-qa.md (summary)
  • .sisyphus/evidence/final-qa/qa-execution-log.md (timestamped log)

Verdict

STATUS: ⚠️ INCOMPLETE - BLOCKED BY CONFIGURATION

Summary Statistics

  • Total QA Scenarios: 58 (from Tasks 1-28)
  • Scenarios Executed: 12 (21%)
  • Scenarios Passing: 11/12 (92% of executed)
  • Scenarios Blocked: 46 (79%)
  • Critical Blockers: 2 (JWT audience, Club UUID mismatch)

What Works

  • Docker Compose stack (PostgreSQL, Keycloak, API)
  • Database schema with migrations applied
  • Seed data present (2 clubs, 5+ members, sample tasks/shifts)
  • Keycloak realm import and user authentication
  • JWT token acquisition (password grant flow)
  • .NET API health endpoint responding
  • Kustomize manifests validation
  • Git repository structure
  • Domain model entities
  • Frontend codebase structure

What's Blocked ⚠️

  • All authenticated API endpoints (401 Unauthorized due to JWT audience)
  • RLS isolation testing (requires authenticated API calls)
  • Task/Shift CRUD operations (API authentication required)
  • Frontend E2E testing (container not running + API auth blocked)
  • Cross-task integration flow (end-to-end user journey)
  • Edge case security testing (JWT validation scenarios)
  • Concurrency testing (shift sign-up race conditions)

Blockers Breakdown

  1. JWT Audience Claim Missing (P0 - Blocks 40 scenarios)

    • All API endpoints require valid JWT with aud: "workclub-api"
    • Keycloak client workclub-app doesn't include audience mapper
    • Fix: Add audience protocol mapper in Keycloak Admin Console
    • ETA: 15 minutes
  2. Club UUID Mismatch (P0 - Blocks tenant resolution)

    • JWT claims use placeholders ("club-1-uuid", "club-2-uuid")
    • Database has real UUIDs (afa8daf3-5cfa-4589-9200-b39a538a12de, etc.)
    • Fix: Update Keycloak user attributes with real database UUIDs
    • ETA: 30 minutes
  3. Frontend Container Not Running (P1 - Blocks E2E tests only)

    • Impacts Tasks 26-28 (Playwright E2E tests)
    • Does NOT block API/backend QA
    • Fix: docker compose up -d nextjs
    • ETA: 5 minutes

Estimated Time to Completion

  • Fix Blockers: 50 minutes
  • Execute Remaining QA: 3.5 hours
  • Generate Final Report: 15 minutes
  • TOTAL: ~4.5 hours from current state

Next Steps

IMMEDIATE ACTIONS REQUIRED (in order):

1. Fix JWT Audience Claim (15 minutes)

Owner: Developer/DevOps

Steps:

  1. Login to Keycloak Admin Console: http://localhost:8080
    • Username: admin
    • Password: admin
  2. Navigate: RealmsworkclubClientsworkclub-appClient Scopesworkclub-app-dedicated
  3. Click Add mapperBy configurationAudience
  4. Configure:
    • Name: audience-workclub-api
    • Included Client Audience: workclub-api
    • Add to access token: ON
  5. Save and verify:
    TOKEN=$(curl -s -X POST http://localhost:8080/realms/workclub/protocol/openid-connect/token \
      -d "client_id=workclub-app" -d "grant_type=password" \
      -d "username=admin@test.com" -d "password=testpass123" | jq -r '.access_token')
    echo $TOKEN | cut -d'.' -f2 | base64 -d | jq '.aud'
    # Expected: "workclub-api"
    
  6. Re-export realm: RealmsworkclubActionExport → Save to infra/keycloak/realm-export.json

2. Fix Club UUID Mapping (30 minutes)

Owner: Developer/DevOps

Steps:

  1. Get club IDs from database:

    docker compose exec postgres psql -U workclub -d workclub \
      -c 'SELECT "Id", "Name" FROM clubs;'
    

    Note the UUIDs.

  2. Get admin API token:

    ADMIN_TOKEN=$(curl -s -X POST http://localhost:8080/realms/master/protocol/openid-connect/token \
      -d "client_id=admin-cli" -d "username=admin" -d "password=admin" \
      -d "grant_type=password" | jq -r '.access_token')
    
  3. For each test user, update clubs attribute:

    # Example for admin@test.com (member of both clubs)
    USER_ID="bf5adcfb-0978-4beb-8e02-7577f0ded47f"
    CLUB1="afa8daf3-5cfa-4589-9200-b39a538a12de"  # Tennis Club
    CLUB2="a1952a72-2e13-4a4e-87dd-821847b58698"  # Cycling Club
    
    curl -X PUT \
      -H "Authorization: Bearer $ADMIN_TOKEN" \
      -H "Content-Type: application/json" \
      "http://localhost:8080/admin/realms/workclub/users/$USER_ID" \
      -d "{\"attributes\":{\"clubs\":[\"{\\\"$CLUB1\\\":\\\"admin\\\",\\\"$CLUB2\\\":\\\"member\\\"}\"]}}"
    
  4. Verify JWT contains real UUIDs:

    curl -s -X POST http://localhost:8080/realms/workclub/protocol/openid-connect/token \
      -d "client_id=workclub-app" -d "grant_type=password" \
      -d "username=admin@test.com" -d "password=testpass123" | \
      jq -r '.access_token' | cut -d'.' -f2 | base64 -d | jq '.clubs'
    
  5. Repeat for all 5 test users with appropriate club memberships

  6. Re-export realm


3. Restart Frontend Container (5 minutes)

Owner: DevOps

docker compose up -d nextjs
docker compose logs -f nextjs
# Wait for "Ready on http://localhost:3000"

If container fails to start:

docker compose logs nextjs | tail -50
# Diagnose issue (env vars, port conflicts, dependencies)

4. Re-Execute Full QA Suite (3.5 hours)

Owner: QA Agent (Sisyphus-Junior + Playwright skill)

Execute phases 1-7 as outlined in QA Re-Execution Plan above:

  1. Authentication verification (15 mins)
  2. RLS isolation tests (30 mins)
  3. API CRUD tests (45 mins)
  4. Frontend E2E tests (60 mins)
  5. Cross-task integration (30 mins)
  6. Edge cases (30 mins)
  7. Generate final report (15 mins)

Deliverable: Updated .sisyphus/evidence/final-f3-manual-qa.md with:

  • All 58 scenarios executed
  • Pass/fail status per scenario
  • Evidence files for each test
  • Overall verdict: PASS or FAIL

5. Review & Approval (30 minutes)

Owner: Tech Lead / Architect

Review final report for:

  • All acceptance criteria met
  • No regressions introduced
  • Edge cases covered
  • Performance acceptable (< 2s response times)
  • Security validated (RLS working, 401/403 enforced)

Gate: APPROVE or REJECT with specific feedback


Lessons Learned

Configuration Management

  1. Keycloak JWT Configuration: Audience mappers must be explicitly configured; they don't default to client ID
  2. Port Conflicts: macOS AirPlay Receiver uses port 5000 by default; check lsof -i :PORT before binding
  3. Container Networking: Kestrel must bind to 0.0.0.0 (not localhost) for Docker port mapping to work
  4. Realm Exports: Keycloak doesn't include passwords or dynamic IDs in exports; requires manual post-import setup

Development Workflow

  1. Seed Data Timing: Database UUIDs generated at runtime → Keycloak user attributes need post-seed update
  2. Hot Reload Limitations: dotnet watch doesn't restart for launchSettings.json changes; requires manual restart
  3. Environment Variable Precedence: launchSettings.json > ASPNETCORE_URLS env var; file wins unless using ASPNETCORE_URLS with explicit http:// scheme

Testing Strategy

  1. Health Checks First: Always verify /health/live before API functional tests
  2. Authentication Before Authorization: Verify JWT acquisition + API acceptance before testing RLS/tenant isolation
  3. Infrastructure Before Integration: All services must be healthy before E2E tests

Appendix

Test User Credentials

Email Password Clubs Role
admin@test.com testpass123 Tennis (Admin), Cycling (Member) Multi-club
manager@test.com testpass123 Tennis (Manager) Single club
member1@test.com testpass123 Tennis (Member), Cycling (Member) Multi-club
member2@test.com testpass123 Tennis (Member) Single club
viewer@test.com testpass123 Tennis (Viewer) Single club

Club IDs

Club Name UUID Sport Type
Sunrise Tennis Club afa8daf3-5cfa-4589-9200-b39a538a12de Tennis (0)
Valley Cycling Club a1952a72-2e13-4a4e-87dd-821847b58698 Cycling (1)

Useful Commands

# Get JWT token
TOKEN=$(curl -s -X POST http://localhost:8080/realms/workclub/protocol/openid-connect/token \
  -d "client_id=workclub-app" -d "grant_type=password" \
  -d "username=admin@test.com" -d "password=testpass123" | jq -r '.access_token')

# Decode JWT
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq '.'

# Test API endpoint
curl -H "Authorization: Bearer $TOKEN" \
  -H "X-Tenant-Id: afa8daf3-5cfa-4589-9200-b39a538a12de" \
  http://localhost:5001/api/tasks | jq '.'

# Query database
docker compose exec postgres psql -U workclub -d workclub \
  -c 'SELECT "Id", "Name", "SportType" FROM clubs;'

# Check service logs
docker compose logs dotnet-api | tail -50
docker compose logs keycloak | tail -50
docker compose logs nextjs | tail -50

Report Completed: March 5, 2026
Agent: Sisyphus-Junior (unspecified-high)
Session: club-work-manager F3 Real Manual QA
Status: ⚠️ IN PROGRESS - Awaiting blocker resolution


Change Log

Date Version Changes
2026-03-05 1.0 Initial report - Environment setup complete, authentication blocked
TBD 2.0 Post-fix update - Full QA execution results