Patient Self-Service Signup Testing Guide
How to test patient registration, authentication, profile management, and legacy migration in the new Identity-based patient signup system.
Table of Contents
- Prerequisites
- Test File Locations
- Running Automated Tests
- Backend Service Tests
- API Endpoint Testing
- Frontend Testing
- Legacy Migration Testing
- Rails Console Verification
- End-to-End Test Scenarios
- Troubleshooting
Prerequisites
Before testing, ensure:
- PostgreSQL 14+ running with identity, clinical, and shared schemas migrated
- Redis running (for Sidekiq background jobs)
- Backend dependencies installed (
bundle install) - Patient webapp dependencies installed (
cd patient && pnpm install) - Test database migrated (
RAILS_ENV=test bundle exec rake db:migrate)
cd backend
# Verify schemas and tables exist
bundle exec rails runner "
puts 'identity.users: ' + Identity::User.table_exists?.to_s
puts 'identity.devices: ' + Identity::Device.table_exists?.to_s
puts 'identity.sessions: ' + Identity::Session.table_exists?.to_s
puts 'clinical.patients: ' + Clinical::Patient.table_exists?.to_s
puts 'clinical.patients has user_id: ' + Clinical::Patient.column_names.include?('user_id').to_s
"Test File Locations
Backend Tests
| Test File | What It Tests |
|---|---|
domains/clinical/spec/services/patients/find_or_create_service_spec.rb | Patient creation, Identity user linking, backfill |
domains/clinical/spec/services/patients/self_registration_service_spec.rb | Patient record creation when visiting a clinic |
domains/clinical/spec/services/patients/linked_facilities_query_spec.rb | Cross-tenant facility lookup for patients |
domains/clinical/spec/services/patients/longitudinal_record_service_spec.rb | Aggregating patient data across tenants |
domains/identity/spec/concerns/user_sign_up_validations_spec.rb | Duplicate detection (Identity + legacy) |
domains/identity/spec/requests/devices_spec.rb | Push token registration endpoint |
domains/identity/spec/requests/patient_profiles_spec.rb | Patient profile and facility registration endpoints |
Frontend Tests
| File | What It Tests |
|---|---|
patient/src/types/auth.types.ts | Type definitions (compile-time verification) |
patient/src/utils/patient.ts | Patient utility functions |
Running Automated Tests
All Patient-Related Backend Tests
cd backend
# Run all patient service specs
bundle exec rspec domains/clinical/spec/services/patients/
# Run registration validation spec
bundle exec rspec domains/identity/spec/concerns/user_sign_up_validations_spec.rb
# Run devices endpoint spec
bundle exec rspec domains/identity/spec/requests/devices_spec.rb
# Run patient profile and registration endpoint spec
bundle exec rspec domains/identity/spec/requests/patient_profiles_spec.rb
# Run all at once
bundle exec rspec \
domains/clinical/spec/services/patients/ \
domains/identity/spec/concerns/ \
domains/identity/spec/requests/devices_spec.rb \
domains/identity/spec/requests/patient_profiles_spec.rbFrontend TypeScript Check
cd patient
# Verify all types compile cleanly
npx tsc --noEmit
# Run tests (if configured)
pnpm testBackend Service Tests
LinkedFacilitiesQuery
Tests that a patient's linked facilities are returned correctly across tenants.
bundle exec rspec domains/clinical/spec/services/patients/linked_facilities_query_spec.rbKey scenarios:
- Returns facilities where patient has records
- Handles patients with no clinical records
- Respects RLS boundaries (uses
with_rls_context)
FindOrCreateService
Tests central patient creation and Identity user linking.
bundle exec rspec domains/clinical/spec/services/patients/find_or_create_service_spec.rbKey scenarios:
- Finds existing patient by ID or phone number
- Creates new patient with required fields
- Creates PatientFacility link on creation
- Auto-links Identity::User by phone number lookup
- Backfills user_id on existing patients missing it
- Does not overwrite existing user_id
- Accepts explicit identity_user parameter
SelfRegistrationService
Tests patient record creation when visiting a clinic for the first time.
bundle exec rspec domains/clinical/spec/services/patients/self_registration_service_spec.rbKey scenarios:
- Creates Clinical::Patient with user_id link
- Returns existing patient if already registered at that clinic
- Creates PatientFacility link and generates MRN
- Handles multi-facility and multi-tenant visits
- Idempotent on repeat calls
LongitudinalRecordService
Tests aggregation of patient data across multiple tenants.
bundle exec rspec domains/clinical/spec/services/patients/longitudinal_record_service_spec.rbKey scenarios:
- Aggregates encounters, observations, medications across tenants
- Returns empty results for patients with no data
- Handles missing or invalid user IDs
API Endpoint Testing
Manual Testing with cURL
1. Register a New Patient
curl -X POST http://localhost:3000/identity/registrations \
-H "Content-Type: application/json" \
-d '{
"user_sign_up_form": {
"first_name": "Test",
"last_name": "Patient",
"signed_up_as": "patient",
"phone_country_code": "91",
"phone_number": "9999900001",
"password": "TestPass1!"
}
}'Expected: 200 OK with user data. OTP sent to phone (check Rails logs in dev).
2. Verify OTP
# Get the OTP from Rails logs: "Sending verification code to 9999900001"
# Or from rails console: Identity::User.last.verification_code
curl -X POST http://localhost:3000/identity/registrations/verify \
-H "Content-Type: application/json" \
-d '{
"phone_country_code": "91",
"phone_number": "9999900001",
"verification_code": "123456",
"device_details": {
"device_type": "web",
"device_token": "test-token",
"device_uuid": "test-uuid",
"app_type": "patient"
}
}'Expected: 200 OK with access_token, refresh_token, available_profiles.
3. Login
curl -X POST http://localhost:3000/identity/sessions \
-H "Content-Type: application/json" \
-d '{
"user_signin_form": {
"phone_country_code": "91",
"phone_number": "9999900001",
"password": "TestPass1!"
},
"device_details": {
"device_type": "web",
"device_token": "test-token",
"device_uuid": "test-uuid"
}
}'Expected: 200 OK with tokens and available_profiles containing { type: "patient" }.
4. Get Patient Profile
curl http://localhost:3000/identity/patient/profile \
-H "Authorization: Bearer <access_token>"Expected: 200 OK with user details and linked_facilities array.
5. Register at a Facility
# Get a facility_id from rails console:
# Identity::Facility.first.id
curl -X POST http://localhost:3000/identity/patient/register_at_facility \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"facility_id": "<facility_uuid>",
"gender": "female",
"birth_date": "1990-05-15"
}'Expected: 201 Created with patient_id, facility_id, facility_name, and mrn.
Calling again with the same facility_id returns the same patient (idempotent).
6. Register Push Token
curl -X POST http://localhost:3000/identity/devices/push_token \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"device_details": {
"device_type": "ios",
"device_token": "apns-test-token",
"app_type": "patient",
"app_version": "1.0.0",
"device_uuid": "ios-device-uuid"
}
}'Expected: 200 OK with device_id and device_type.
7. Password Reset Flow
# Step 1: Request reset
curl -X POST http://localhost:3000/identity/passwords/reset \
-H "Content-Type: application/json" \
-d '{ "login_id": "9999900001", "country_code": "91" }'
# Step 2: Verify code (get from logs/console)
curl -X POST http://localhost:3000/identity/passwords/verify_code \
-H "Content-Type: application/json" \
-d '{ "phone_country_code": "91", "phone_number": "9999900001", "code": "123456" }'
# -> Returns access_token
# Step 3: Change password using temp token
curl -X POST http://localhost:3000/identity/passwords/change \
-H "Authorization: Bearer <temp_access_token>" \
-H "Content-Type: application/json" \
-d '{ "password": "NewPass1!" }'8. Logout
curl -X DELETE http://localhost:3000/identity/sessions/logout \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"device_details": {
"device_type": "web",
"device_token": "test-token",
"device_uuid": "test-uuid"
}
}'Frontend Testing
TypeScript Compilation
cd patient
npx tsc --noEmitZero errors expected. This verifies all type contracts between the frontend and Identity API.
Manual Browser Testing
- Start the backend:
cd backend && rails s - Start the patient app:
cd patient && pnpm dev - Navigate to
http://localhost:5173(or configured port)
Test Registration Flow
- Go to
/register - Fill in first name, last name, phone, password
- Submit -- should redirect to OTP verification
- Enter OTP (from Rails logs)
- After verification, should redirect to dashboard
- Sidebar should show user's full name and phone
Test Login Flow
- Go to
/login - Enter registered phone and password
- Should redirect to dashboard
- Check localStorage for
auth_tokenandrefresh_token
Test Password Reset
- Go to
/forgot-password - Enter phone number
- Enter verification code
- Set new password
- Login with new password
Verify Patient Debug Info
- Navigate to any page with PatientDebug component
- Verify it shows: User Info, Patient Info, Available Profiles
- Available Profiles should show
type: patientwith linked_facilities
Legacy Migration Testing
Dry Run
cd backend
bundle exec rake patient_migration:dry_runThis shows what would be migrated without making changes. Output format:
DRY RUN - No changes will be made
========================================
Found 42 legacy patient users
CREATE Legacy #123 (91-9876543210) John Doe
SKIP Legacy #456 (91-9876543211) -> Identity abc-123 existsExecute Migration
bundle exec rake patient_migration:migrateOutput:
Starting legacy patient migration...
========================================
Found 42 legacy patient users
OK Legacy #123 -> Identity abc-def-123
OK Legacy #456 -> Identity abc-def-456
========================================
Migration complete!
Total legacy patients: 42
Created in Identity: 40
Already existed: 2
Errors: 0Verify Migration
bundle exec rails runner "
legacy_count = User.where(signed_up_as: 'patient').count
identity_count = Identity::User.where(signed_up_as: 'patient').count
puts \"Legacy patients: #{legacy_count}\"
puts \"Identity patients: #{identity_count}\"
# Check a specific user
legacy = User.where(signed_up_as: 'patient').first
if legacy
identity = Identity::User.by_phone(legacy.country_code, legacy.phone_number)&.first
puts \"Legacy ##{legacy.id} -> Identity #{identity&.id || 'NOT FOUND'}\"
puts \"Password digest matches: #{legacy.password_digest == identity&.password_digest}\"
end
"Test Duplicate Detection
After migration, try registering with an existing patient's phone:
curl -X POST http://localhost:3000/identity/registrations \
-H "Content-Type: application/json" \
-d '{
"user_sign_up_form": {
"first_name": "Duplicate",
"last_name": "Test",
"signed_up_as": "patient",
"phone_country_code": "91",
"phone_number": "<existing_patient_phone>",
"password": "TestPass1!"
}
}'Expected: 422 error with message about existing account.
Rails Console Verification
Check User Records
# Find a patient in Identity
user = Identity::User.find_by(signed_up_as: 'patient')
user.attributes.slice('id', 'first_name', 'last_name', 'phone_number', 'phone_verified')
# Check available profiles
result = Identity::Profiles::GetAvailableProfilesService.call(user: user)
result.data[:available_profiles]
# Check linked facilities
Clinical::Patients::LinkedFacilitiesQuery.call(user.id)Check Sessions and Devices
user = Identity::User.find_by(phone_number: '9999900001')
# Active sessions
Identity::Session.where(user: user).order(created_at: :desc).limit(5).map { |s|
{ id: s.id, status: s.status, last_activity: s.last_activity_at }
}
# Registered devices
Identity::Device.where(user: user).map { |d|
{ id: d.id, type: d.device_type, app_type: d.app_type, token: d.device_token&.first(10) }
}Check Identity-Patient Linking
# Find patients linked to an Identity::User
user = Identity::User.find_by(phone_number: '9999900001')
patients = Clinical::Patient.unscoped.where(user_id: user.id)
patients.map { |p| { id: p.id, tenant_id: p.tenant_id, name: p.display_name, mrn: p.mrn } }
# Find patients missing user_id (candidates for backfill)
Clinical::Patient.unscoped.where(user_id: nil).count
# Manually trigger backfill for a patient
patient = Clinical::Patient.unscoped.find_by(phone_number: '9876543210', user_id: nil)
if patient
user = Identity::User.by_phone(patient.phone_country_code, patient.phone_number)&.first
patient.update_columns(user_id: user.id) if user
endCheck Legacy Migration Status
# Find legacy users not yet migrated
legacy_phones = User.where(signed_up_as: 'patient').pluck(:country_code, :phone_number)
migrated = legacy_phones.count { |cc, phone|
Identity::User.by_phone(cc, phone).exists?
}
puts "#{migrated}/#{legacy_phones.count} migrated"End-to-End Test Scenarios
Scenario 1: New Patient Registration
- Register via API or webapp
- Verify OTP
- Login with credentials
- Check profile endpoint returns user data
- Check available_profiles includes
{ type: 'patient' } - Check no Clinical::Patient record exists yet (patient hasn't visited a clinic)
Scenario 2: Migrated Legacy Patient
- Run migration rake task
- Login with legacy credentials (same phone + password)
- Verify tokens are issued
- Check profile shows correct name and phone
- Check linked_facilities shows existing clinic connections
Scenario 3: Mobile App Login
- Login with
device_detailsincludingdevice_type: 'ios' - Verify device record created in
identity.devices - Register push token via
/identity/devices/push_token - Update push token (same endpoint, new token value)
- Verify device record updated (not duplicated)
Scenario 4: Duplicate Prevention
- Create a legacy User with
signed_up_as: 'patient' - Attempt Identity registration with same phone
- Verify error: "associated with an existing account"
- Run migration to create Identity::User
- Login should now work with existing password
Scenario 5: Patient Registers at Facility
- Register and verify a patient (Scenario 1)
- Call
POST /identity/patient/register_at_facilitywith a validfacility_id - Verify 201 response with
patient_id,mrn,facility_name - Verify
Clinical::Patientcreated withuser_idmatching theIdentity::User - Verify
PatientFacilitylink exists - Call again with same
facility_id-- should return samepatient_id(idempotent) - Call with a different facility in same tenant -- should return same patient, new facility link
- Check profile endpoint now shows the facility in
linked_facilities
Scenario 6: Walk-In Check-In Identity Linking
- Create an
Identity::Userwith phone91-9876500001 - Staff performs walk-in check-in with same phone via
POST /clinical/facilities/:id/queue/check_in - Verify
Clinical::Patientcreated withuser_idmatching theIdentity::User - Verify
PatientFacilitylink exists
Scenario 7: Backfill Identity Link on Existing Patient
- Create a
Clinical::Patientwithoutuser_id(legacy patient) - Create an
Identity::Userwith the same phone number - Look up the patient via
FindOrCreateService(any appointment flow) - Verify
user_idwas backfilled on the existing patient record
Scenario 8: Password Reset
- Register and verify a patient
- Request password reset
- Verify reset code
- Change password using temp access token
- Login with new password
- Verify old password no longer works
Troubleshooting
"Authentication required" on patient endpoints
Patient endpoints use require_authentication! which checks both current_user and current_session. Ensure:
- The JWT is valid and not expired
- A session was created during login (check
identity.sessions) - The Authorization header format is
Bearer <token>
OTP not received
In development, OTPs are logged to Rails console:
Sending verification code to 9999900001Check with:
Identity::User.find_by(phone_number: '9999900001').verification_code"associated with an existing account" error
This means the phone exists in the legacy public.users table. Options:
- Run
rake patient_migration:migrateto create Identity records - The migrated user can then login with their existing password
Push token registration fails
Ensure device_details includes both device_type and device_token:
{
"device_details": {
"device_type": "ios", // required: 'web', 'ios', or 'android'
"device_token": "token", // required: push notification token
"device_uuid": "uuid" // recommended for web devices
}
}TypeScript compilation errors in patient app
cd patient
npx tsc --noEmit 2>&1 | head -20Common causes:
- Missing
available_profilesfield on User type - Using
user.nameinstead ofuser.full_name - Using
user.country_codeinstead ofuser.phone_country_code