Kulcare Docs

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

  1. Prerequisites
  2. Test File Locations
  3. Running Automated Tests
  4. Backend Service Tests
  5. API Endpoint Testing
  6. Frontend Testing
  7. Legacy Migration Testing
  8. Rails Console Verification
  9. End-to-End Test Scenarios
  10. 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 FileWhat It Tests
domains/clinical/spec/services/patients/find_or_create_service_spec.rbPatient creation, Identity user linking, backfill
domains/clinical/spec/services/patients/self_registration_service_spec.rbPatient record creation when visiting a clinic
domains/clinical/spec/services/patients/linked_facilities_query_spec.rbCross-tenant facility lookup for patients
domains/clinical/spec/services/patients/longitudinal_record_service_spec.rbAggregating patient data across tenants
domains/identity/spec/concerns/user_sign_up_validations_spec.rbDuplicate detection (Identity + legacy)
domains/identity/spec/requests/devices_spec.rbPush token registration endpoint
domains/identity/spec/requests/patient_profiles_spec.rbPatient profile and facility registration endpoints

Frontend Tests

FileWhat It Tests
patient/src/types/auth.types.tsType definitions (compile-time verification)
patient/src/utils/patient.tsPatient utility functions

Running Automated 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.rb

Frontend TypeScript Check

cd patient

# Verify all types compile cleanly
npx tsc --noEmit

# Run tests (if configured)
pnpm test

Backend 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.rb

Key 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.rb

Key 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.rb

Key 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.rb

Key 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 --noEmit

Zero errors expected. This verifies all type contracts between the frontend and Identity API.

Manual Browser Testing

  1. Start the backend: cd backend && rails s
  2. Start the patient app: cd patient && pnpm dev
  3. Navigate to http://localhost:5173 (or configured port)

Test Registration Flow

  1. Go to /register
  2. Fill in first name, last name, phone, password
  3. Submit -- should redirect to OTP verification
  4. Enter OTP (from Rails logs)
  5. After verification, should redirect to dashboard
  6. Sidebar should show user's full name and phone

Test Login Flow

  1. Go to /login
  2. Enter registered phone and password
  3. Should redirect to dashboard
  4. Check localStorage for auth_token and refresh_token

Test Password Reset

  1. Go to /forgot-password
  2. Enter phone number
  3. Enter verification code
  4. Set new password
  5. Login with new password

Verify Patient Debug Info

  1. Navigate to any page with PatientDebug component
  2. Verify it shows: User Info, Patient Info, Available Profiles
  3. Available Profiles should show type: patient with linked_facilities

Legacy Migration Testing

Dry Run

cd backend
bundle exec rake patient_migration:dry_run

This 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 exists

Execute Migration

bundle exec rake patient_migration:migrate

Output:

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:                0

Verify 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
end

Check 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

  1. Register via API or webapp
  2. Verify OTP
  3. Login with credentials
  4. Check profile endpoint returns user data
  5. Check available_profiles includes { type: 'patient' }
  6. Check no Clinical::Patient record exists yet (patient hasn't visited a clinic)

Scenario 2: Migrated Legacy Patient

  1. Run migration rake task
  2. Login with legacy credentials (same phone + password)
  3. Verify tokens are issued
  4. Check profile shows correct name and phone
  5. Check linked_facilities shows existing clinic connections

Scenario 3: Mobile App Login

  1. Login with device_details including device_type: 'ios'
  2. Verify device record created in identity.devices
  3. Register push token via /identity/devices/push_token
  4. Update push token (same endpoint, new token value)
  5. Verify device record updated (not duplicated)

Scenario 4: Duplicate Prevention

  1. Create a legacy User with signed_up_as: 'patient'
  2. Attempt Identity registration with same phone
  3. Verify error: "associated with an existing account"
  4. Run migration to create Identity::User
  5. Login should now work with existing password

Scenario 5: Patient Registers at Facility

  1. Register and verify a patient (Scenario 1)
  2. Call POST /identity/patient/register_at_facility with a valid facility_id
  3. Verify 201 response with patient_id, mrn, facility_name
  4. Verify Clinical::Patient created with user_id matching the Identity::User
  5. Verify PatientFacility link exists
  6. Call again with same facility_id -- should return same patient_id (idempotent)
  7. Call with a different facility in same tenant -- should return same patient, new facility link
  8. Check profile endpoint now shows the facility in linked_facilities

Scenario 6: Walk-In Check-In Identity Linking

  1. Create an Identity::User with phone 91-9876500001
  2. Staff performs walk-in check-in with same phone via POST /clinical/facilities/:id/queue/check_in
  3. Verify Clinical::Patient created with user_id matching the Identity::User
  4. Verify PatientFacility link exists
  1. Create a Clinical::Patient without user_id (legacy patient)
  2. Create an Identity::User with the same phone number
  3. Look up the patient via FindOrCreateService (any appointment flow)
  4. Verify user_id was backfilled on the existing patient record

Scenario 8: Password Reset

  1. Register and verify a patient
  2. Request password reset
  3. Verify reset code
  4. Change password using temp access token
  5. Login with new password
  6. 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 9999900001

Check 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:

  1. Run rake patient_migration:migrate to create Identity records
  2. 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 -20

Common causes:

  • Missing available_profiles field on User type
  • Using user.name instead of user.full_name
  • Using user.country_code instead of user.phone_country_code

On this page