New Schema Testing Guide
This guide covers how to test the new PostgreSQL multi-schema architecture, including running automated tests, manual API testing, and verifying tenant isolation.
Table of Contents
- Prerequisites
- Database Setup
- Running Automated Tests
- Manual API Testing
- Rails Console Verification
- Tenant Isolation Testing
- Application-Level Tenant Context Testing
- Background Job Testing
- Frontend Manual QA
- Troubleshooting
Prerequisites
Before testing, ensure you have:
- PostgreSQL 14+ running
- Ruby and Bundler installed
- Backend dependencies installed (
bundle install) - Test database created
cd backend
# Install dependencies
bundle install
# Verify PostgreSQL is running
pg_isreadyDatabase Setup
Create Test Database with New Schemas
# Create and migrate test database
RAILS_ENV=test bundle exec rake db:create db:migrate
# Verify schemas were created
RAILS_ENV=test bundle exec rake new_schema:verifySeed Reference Data (Optional but Recommended)
# Seed reference data for meaningful tests
RAILS_ENV=test bundle exec rake new_schema:seed_reference
# Verify seed data
RAILS_ENV=test bundle exec rake new_schema:verify_reference_dataCheck Schema Status
# View migration status for new schema migrations
bundle exec rake db:migrate:status | grep 20251231
# Expected output:
# up 20251231000001 Create new pg schemas
# up 20251231000002 Create tenant context function
# up 20251231000003 Add system schema
# up 20251231000004 Add uuidv7 support
# up 20251231000005 Create reference foundation tables
# up 20251231000006 Create identity foundation tables
# up 20251231000007 Setup identity rlsRunning Automated Tests
Test Locations
domains/
reference/spec/
rails_helper.rb
blueprints/ # Blueprint serialization tests
requests/ # API integration tests (8 controllers)
countries_spec.rb
states_spec.rb
cities_spec.rb
languages_spec.rb
currencies_spec.rb
specialties_spec.rb
facility_types_spec.rb
identifier_systems_spec.rb
identity/spec/
rails_helper.rb
blueprints/ # Blueprint serialization tests
models/ # Model unit tests
requests/ # API integration tests (7 controllers + isolation)
tenants_spec.rb
users_spec.rb
organizations_spec.rb
facilities_spec.rb
practitioners_spec.rb
practitioner_roles_spec.rb
healthcare_services_spec.rb
tenant_isolation_spec.rbRunning All Domain Tests
# Run all Reference domain tests
bundle exec rspec domains/reference/spec/
# Run all Identity domain tests
bundle exec rspec domains/identity/spec/
# Run all domain tests together
bundle exec rspec domains/*/spec/Running Specific Test Types
# Blueprint tests only
bundle exec rspec domains/reference/spec/blueprints/
bundle exec rspec domains/identity/spec/blueprints/
# Request specs (API integration) only
bundle exec rspec domains/reference/spec/requests/
bundle exec rspec domains/identity/spec/requests/
# Model tests only
bundle exec rspec domains/identity/spec/models/Running Individual Test Files
# Single file
bundle exec rspec domains/reference/spec/requests/countries_spec.rb
# With verbose output
bundle exec rspec domains/reference/spec/requests/countries_spec.rb -fd
# Specific test by line number
bundle exec rspec domains/identity/spec/requests/facilities_spec.rb:42Running Tenant Isolation Tests
# Run all RLS verification tests
bundle exec rspec domains/identity/spec/requests/tenant_isolation_spec.rb -fd
# Run specific isolation test
bundle exec rspec domains/identity/spec/requests/tenant_isolation_spec.rb \
-e "Organization Isolation"Test Output Options
# Documentation format (verbose)
bundle exec rspec domains/*/spec/requests/ -fd
# Show only failures
bundle exec rspec domains/*/spec/requests/ --only-failures
# Dry run (list tests without running)
bundle exec rspec domains/*/spec/requests/ --dry-run
# With timing info
bundle exec rspec domains/*/spec/requests/ --profileManual API Testing
Start the Development Server
cd backend
rails serverReference API Endpoints (No Authentication Required)
# List all countries
curl -s http://localhost:3000/reference/countries | jq
# Get a specific country
curl -s http://localhost:3000/reference/countries/<UUID> | jq
# Search countries
curl -s "http://localhost:3000/reference/countries?q=India" | jq
# List states for a country
curl -s "http://localhost:3000/reference/states?country_id=<UUID>" | jq
# List cities with filters
curl -s "http://localhost:3000/reference/cities?tier=metro" | jq
curl -s "http://localhost:3000/reference/cities?state_id=<UUID>" | jq
# List languages
curl -s http://localhost:3000/reference/languages | jq
# List currencies
curl -s http://localhost:3000/reference/currencies | jq
# List specialties with filters
curl -s http://localhost:3000/reference/specialties | jq
curl -s "http://localhost:3000/reference/specialties?major=true" | jq
curl -s "http://localhost:3000/reference/specialties?surgical=true" | jq
# List facility types
curl -s http://localhost:3000/reference/facility_types | jq
# List identifier systems
curl -s "http://localhost:3000/reference/identifier_systems?country_id=<UUID>" | jqExpected Response Format
All endpoints return responses in this format (snake_case keys):
{
"success": true,
"data": {
"id": "019b7d57-2257-7b1e-bf8c-d08ffa8f5081",
"name": "India",
"alpha_2_code": "IN",
"alpha_3_code": "IND",
"phone_code": "+91",
"active": true
}
}For collections with pagination:
{
"success": true,
"data": [...],
"meta": {
"pagination": {
"total_entries": 25,
"total_pages": 3,
"current_page": 1,
"per_page": 10
}
}
}Pagination Parameters
# Page 2 with 5 items per page
curl -s "http://localhost:3000/reference/countries?page=2&per_page=5" | jqIdentity API Endpoints (Requires Authentication)
Identity endpoints require a valid JWT token with tenant context. For manual testing, you'll need to:
- Obtain a JWT token through login
- Include the token in the Authorization header
# Example with JWT token
TOKEN="your-jwt-token-here"
curl -s http://localhost:3000/identity/tenant \
-H "Authorization: Bearer $TOKEN" | jq
curl -s http://localhost:3000/identity/facilities \
-H "Authorization: Bearer $TOKEN" | jq
curl -s http://localhost:3000/identity/practitioners \
-H "Authorization: Bearer $TOKEN" | jqRails Console Verification
Quick Schema Verification
bundle exec rails console# Check if tables exist
puts "Reference Tables:"
puts " Countries: #{Reference::Country.table_exists?}"
puts " States: #{Reference::State.table_exists?}"
puts " Cities: #{Reference::City.table_exists?}"
puts " Languages: #{Reference::Language.table_exists?}"
puts " Currencies: #{Reference::Currency.table_exists?}"
puts " Specialties: #{Reference::Specialty.table_exists?}"
puts " FacilityTypes: #{Reference::FacilityType.table_exists?}"
puts " IdentifierSystems: #{Reference::IdentifierSystem.table_exists?}"
puts "\nIdentity Tables:"
puts " Tenants: #{Identity::Tenant.table_exists?}"
puts " Users: #{Identity::User.table_exists?}"
puts " UserMemberships: #{Identity::UserMembership.table_exists?}"
puts " Organizations: #{Identity::Organization.table_exists?}"
puts " Facilities: #{Identity::Facility.table_exists?}"
puts " Practitioners: #{Identity::Practitioner.table_exists?}"
puts " PractitionerRoles: #{Identity::PractitionerRole.table_exists?}"Test Reference Data
# Count records
Reference::Country.count
Reference::Specialty.count
# Query specific records
Reference::Country.find_by(alpha_2_code: 'IN')
Reference::Specialty.major.active.count
# Test associations
india = Reference::Country.find_by(alpha_2_code: 'IN')
india.states.count
india.states.first.cities.countTest Identity Models
# Create a test tenant
tenant = Identity::Tenant.create!(
name: 'Test Clinic',
slug: 'test-clinic',
status: 'active',
plan_tier: 'professional'
)
# Create an organization
org = Identity::Organization.create!(
tenant: tenant,
name: 'Test Organization',
organization_type: 'clinic_group',
active: true
)
# Create a facility
facility = Identity::Facility.create!(
tenant: tenant,
organization: org,
name: 'Main Clinic',
service_level: 'primary',
has_opd: true,
active: true
)
# Create a practitioner (cross-tenant)
practitioner = Identity::Practitioner.create!(
first_name: 'John',
last_name: 'Doe',
display_name: 'Dr. John Doe',
active: true
)
# Create a practitioner role (tenant-scoped)
role = Identity::PractitionerRole.create!(
tenant: tenant,
practitioner: practitioner,
facility: facility,
role_type: 'physician',
status: 'active'
)
# Verify associations
tenant.organizations.count # => 1
tenant.facilities.count # => 1
facility.practitioner_roles.count # => 1Test Blueprinter Serialization
# Test blueprint rendering
country = Reference::Country.first
puts Reference::CountryBlueprint.render(country, view: :normal)
puts Reference::CountryBlueprint.render(country, view: :detailed)
# Test Identity blueprint
facility = Identity::Facility.first
puts Identity::FacilityBlueprint.render(facility, view: :normal)
puts Identity::FacilityBlueprint.render(facility, view: :detailed)Tenant Isolation Testing
Manual RLS Verification
# In Rails console
# Create two tenants
tenant_a = Identity::Tenant.create!(name: 'Tenant A', slug: 'tenant-a', status: 'active', plan_tier: 'free')
tenant_b = Identity::Tenant.create!(name: 'Tenant B', slug: 'tenant-b', status: 'active', plan_tier: 'free')
# Create organizations in each tenant
org_a = Identity::Organization.create!(tenant: tenant_a, name: 'Org A', organization_type: 'clinic_group')
org_b = Identity::Organization.create!(tenant: tenant_b, name: 'Org B', organization_type: 'clinic_group')
# Without tenant context, both are visible (via ActiveRecord)
Identity::Organization.count # => 2
# Set tenant context for Tenant A
ActiveRecord::Base.connection.execute("SELECT set_tenant_context('#{tenant_a.id}')")
# With RLS enabled, only Tenant A's data should be visible
# (This requires RLS to be enforced on the connection)
# Clear tenant context
ActiveRecord::Base.connection.execute("SELECT clear_tenant_context()")Automated Tenant Isolation Tests
The tenant_isolation_spec.rb file tests these scenarios:
-
Organization Isolation
- Tenant A can only see their organizations
- Tenant A cannot access Tenant B's organizations
- Tenant A cannot modify/delete Tenant B's organizations
-
Facility Isolation
- Facilities are scoped to their tenant
- Cross-tenant facility access returns 404
-
Practitioner Isolation
- Practitioners are visible through their roles in the tenant
- Cross-tenant practitioner access returns 404
-
PractitionerRole Isolation
- Role assignments are tenant-scoped
- Cannot modify roles in other tenants
-
HealthcareService Isolation
- Services are tenant-scoped through facilities
- Cross-tenant service access returns 404
-
Cross-Tenant Creation Prevention
- New resources always belong to current tenant
- Cannot specify a different tenant_id
-
Data Integrity After Tenant Switch
- Switching tenant context shows different data
- No data leakage between tenants
Run these tests:
bundle exec rspec domains/identity/spec/requests/tenant_isolation_spec.rb -fdApplication-Level Tenant Context Testing
This section covers testing the application-level tenant context system (Shared::Current) and TenantScoped model behavior.
Testing with Shared::Current
The Shared::Current class provides request-scoped storage. In tests, you need to set this up manually.
RSpec Support Helper
Create a support file for tenant context helpers:
# spec/support/tenant_context.rb
module TenantContextHelpers
# Set up tenant context for tests
def set_tenant_context(tenant)
Shared::Current.tenant = tenant
end
# Set up full request context (tenant + user + membership)
def set_full_context(tenant:, user:, membership: nil)
Shared::Current.tenant = tenant
Shared::Current.user = user
Shared::Current.membership = membership || user.membership_for(tenant)
end
# Clear tenant context
def clear_tenant_context
Shared::Current.reset
# Also clear RLS context
ActiveRecord::Base.connection.execute(
"SELECT set_config('app.current_tenant_id', '', true)"
)
end
end
RSpec.configure do |config|
config.include TenantContextHelpers
# Clear context after each test
config.after(:each) do
clear_tenant_context
end
endUnit Test Examples
# spec/domains/identity/models/organization_spec.rb
require 'rails_helper'
RSpec.describe Identity::Organization do
let(:tenant) { create(:identity_tenant) }
let(:other_tenant) { create(:identity_tenant) }
before { set_tenant_context(tenant) }
describe 'TenantScoped concern' do
it 'auto-assigns tenant_id on creation' do
org = Identity::Organization.create!(
name: 'Test Org',
organization_type: 'clinic_group'
# Note: tenant_id is NOT specified
)
expect(org.tenant_id).to eq(tenant.id)
end
it 'validates tenant_id presence' do
Shared::Current.reset # Clear context
org = Identity::Organization.new(
name: 'Test Org',
organization_type: 'clinic_group'
)
expect(org).not_to be_valid
expect(org.errors[:tenant_id]).to include("can't be blank")
end
it 'prevents tenant_id modification' do
org = create(:identity_organization, tenant: tenant)
expect {
org.update!(tenant_id: other_tenant.id)
}.to raise_error(ActiveRecord::RecordInvalid, /Tenant cannot be changed/)
end
end
describe 'default_scope filtering' do
let!(:org_a) { create(:identity_organization, tenant: tenant) }
let!(:org_b) { create(:identity_organization, tenant: other_tenant) }
it 'only returns records for current tenant' do
expect(Identity::Organization.all).to include(org_a)
expect(Identity::Organization.all).not_to include(org_b)
end
it 'can bypass default_scope with unscoped_by_tenant' do
all_orgs = Identity::Organization.unscoped_by_tenant.all
expect(all_orgs).to include(org_a, org_b)
end
it 'can query specific tenant with for_tenant' do
other_orgs = Identity::Organization.for_tenant(other_tenant.id).all
expect(other_orgs).to include(org_b)
expect(other_orgs).not_to include(org_a)
end
end
endTesting Current User and Permissions
# spec/domains/identity/models/user_membership_spec.rb
RSpec.describe Identity::UserMembership do
let(:tenant) { create(:identity_tenant) }
let(:user) { create(:identity_user) }
describe 'Shared::Current helpers' do
it 'exposes permission checks' do
membership = create(:identity_user_membership,
tenant: tenant,
user: user,
role: 'admin'
)
set_full_context(tenant: tenant, user: user, membership: membership)
expect(Shared::Current.admin?).to be true
expect(Shared::Current.owner?).to be false
expect(Shared::Current.staff?).to be true
end
it 'checks specific permissions' do
membership = create(:identity_user_membership,
tenant: tenant,
user: user,
role: 'staff'
)
set_full_context(tenant: tenant, user: user, membership: membership)
expect(Shared::Current.membership.has_permission?('view_patients')).to be true
expect(Shared::Current.membership.has_permission?('manage_users')).to be false
end
end
endRequest Spec with Tenant Context
# spec/domains/identity/requests/organizations_spec.rb
require 'rails_helper'
RSpec.describe 'Organizations API', type: :request do
let(:tenant) { create(:identity_tenant) }
let(:user) { create(:identity_user) }
let(:membership) { create(:identity_user_membership, tenant: tenant, user: user, role: 'admin') }
# Mock JWT authentication
let(:auth_headers) do
token = generate_jwt_token(user: user, tenant: tenant)
{ 'Authorization' => "Bearer #{token}" }
end
describe 'GET /identity/organizations' do
let!(:my_org) { create(:identity_organization, tenant: tenant) }
let!(:other_org) { create(:identity_organization) } # Different tenant
it 'only returns organizations for authenticated tenant' do
get '/identity/organizations', headers: auth_headers
expect(response).to have_http_status(:ok)
data = JSON.parse(response.body)['data']
ids = data.map { |o| o['id'] }
expect(ids).to include(my_org.id)
expect(ids).not_to include(other_org.id)
end
end
describe 'POST /identity/organizations' do
it 'auto-assigns tenant_id from context' do
post '/identity/organizations',
params: { organization: { name: 'New Org', organization_type: 'clinic_group' } },
headers: auth_headers
expect(response).to have_http_status(:created)
org = Identity::Organization.last
expect(org.tenant_id).to eq(tenant.id)
end
end
endTesting RLS Context Setting
# spec/domains/shared/current_spec.rb
require 'rails_helper'
RSpec.describe Shared::Current do
let(:tenant) { create(:identity_tenant) }
describe '#tenant=' do
it 'sets RLS context when tenant is assigned' do
Shared::Current.tenant = tenant
result = ActiveRecord::Base.connection.execute(
"SELECT current_setting('app.current_tenant_id', true)"
)
expect(result.first['current_setting']).to eq(tenant.id)
end
it 'clears RLS context when tenant is nil' do
Shared::Current.tenant = tenant
Shared::Current.tenant = nil
result = ActiveRecord::Base.connection.execute(
"SELECT current_setting('app.current_tenant_id', true)"
)
expect(result.first['current_setting']).to be_empty
end
end
endBackground Job Testing
This section covers testing Sidekiq jobs that require tenant context.
Testing Jobs with Automatic Context
The Sidekiq middleware automatically captures and restores tenant context:
# spec/jobs/send_notification_job_spec.rb
require 'rails_helper'
RSpec.describe SendNotificationJob, type: :job do
include ActiveJob::TestHelper
let(:tenant) { create(:identity_tenant) }
let(:user) { create(:identity_user) }
let(:notification) { create(:notification, tenant: tenant) }
before { set_tenant_context(tenant) }
it 'executes within tenant context' do
# Enqueue from within tenant context
expect {
SendNotificationJob.perform_later(notification.id)
}.to have_enqueued_job(SendNotificationJob)
# Verify job payload includes tenant_id
job = enqueued_jobs.last
expect(job['tenant_id']).to eq(tenant.id)
end
it 'restores tenant context when performed' do
perform_enqueued_jobs do
SendNotificationJob.perform_later(notification.id)
end
# Verify the job executed with correct tenant
# (Add assertions based on what your job does)
end
endTesting TenantAwareJob Concern
For jobs that use the TenantAwareJob concern for explicit tenant control:
# spec/jobs/cross_tenant_report_job_spec.rb
require 'rails_helper'
RSpec.describe CrossTenantReportJob, type: :job do
include ActiveJob::TestHelper
let(:tenant_a) { create(:identity_tenant) }
let(:tenant_b) { create(:identity_tenant) }
describe '.perform_for_tenant' do
it 'enqueues with explicit tenant context' do
# No current tenant context set
CrossTenantReportJob.perform_for_tenant(tenant_a, report_params)
job = enqueued_jobs.last
expect(job['tenant_id']).to eq(tenant_a.id)
end
end
describe '#with_tenant' do
it 'switches tenant context within job' do
job = CrossTenantReportJob.new
# Verify tenant A context
job.send(:with_tenant, tenant_a) do
expect(Shared::Current.tenant).to eq(tenant_a)
expect(Identity::Organization.all.to_sql).to include(tenant_a.id)
end
# Verify tenant B context
job.send(:with_tenant, tenant_b) do
expect(Shared::Current.tenant).to eq(tenant_b)
expect(Identity::Organization.all.to_sql).to include(tenant_b.id)
end
end
it 'restores previous context after block' do
set_tenant_context(tenant_a)
job = CrossTenantReportJob.new
job.send(:with_tenant, tenant_b) do
# Inside block, tenant_b is active
expect(Shared::Current.tenant).to eq(tenant_b)
end
# After block, tenant_a is restored
expect(Shared::Current.tenant).to eq(tenant_a)
end
end
describe '#each_tenant' do
let!(:org_a) { create(:identity_organization, tenant: tenant_a) }
let!(:org_b) { create(:identity_organization, tenant: tenant_b) }
it 'iterates through all tenants with correct context' do
job = CrossTenantReportJob.new
processed_tenants = []
job.send(:each_tenant) do |tenant|
processed_tenants << tenant
# Verify correct organization is visible
orgs = Identity::Organization.all.to_a
expect(orgs.map(&:tenant_id).uniq).to eq([tenant.id])
end
expect(processed_tenants).to include(tenant_a, tenant_b)
end
it 'accepts a scope to limit tenants' do
active_tenants = Identity::Tenant.where(status: 'active')
job = CrossTenantReportJob.new
processed = []
job.send(:each_tenant, active_tenants) do |tenant|
processed << tenant
end
# Only active tenants are processed
expect(processed.map(&:status).uniq).to eq(['active'])
end
end
describe '#require_tenant_context!' do
it 'raises error when no tenant context' do
clear_tenant_context
job = CrossTenantReportJob.new
expect {
job.send(:require_tenant_context!)
}.to raise_error(Shared::Errors::TenantRequiredError)
end
it 'passes when tenant context is set' do
set_tenant_context(tenant_a)
job = CrossTenantReportJob.new
expect { job.send(:require_tenant_context!) }.not_to raise_error
end
end
endTesting Sidekiq Middleware Directly
# spec/middleware/sidekiq_tenant_middleware_spec.rb
require 'rails_helper'
RSpec.describe 'Sidekiq Tenant Middleware' do
let(:tenant) { create(:identity_tenant) }
let(:user) { create(:identity_user) }
describe Sidekiq::Middleware::TenantContextClient do
let(:middleware) { Sidekiq::Middleware::TenantContextClient.new }
it 'captures tenant_id in job payload' do
set_full_context(tenant: tenant, user: user)
job = {}
middleware.call('SomeWorker', job, 'default', nil) { }
expect(job['tenant_id']).to eq(tenant.id)
expect(job['user_id']).to eq(user.id)
end
it 'handles nil tenant gracefully' do
clear_tenant_context
job = {}
middleware.call('SomeWorker', job, 'default', nil) { }
expect(job['tenant_id']).to be_nil
expect(job['user_id']).to be_nil
end
end
describe Sidekiq::Middleware::TenantContextServer do
let(:middleware) { Sidekiq::Middleware::TenantContextServer.new }
it 'restores tenant context from job payload' do
job = {
'tenant_id' => tenant.id,
'user_id' => user.id
}
middleware.call(nil, job, 'default') do
expect(Shared::Current.tenant).to eq(tenant)
expect(Shared::Current.user).to eq(user)
end
end
it 'clears context after job completes' do
job = { 'tenant_id' => tenant.id }
middleware.call(nil, job, 'default') { }
expect(Shared::Current.tenant).to be_nil
result = ActiveRecord::Base.connection.execute(
"SELECT current_setting('app.current_tenant_id', true)"
)
expect(result.first['current_setting']).to be_empty
end
it 'clears context even if job raises' do
job = { 'tenant_id' => tenant.id }
expect {
middleware.call(nil, job, 'default') { raise StandardError }
}.to raise_error(StandardError)
expect(Shared::Current.tenant).to be_nil
end
end
endIntegration Test for Full Job Lifecycle
# spec/jobs/integration/tenant_job_lifecycle_spec.rb
require 'rails_helper'
require 'sidekiq/testing'
RSpec.describe 'Tenant Job Lifecycle', type: :job do
let(:tenant) { create(:identity_tenant) }
let!(:org) { create(:identity_organization, tenant: tenant) }
around do |example|
Sidekiq::Testing.inline! { example.run }
end
it 'maintains tenant context through full job lifecycle' do
# Set up tenant context (simulating a request)
set_tenant_context(tenant)
# Enqueue a job that queries tenant-scoped data
result = nil
TestTenantJob.perform_later do
result = Identity::Organization.all.to_a
end
# Verify the job saw the correct data
expect(result).to eq([org])
end
end
# Test job class
class TestTenantJob < ApplicationJob
def perform(&block)
block.call if block_given?
end
endFactory Setup for Tenant Testing
# spec/factories/identity_tenants.rb
FactoryBot.define do
factory :identity_tenant, class: 'Identity::Tenant' do
sequence(:name) { |n| "Tenant #{n}" }
sequence(:slug) { |n| "tenant-#{n}" }
status { 'active' }
plan_tier { 'professional' }
end
factory :identity_user, class: 'Identity::User' do
sequence(:email) { |n| "user#{n}@example.com" }
sequence(:phone_number) { |n| "+1555000#{n.to_s.rjust(4, '0')}" }
primary_identifier_type { 'email' }
status { 'active' }
end
factory :identity_user_membership, class: 'Identity::UserMembership' do
association :tenant, factory: :identity_tenant
association :user, factory: :identity_user
role { 'staff' }
status { 'active' }
end
factory :identity_organization, class: 'Identity::Organization' do
association :tenant, factory: :identity_tenant
sequence(:name) { |n| "Organization #{n}" }
organization_type { 'clinic_group' }
active { true }
end
endFrontend Manual QA
This section covers manual testing for the CMS frontend encounter module, particularly verifying that the query gating optimization works correctly.
Encounter Query Gating Verification
The useEncounterClinicalData hook gates specialty-specific API calls based on which tabs are active for the current practitioner type. This prevents unnecessary API calls (e.g., a general doctor encounter should not fire dental, cardiology, or pediatric queries).
What to Test
Open encounters for different practitioner types and verify that only relevant API calls fire in the browser Network tab.
Setup
- Start the backend:
cd backend && rails server - Start the frontend:
cd frontend && pnpm dev:cms - Open Chrome DevTools > Network tab
- Filter by
observationsandservice_requeststo isolate clinical API calls
Test Cases
General Doctor (expect ~6 API calls)
- Open an encounter for a general practitioner
- In the Network tab, verify only these queries fire:
observations?encounter_id=...&vital_signs=true(vitals)observations?encounter_id=...&category=exam(notes)medication_requests?encounter_id=...(medications)service_requests?encounter_id=...(investigations)observations?encounter_id=...&category=advice(advice)observations?encounter_id=...&category=followup(follow-up)
- Verify NO specialty queries fire (no
dental_examination,cardiac_examination,pediatric_examination, etc.)
Dentist (expect ~8 API calls)
- Open an encounter for a dentist
- Verify the 6 core queries fire (same as general doctor)
- Verify dental-specific queries fire:
observations?encounter_id=...&category=dental_examinationservice_requests?encounter_id=...&category=dental_procedure
- Verify NO non-dental specialty queries fire
Pediatrician (expect ~10 API calls)
- Open an encounter for a pediatrician
- Verify the 6 core queries fire
- Verify pediatric-specific queries fire:
observations?encounter_id=...&category=pediatric_examinationobservations?encounter_id=...&category=immunization_recordobservations?encounter_id=...&category=growth_assessmentservice_requests?encounter_id=...&category=pediatric_procedure
- Verify NO non-pediatric specialty queries fire
Cardiologist (expect ~10 API calls)
- Open an encounter for a cardiologist
- Verify the 6 core queries fire
- Verify cardiology-specific queries fire:
observations?encounter_id=...&category=cardiac_examinationobservations?encounter_id=...&category=ecg_interpretationobservations?encounter_id=...&category=cardiac_risk_assessmentservice_requests?encounter_id=...&category=cardiac_procedure
- Verify NO non-cardiology specialty queries fire
Expected Behavior Summary
| Practitioner Type | Expected API Calls | Should NOT Fire |
|---|---|---|
| General Doctor | ~6 (core only) | All specialty queries |
| Dentist | ~8 (core + dental) | All non-dental specialties |
| Physiotherapist | ~10 (core + physio + history) | All non-physio specialties |
| Dermatologist | ~9 (core + derm) | All non-derm specialties |
| Cardiologist | ~10 (core + cardiac) | All non-cardiac specialties |
| Pediatrician | ~10 (core + pediatric) | All non-pediatric specialties |
| Gynecologist | ~9 (core + gynec) | All non-gynec specialties |
Regression Checks
After verifying reduced API calls, confirm that:
- Data loads correctly: Switch between tabs within the encounter -- data should display properly on each tab
- Saving works: Make changes on a specialty tab and save -- verify the save completes without errors
- No missing data: Open an existing encounter with previously saved specialty data -- verify it loads correctly
- Tab switching: Navigate between tabs and confirm no additional unexpected API calls fire
- TypeScript: Run
pnpm typecheck --filter=@kulcare/cmsto confirm no type errors
Troubleshooting
Tests Fail with 500 Errors
This usually means the database schemas don't exist:
# Check if migrations have run
bundle exec rake db:migrate:status | grep 20251231
# Run migrations for test environment
RAILS_ENV=test bundle exec rake db:migrate
# Or reset test database completely
RAILS_ENV=test bundle exec rake db:drop db:create db:migrateTests Skip with "Schema not available"
This is expected behavior when schemas don't exist. The specs include guards:
before do
skip 'Identity schema not available' unless Identity::Tenant.table_exists?
endTo fix, run migrations as shown above.
"Table does not exist" Errors
# Verify table exists
bundle exec rails runner "puts Identity::Tenant.table_exists?"
# Check schema search path
bundle exec rails runner "puts ActiveRecord::Base.connection.schema_search_path"UUIDv7 Function Not Found
# Verify UUIDv7 function exists
bundle exec rails runner "
result = ActiveRecord::Base.connection.execute('SELECT generate_uuidv7()')
puts result.first['generate_uuidv7']
"If it fails, run the UUIDv7 migration:
bundle exec rake db:migrate VERSION=20251231000004RLS Not Working
# Verify RLS is enabled on a table
bundle exec rails runner "
sql = \"SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE relname = 'facilities'
AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'identity')\"
result = ActiveRecord::Base.connection.execute(sql)
puts result.to_a
"Tenant Context Not Set
# In Rails console, check current tenant context
result = ActiveRecord::Base.connection.execute("SELECT current_tenant_id()")
puts result.first['current_tenant_id']
# Set tenant context
ActiveRecord::Base.connection.execute("SELECT set_tenant_context('your-tenant-uuid')")
# Clear tenant context
ActiveRecord::Base.connection.execute("SELECT clear_tenant_context()")Blueprint snake_case Not Working
Verify the transformer is configured:
# Check Blueprinter configuration
puts Blueprinter.configuration.inspect
# Test transformation manually
country = Reference::Country.first
json = Reference::CountryBlueprint.render_as_hash(country, view: :normal)
puts json.keys # Should be snake_case: ["id", "name", "alpha_2_code", ...]Shared::Current Not Set in Tests
If Shared::Current.tenant is nil in tests:
# Verify the support helper is loaded
# spec/support/tenant_context.rb should be required
# In spec/rails_helper.rb or spec/spec_helper.rb:
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
# Or explicitly set context in test:
before do
Shared::Current.tenant = create(:identity_tenant)
end
# Always clear in after hook to prevent leakage:
after do
Shared::Current.reset
endTenantScoped Model Returns Empty Results
If tenant-scoped queries return empty when data exists:
# Check if tenant context is set
puts Shared::Current.tenant.inspect # Should show tenant, not nil
# Check the RLS context
result = ActiveRecord::Base.connection.execute(
"SELECT current_setting('app.current_tenant_id', true)"
)
puts result.first['current_setting'] # Should be tenant UUID
# If empty, set it manually:
set_tenant_context(tenant)
# Or bypass default_scope to verify data exists:
Identity::Organization.unscoped_by_tenant.countSidekiq Jobs Missing Tenant Context
If background jobs fail with tenant-related errors:
# Verify Sidekiq middleware is loaded
# Check config/initializers/sidekiq_tenant_middleware.rb exists
# In development, check job payload:
Sidekiq::Testing.fake!
MyJob.perform_later(args)
job = Sidekiq::Worker.jobs.last
puts job['tenant_id'] # Should have tenant UUID
# If nil, ensure context is set when enqueueing:
Shared::Current.tenant = tenant
MyJob.perform_later(args)
# Or use TenantAwareJob for explicit context:
MyJob.perform_for_tenant(tenant, args)Context Leaking Between Tests
If tests affect each other due to context not being cleared:
# Ensure after hook clears context
RSpec.configure do |config|
config.after(:each) do
Shared::Current.reset
ActiveRecord::Base.connection.execute(
"SELECT set_config('app.current_tenant_id', '', true)"
)
end
end
# Use database cleaner with transaction strategy:
RSpec.configure do |config|
config.use_transactional_fixtures = true
endTenantRequiredError in Jobs
If jobs raise TenantRequiredError:
# Option 1: Ensure job is enqueued with context
Shared::Current.tenant = tenant
MyJob.perform_later(args)
# Option 2: Use TenantAwareJob concern
class MyJob < ApplicationJob
include Shared::Concerns::TenantAwareJob
def perform(tenant_id, args)
with_tenant(tenant_id) do
# Job logic here
end
end
end
# Option 3: Make job context-optional
def perform(args)
return unless tenant_context? # Skip if no context
# Job logic
endQuick Reference
Rake Tasks
bundle exec rake new_schema:verify # Verify all schemas
bundle exec rake new_schema:verify_schemas # Verify schema existence
bundle exec rake new_schema:verify_functions # Verify tenant context functions
bundle exec rake new_schema:verify_rls # Verify RLS policies
bundle exec rake new_schema:seed_reference # Seed reference data
bundle exec rake new_schema:verify_reference_data # Verify seed data counts
bundle exec rake new_schema:status # Show current statusTest Commands
# All tests
bundle exec rspec domains/*/spec/
# Reference only
bundle exec rspec domains/reference/spec/
# Identity only
bundle exec rspec domains/identity/spec/
# Request specs only
bundle exec rspec domains/*/spec/requests/
# Model specs only
bundle exec rspec domains/*/spec/models/
# Tenant isolation only
bundle exec rspec domains/identity/spec/requests/tenant_isolation_spec.rb
# Tenant context tests
bundle exec rspec spec/domains/shared/current_spec.rb
bundle exec rspec spec/middleware/
# Background job tests
bundle exec rspec spec/jobs/
# Verbose output
bundle exec rspec domains/*/spec/requests/ -fdTenant Context Helpers
# Set tenant context
set_tenant_context(tenant)
# Set full context (tenant + user + membership)
set_full_context(tenant: tenant, user: user, membership: membership)
# Clear context
clear_tenant_context
# Bypass default scope
Identity::Organization.unscoped_by_tenant.all
# Query specific tenant
Identity::Organization.for_tenant(tenant_id).allAPI Endpoints Quick Reference
| Domain | Endpoint | Auth Required |
|---|---|---|
| Reference | /reference/countries | No |
| Reference | /reference/states | No |
| Reference | /reference/cities | No |
| Reference | /reference/languages | No |
| Reference | /reference/currencies | No |
| Reference | /reference/specialties | No |
| Reference | /reference/facility_types | No |
| Reference | /reference/identifier_systems | No |
| Identity | /identity/tenant | Yes |
| Identity | /identity/tenants | Yes (Admin) |
| Identity | /identity/users | Yes |
| Identity | /identity/organizations | Yes |
| Identity | /identity/facilities | Yes |
| Identity | /identity/practitioners | Yes |
| Identity | /identity/practitioner_roles | Yes |
| Identity | /identity/healthcare_services | Yes |