HIPAA Software Testing Guide: Testing for Healthcare Data Compliance

HIPAA Software Testing Guide: Testing for Healthcare Data Compliance

HIPAA (Health Insurance Portability and Accountability Act) imposes specific technical safeguards on software that handles Protected Health Information (PHI). Testing for HIPAA compliance means verifying these safeguards work as designed — not just documenting that policies exist.

This guide focuses on what to test, how to structure those tests, and how to integrate compliance testing into your development workflow.

Note: HIPAA compliance involves legal, administrative, and technical dimensions. This guide covers the software testing aspects. Consult your legal and compliance team for the full compliance picture.

What Counts as PHI

Protected Health Information includes any individually identifiable health information. In software:

  • Name + any health data
  • Social Security Number
  • Date of birth with health data
  • Geographic data smaller than state level with health data
  • Medical record numbers
  • Health plan beneficiary numbers
  • Diagnoses, treatments, medications
  • Lab results
  • Insurance information

Test data should either use synthetic data or properly de-identified data. Using real patient records in test environments is a HIPAA violation.

Access Control Testing (Technical Safeguard §164.312(a))

HIPAA requires unique user identification and automatic logoff.

// tests/hipaa/access-control.test.js

test('each user has a unique identifier — no shared accounts', async ({ request }) => {
  const users = await request.get('/api/admin/users', {
    headers: { Authorization: `Bearer ${adminToken}` }
  });
  const data = await users.json();
  
  const ids = data.map(u => u.id);
  const uniqueIds = new Set(ids);
  expect(uniqueIds.size).toBe(ids.length);
  
  // No test accounts or shared accounts
  const sharedAccounts = data.filter(u => 
    u.email.includes('shared') || u.email.includes('generic')
  );
  expect(sharedAccounts).toHaveLength(0);
});

test('session expires after inactivity', async ({ page }) => {
  await loginAsUser(page);
  await page.goto('/patient-records');
  
  // Simulate inactivity by waiting (or manipulating session)
  await simulateInactivity(page, 15 * 60 * 1000); // 15 minutes
  
  // Attempt to access PHI
  await page.goto('/patient-records');
  
  // Should be redirected to login
  expect(page.url()).toContain('/login');
});

test('role-based access prevents unauthorized PHI access', async ({ request }) => {
  // Billing staff should not access clinical notes
  const response = await request.get('/api/patients/123/clinical-notes', {
    headers: { Authorization: `Bearer ${billingStaffToken}` }
  });
  expect(response.status()).toBe(403);
  
  // Clinical staff can access clinical notes
  const clinicalResponse = await request.get('/api/patients/123/clinical-notes', {
    headers: { Authorization: `Bearer ${clinicalStaffToken}` }
  });
  expect(clinicalResponse.ok()).toBe(true);
});

test('emergency access override is logged', async ({ request }) => {
  // Emergency access ("break glass") should work but must be logged
  const response = await request.post('/api/emergency-access', {
    headers: { Authorization: `Bearer ${emergencyToken}` },
    data: { patientId: '123', reason: 'Emergency care required' }
  });
  
  expect(response.ok()).toBe(true);
  
  // Verify the override was logged
  const auditLog = await getAuditLog({ action: 'emergency_access', patientId: '123' });
  expect(auditLog).toBeDefined();
  expect(auditLog.userId).toBeDefined();
  expect(auditLog.reason).toBe('Emergency care required');
});

Audit Control Testing (Technical Safeguard §164.312(b))

HIPAA requires audit logs for all PHI access.

test('all PHI access is logged', async ({ request }) => {
  const patientId = 'test-patient-123';
  
  // Clear audit log for this patient (in test environment)
  await clearAuditLog(patientId);
  
  // Access patient record
  await request.get(`/api/patients/${patientId}`, {
    headers: { Authorization: `Bearer ${clinicalToken}` }
  });
  
  // Verify access was logged
  const logs = await getAuditLog({ resourceId: patientId });
  expect(logs).toHaveLength(1);
  expect(logs[0]).toMatchObject({
    action: 'read',
    resource: 'patient',
    resourceId: patientId,
    userId: expect.any(String),
    timestamp: expect.any(String),
    ipAddress: expect.any(String),
  });
});

test('PHI modification is logged with before/after values', async ({ request }) => {
  const patientId = 'test-patient-456';
  
  await request.patch(`/api/patients/${patientId}`, {
    headers: { Authorization: `Bearer ${clinicalToken}` },
    data: { diagnosis: 'Updated diagnosis' }
  });
  
  const logs = await getAuditLog({ resourceId: patientId, action: 'update' });
  expect(logs[0]).toMatchObject({
    action: 'update',
    changes: expect.arrayContaining([
      expect.objectContaining({ field: 'diagnosis' })
    ])
  });
});

test('failed access attempts are logged', async ({ request }) => {
  await request.get('/api/patients/999', {
    headers: { Authorization: `Bearer ${unauthorizedToken}` }
  });
  
  const logs = await getAuditLog({ action: 'access_denied' });
  const recentDenial = logs.find(l => l.resource === 'patient');
  expect(recentDenial).toBeDefined();
  expect(recentDenial.userId).toBeDefined();
});

test('audit logs are tamper-evident', async ({ request }) => {
  // Audit logs should be append-only — no update or delete endpoints
  const response = await request.delete('/api/audit-logs/1', {
    headers: { Authorization: `Bearer ${adminToken}` }
  });
  expect(response.status()).toBe(405); // Method Not Allowed
  
  const patchResponse = await request.patch('/api/audit-logs/1', {
    headers: { Authorization: `Bearer ${adminToken}` },
    data: { action: 'modified_action' }
  });
  expect(patchResponse.status()).toBe(405);
});

