Testing
CivicPulse has a two-tier testing strategy: Vitest for the React frontend and Jest + Supertest for the Node.js backend. Both suites run in CI before every deployment.
Frontend Tests (Vitest)β
The frontend uses Vitest with jsdom and React Testing Library.
Run Testsβ
cd frontend
npm test # single run with coverage
npm run test:watch # watch mode (re-runs on save)
npm run test:coverage # detailed coverage report
Test Filesβ
| File | Tests | What it covers |
|---|---|---|
src/__tests__/AdminLogin.test.jsx | 8 | Login form UI, success/error/403 flows, auto-redirect |
src/__tests__/useFlags.test.js | 6 | Flag defaults, localStorage, server merge, fetch failure |
src/__tests__/DonorWall.test.jsx | 5 | Donor data structure, showProfile, image handling |
Setupβ
Vitest is configured in vite.config.js:
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/__tests__/setup.js',
}
setup.js provides:
- Global
fetchmock viavi.fn() sessionStorageandlocalStoragemocks@testing-library/jest-dommatchers
Example β AdminLoginβ
it('shows no-access screen on 403', async () => {
mockApi.login.mockRejectedValue({ status: 403 });
render(<AdminLogin />);
await userEvent.type(screen.getByLabelText(/email/i), 'x@x.com');
await userEvent.type(screen.getByLabelText(/password/i), 'wrong');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(await screen.findByText(/do not have access/i)).toBeInTheDocument();
});
Backend Tests (Jest + Supertest)β
The backend uses Jest with Supertest for HTTP-layer testing. DynamoDB is fully mocked β no real AWS calls are made.
Run Testsβ
cd backend
npm test # single run with coverage
Test Filesβ
| File | Tests | What it covers |
|---|---|---|
tests/admin.auth.test.js | 12 | Login, JWT issuance/verification, password validation, duplicate email |
tests/admin.config.test.js | 8 | Feature flags defaults/merge, donation config filtering |
tests/donors.test.js | 10 | Twitter API fetch, donor CRUD, public list filtering |
tests/issues.resolution.test.js | 26 | Resolve with proof media, community rating, reopen request flow |
tests/xService.test.js | 21 | X_HANDLE config, detectCategory/City, fetchMentions demo/live/error modes |
tests/whatsappService.test.js | 26 | WA_PHONE_NUMBER_ID, detectCategory/City, maskPhone, webhook ingestion, demo/live/fallback |
tests/routes.whatsapp.test.js | 17 | HTTP layer: webhook verification, HMAC signature, message feed, convert auth+validation |
tests/routes.x.test.js | 9 | HTTP layer: mentions feed shape, demo mode, convert auth+validation |
Total: 120 tests across 8 suites β all green.
Test Environment Variablesβ
The test runner needs these env vars set (via shell or .env.test):
ADMIN_JWT_SECRET=test-secret-at-least-32-characters-long
AWS_REGION=us-east-1
# No real AWS credentials needed β DynamoDB is mocked
Mocking DynamoDBβ
Each test file mocks ../config/dynamodb before requiring application code:
jest.mock('../config/dynamodb', () => ({
docClient: { send: jest.fn() },
TABLES: {
ADMINS: 'civicpulse-admins',
SITE_CONFIG: 'civicpulse-site-config',
DONORS: 'civicpulse-donors',
},
}));
Example β Admin Authβ
it('rejects wrong password with 401', async () => {
mockDocClient.send.mockResolvedValueOnce({
Item: { ...adminRecord, passwordHash: await bcrypt.hash('correct', 4) },
});
const res = await request(app)
.post('/api/admin/auth/login')
.send({ email: 'admin@test.com', password: 'wrong-password' });
expect(res.status).toBe(401);
});
Coverage Thresholdsβ
| Layer | Threshold | Notes |
|---|---|---|
| Frontend | β | No hard threshold; tracked in CI output |
| Backend | 10% lines | Minimum gate; new service files should target 80%+ |
Coverage reports are written to:
frontend/coverage/β lcov + htmlbackend/coverage/β lcov + html
CI Integrationβ
Tests run automatically in the test job in .github/workflows/deploy.yml before any deployment:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: cd frontend && npm ci && npm test
- run: cd backend && npm ci && npm test
env:
ADMIN_JWT_SECRET: ${{ secrets.ADMIN_JWT_SECRET }}
AWS_REGION: us-east-1
- run: cd frontend && npm run build # verify build doesn't break
The deploy-backend and deploy-frontend jobs both need: [test] β a failing test blocks deployment.
Adding New Testsβ
Frontendβ
- Create
src/__tests__/MyComponent.test.jsx - Import from
@testing-library/reactandvitest - Mock external modules with
vi.mock('../path/to/module')
Backendβ
- Create
tests/my-service.test.js - Add
jest.mock('../config/dynamodb', ...)at the top - Set required env vars in
beforeAll - Use
supertest(app)for HTTP tests or import service functions directly for unit tests