How to run and write tests for OpenJustice.
OpenJustice has two test suites:
- Server (Jest): 122 unit tests covering auth, encryption, permissions, audit, cases, persons, and evidence
- Client (Vitest): 23 unit tests covering auth context, offline storage, and background sync
Tests are colocated with source files in the server (*.spec.ts) and grouped under lib/__tests__/ in the client.
cd server
npm test # run all tests
npm run test:watch # watch mode
npm run test:cov # with coverage report
npm run test:e2e # e2e tests (requires running database)cd client
npm test # run all tests (vitest run)
npm run test:watch # watch mode (vitest)
npm run test:coverage # with coverageThe server uses Jest 30 with ts-jest. Configuration is in package.json:
{
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": { "^.+\\.(t|j)s$": "ts-jest" },
"collectCoverageFrom": ["**/*.(t|j)s"],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}| File | What it tests |
|---|---|
common/utils/encryption.util.spec.ts |
AES-256-GCM encrypt/decrypt, PII helpers, key validation |
common/utils/hash.util.spec.ts |
Argon2id PIN hashing and verification |
common/utils/crypto.util.spec.ts |
SHA-256, custody signatures, audit entry hashes |
common/guards/permissions.guard.spec.ts |
Scope hierarchy, SuperAdmin bypass, buildScopeFilter |
modules/auth/auth.service.spec.ts |
Login, PIN expiry, account lockout, token refresh, PIN change/reset |
modules/audit/audit.service.spec.ts |
Hash-chain creation, integrity verification, tamper detection |
modules/cases/cases.service.spec.ts |
Case CRUD, status transitions, person roles, notes |
modules/persons/persons.service.spec.ts |
Person CRUD, NIN uniqueness, wanted status, risk levels, aliases |
modules/evidence/evidence.service.spec.ts |
Evidence CRUD, status transitions, custody events, sealing |
Tests use NestJS Test.createTestingModule with mocked dependencies. Here is the pattern used throughout:
import { Test } from '@nestjs/testing';
import { CasesService } from './cases.service';
import { CasesRepository } from './cases.repository';
import { PrismaService } from '../../common/database/prisma.service';
describe('CasesService', () => {
let service: CasesService;
let casesRepository: Record<string, jest.Mock>;
let prisma: Record<string, any>;
beforeEach(async () => {
casesRepository = {
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
// ... mock each repository method used
};
prisma = {
auditLog: { create: jest.fn().mockResolvedValue({}) },
};
const module = await Test.createTestingModule({
providers: [
CasesService,
{ provide: CasesRepository, useValue: casesRepository },
{ provide: PrismaService, useValue: prisma },
],
}).compile();
service = module.get(CasesService);
});
it('throws NotFoundException when case not found', async () => {
casesRepository.findById.mockResolvedValue(null);
await expect(service.findById('missing', 'off-1'))
.rejects.toThrow(NotFoundException);
});
});Key points:
- Mock repositories and PrismaService -- never hit a real database in unit tests
- Mock the AuditService (or prisma.auditLog) so audit writes don't interfere
- Use
jest.fn()for each method, set return values with.mockResolvedValue() - Test both success paths and error/validation paths
The client uses Vitest with jsdom environment and React Testing Library. Configuration is in vitest.config.ts:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
},
resolve: {
alias: { '@': path.resolve(__dirname, '.') },
},
});| File | What it tests |
|---|---|
lib/__tests__/auth-context.test.tsx |
Permission checking, officer data transformation, session refresh |
lib/__tests__/offline-storage.test.ts |
IndexedDB operations, sync queue, entity caching, store name mapping |
lib/__tests__/sync-service.test.ts |
Queue processing, retry with backoff, max retries, offline guard |
Client tests mock browser APIs (fetch, IndexedDB, navigator) and external modules:
import { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
vi.mock('../api-client', () => ({
default: { post: vi.fn() },
setTokens: vi.fn(),
clearTokens: vi.fn(),
}));
const mockFetch = vi.fn();
global.fetch = mockFetch;
it('returns false for hasPermission when user is null', async () => {
mockFetch.mockResolvedValue({ ok: false });
const { result } = renderHook(() => useAuth(), { wrapper });
await vi.waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.hasPermission('cases', 'read')).toBe(false);
});Both test suites run in GitHub Actions CI:
- Server CI (
.github/workflows/ci.yml): runsnpm test -- --coverageafter lint and build - Client CI (
.github/workflows/ci.yml): runsnpm testbefore build
When adding a new server module, create a colocated *.spec.ts file. When adding new client logic under lib/, add a test file in lib/__tests__/.
Coverage targets are not formally enforced yet, but critical modules (auth, encryption, permissions, audit) should maintain test coverage.