Integrity Testing (Technical Safeguard §164.312(c))

PHI must not be altered or destroyed improperly.

test('PHI has cryptographic integrity check', async ({ request }) => {
  const response = await request.get('/api/patients/123/record', {
    headers: { Authorization: `Bearer ${clinicalToken}` }
  });
  
  const data = await response.json();
  
  // Response should include integrity hash
  expect(data.checksum).toBeDefined();
  
  // Verify the checksum matches the data
  const computedChecksum = computeChecksum(data.content);
  expect(data.checksum).toBe(computedChecksum);
});

test('corrupted data is detected', async () => {
  // Store a patient record with a known hash
  const record = await storePatientRecord({ data: 'original data' });
  
  // Simulate corruption in the database
  await corruptRecord(record.id);
  
  // Retrieval should detect corruption and fail
  await expect(retrievePatientRecord(record.id))
    .rejects.toThrow('integrity check failed');
});

Transmission Security Testing (Technical Safeguard §164.312(e))

PHI must be protected during transmission.

test('PHI is only transmitted over HTTPS', async ({ request }) => {
  // HTTP requests to the API should be rejected or redirected
  const httpResponse = await fetch('http://api.example.com/patients/123', {
    headers: { Authorization: `Bearer ${clinicalToken}` }
  });
  
  // Either rejected (400) or redirected to HTTPS
  expect(
    httpResponse.status === 400 ||
    httpResponse.url.startsWith('https://')
  ).toBe(true);
});

test('API does not return PHI in error messages', async ({ request }) => {
  // Invalid request should return generic error, not patient data
  const response = await request.get('/api/patients/invalid-id', {
    headers: { Authorization: `Bearer ${clinicalToken}` }
  });
  
  const error = await response.json();
  expect(error.message).not.toContain('John');
  expect(error.message).not.toContain('diagnosis');
  expect(error.message).not.toContain('SSN');
});

test('response headers do not expose PHI', async ({ request }) => {
  const response = await request.get('/api/patients/123', {
    headers: { Authorization: `Bearer ${clinicalToken}` }
  });
  
  const headers = response.headers();
  
  // No PHI in debug headers or error headers
  Object.entries(headers).forEach(([key, value]) => {
    expect(value).not.toMatch(/\d{3}-\d{2}-\d{4}/); // SSN pattern
    expect(value).not.toMatch(/diagnosis|treatment|medication/i);
  });
});

Encryption at Rest Testing

test('PHI fields are encrypted in database', async () => {
  // Write a patient record
  const patient = await createPatient({
    ssn: '123-45-6789',
    diagnosis: 'Test diagnosis'
  });
  
  // Read directly from database (bypassing application layer)
  const dbRecord = await db.query(
    'SELECT ssn, diagnosis FROM patients WHERE id = $1',
    [patient.id]
  );
  
  // Values should be encrypted (not plaintext)
  expect(dbRecord.ssn).not.toBe('123-45-6789');
  expect(dbRecord.diagnosis).not.toBe('Test diagnosis');
  
  // Should be decryptable through the application
  const fetched = await getPatient(patient.id);
  expect(fetched.ssn).toBe('123-45-6789');
  expect(fetched.diagnosis).toBe('Test diagnosis');
});

Test Data Management

A common HIPAA testing mistake: using real patient data in test environments.

Synthetic data generation:

// test-helpers/generate-phi.js
const { faker } = require('@faker-js/faker');

function generateSyntheticPatient() {
  return {
    id: `TEST-${faker.string.uuid()}`,
    firstName: faker.person.firstName(),
    lastName: faker.person.lastName(),
    dateOfBirth: faker.date.birthdate({ min: 18, max: 90, mode: 'age' }).toISOString(),
    ssn: `TEST-${faker.string.numeric(9)}`, // Prefix makes it obviously fake
    diagnosis: faker.helpers.arrayElement([
      'Hypertension (test)',
      'Type 2 Diabetes (test)',
      'Asthma (test)',
    ]),
    // Mark all test data clearly
    testData: true,
    createdBy: 'test-harness',
  };
}

Synthetic data is functionally equivalent for testing but doesn't create HIPAA liability.

HIPAA Testing Checklist

## Pre-Release HIPAA Technical Safeguards Checklist

### Access Controls
- [ ] Unique user IDs enforced (no shared accounts)
- [ ] Automatic session timeout implemented and tested
- [ ] Role-based access controls tested for each role
- [ ] Emergency access override works and is logged

### Audit Controls
- [ ] All PHI reads are logged with user, timestamp, IP
- [ ] All PHI writes are logged with before/after values
- [ ] Failed access attempts are logged
- [ ] Audit logs are append-only (no modification/deletion)

### Integrity
- [ ] PHI records have integrity verification
- [ ] Corruption is detected on retrieval

### Transmission Security
- [ ] API only accessible over HTTPS
- [ ] HTTP requests rejected or redirected
- [ ] Error messages do not expose PHI
- [ ] Response headers do not contain PHI

### Encryption
- [ ] PHI fields encrypted at rest
- [ ] Encryption keys managed separately from data
- [ ] Decryption only through authorized application layer

### Test Data
- [ ] No real patient data in test environments
- [ ] Synthetic data clearly marked as test data
- [ ] Test data purged from non-production environments on schedule

HIPAA compliance testing doesn't happen once at launch — it should run continuously as the codebase evolves. Any change to data models, API endpoints, access control logic, or audit logging warrants re-running the compliance test suite.

Read more