# Compliance tests

> The 60 tests your integration must pass before it is deployed.

Each test maps to a rule on this site. Run them automatically in your deployment pipeline, and block the release if any test fails. Fix the code, not the pipeline.

Machine-readable: [tests.json](https://developer.paycaptain.com/tests.json)

## Security and data protection

10 tests.

| ID | Type | Test |
| --- | --- | --- |
| S-01 | Security | API key is loaded from environment variables or a secrets manager and is never hard-coded in source files. |
| S-02 | Security | API key does not appear in any client-side bundle. |
| S-03 | Security | API key does not appear in any log output. |
| S-04 | Security | All PayCaptain API calls are made server-side only. |
| S-05 | Integration | All API communication uses HTTPS. |
| S-06 | E2E | Authentication is required for all routes that display or modify employee data. |
| S-07 | E2E | Role-based access control is enforced. |
| S-08 | E2E | Session expires after the configured inactivity timeout. |
| S-09 | Integration | Login attempts are recorded in the audit log. |
| S-10 | Security | CSRF protection is present on all state-changing endpoints. |

## Employee data

22 tests.

| ID | Type | Test |
| --- | --- | --- |
| E-01 | Unit | Reject an employee record missing any required field. |
| E-02 | Unit | Reject if payBasis is "Salaried" and salary is missing. |
| E-03 | Unit | Accept gender "Male" and "Female". Reject all other values. |
| E-04 | Unit | Accept valid UK postcodes. Reject invalid postcodes. |
| E-05 | Unit | Reject if any address field is empty. |
| E-06 | Unit | Reject if leaveDate is before startDate. |
| E-07 | Unit | Reject if payEffectiveDate is before startDate or after leaveDate. |
| E-08 | Unit | For a new employee, reject if payEffectiveDate does not equal startDate. |
| E-09 | Unit | When salary changes, reject if payEffectiveDate is not updated. |
| E-10 | Unit | When rateOfPay changes, reject if payEffectiveDate is not updated. |
| E-11 | Unit | When payBasis changes, reject if payEffectiveDate is not updated. |
| E-12 | Unit | Reject if two employment records share the same payrollCode. |
| E-13 | Unit | Reject if payrollCode is modified on an existing record. |
| E-14 | Unit | Reject if a re-hired employee reuses a previous payrollCode. |
| E-15 | Unit | Reject if employment date ranges overlap for the same hrEmployeeId. |
| E-16 | Unit | Accept non-overlapping employments for the same person. |
| E-17 | Integration | Do not send an unchanged employee record to the API. |
| E-18 | Integration | Send an employee record when at least one field has changed. |
| E-19 | Unit | Reject records for non-PAYE workers. |
| E-20 | Unit | Reject if payBasis is not exactly "Hourly" or "Salaried". |
| E-21 | Unit | If niNumber is provided, validate the UK National Insurance number format. |
| E-22 | Unit | If starterDeclaration is provided, accept only "A", "B" or "C". |

## Shifts

9 tests.

| ID | Type | Test |
| --- | --- | --- |
| A-01 | Unit | Reject a shift record that has not been approved. |
| A-02 | Integration | Accept a shift record that has been explicitly approved. |
| A-03 | Unit | Reject if type is not one of the allowed values. |
| A-04 | Unit | Reject if hrEmployeeId or payrollCode does not match an active employee. |
| A-05 | Unit | Reject if shiftId is missing or empty. |
| A-06 | Integration | Do not send an unchanged shift record to the API. |
| A-07 | Integration | Setting archived to true triggers a resubmission. |
| A-08 | E2E | A user with the Viewer role cannot approve shift records. |
| A-09 | Unit | Reject if payBasis is not "Hourly" or "Daily". |

## Payments

7 tests.

| ID | Type | Test |
| --- | --- | --- |
| P-01 | Unit | Reject if payCode is missing, empty or not a configured Pay Code Name. |
| P-02 | Unit | Reject if hrEmployeeId or payrollCode does not match an active employee. |
| P-03 | Unit | Reject if paymentId is missing or empty. |
| P-04 | Integration | Do not send an unchanged payment record to the API. |
| P-05 | Integration | Setting archived to true triggers a resubmission. |
| P-06 | Unit | Reject if date is missing or not a valid ISO 8601 date. |
| P-07 | E2E | A user with the Viewer role cannot create, edit or archive pay elements. |

## Integration and resilience

12 tests.

| ID | Type | Test |
| --- | --- | --- |
| I-01 | Integration | The X-API-Key header is present on every request. |
| I-02 | Integration | On HTTP 429, retry with exponential backoff. |
| I-03 | Integration | On HTTP 5xx, retry with exponential backoff. |
| I-04 | Integration | After 5 consecutive failures, halt further attempts and alert. |
| I-05 | Integration | Never exceed 5 requests per second. |
| I-06 | Integration | Never exceed 100 requests per minute. |
| I-07 | Integration | On HTTP 400, do not retry. |
| I-08 | Integration | GET /employees pagination is followed until a 204. |
| I-09 | Integration | GET /payslips pagination is followed until a 204. |
| I-10 | Security | No stack traces or internal errors are exposed to the user. |
| I-11 | Integration | All create, update and archive operations are recorded in the audit log. |
| I-12 | Integration | Never hold more than 3 concurrent connections. |


## Deployment gate

```yaml
name: PayCaptain compliance gate
on: [push, pull_request]

jobs:
  compliance-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:unit -- --tag paycaptain
      - run: npm run test:integration -- --tag paycaptain
      - run: npm run test:security
      - run: npm run test:e2e -- --tag paycaptain

  deploy:
    needs: compliance-tests
    if: success()
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploy only when every compliance test has passed"
```

## Generate the suite with AI

```text
Generate an automated test suite covering every test listed at https://developer.paycaptain.com/testing.md
(S-01 to S-10, E-01 to E-22, A-01 to A-09, P-01 to P-07, I-01 to I-12).
Use [your test framework]. Tag every test with "paycaptain".
Configure the suite as a CI stage that blocks deployment if any test fails.
```
