Notifications Service — Testing¶
Running Tests¶
Uses Node.js built-in test runner (node:test). No external test framework install required. PostgreSQL and Redis are not needed — all external dependencies are mocked.
The test command runs three test files:
Test Files¶
src/handler.test.js¶
Tests processJob(), parsePayload(), and normalizePayload() from src/handler.js.
| Test | What it checks |
|---|---|
parsePayload: flat XADD fields |
Flat key/value array parses correctly, userId is a number |
parsePayload: payload JSON |
Single payload field containing JSON string is parsed |
normalizePayload: default channel=system |
Missing channel defaults to 'system' |
normalizePayload: missing userId throws |
Throws with /userId/ in error message |
processJob: no tokens → status=no_tokens, ack=true |
When public.device_token returns no rows: log updated with no_tokens, returns { ack: true }, FCM not called |
processJob: invalid-registration-token → DELETE token |
Token with action='remove' triggers DELETE FROM public.device_token |
processJob: all tokens OK → status=sent |
All tokens succeed → UPDATE sets status='sent' as first parameter |
Mocking approach: pool and fcm dependencies are injected as the second argument to processJob(fields, { pool, fcm }). Tests provide inline fake implementations with query routing by SQL prefix.
src/consumer.test.js¶
Tests parseFields() from src/consumer.js.
| Test | What it checks |
|---|---|
parseFields: alternating array → object |
['userId', '1', 'title', 'T'] → { userId: '1', title: 'T' } |
parseFields: empty array → empty object |
Edge case |
src/fcm.test.js¶
Tests classifyError() and stringifyAll() from src/fcm.js. No Firebase SDK calls are made.
| Test | What it checks |
|---|---|
classifyError: invalid-registration-token → remove |
Via errorInfo.code |
classifyError: registration-token-not-registered → remove |
Via err.code |
classifyError: server-unavailable → retry |
Transient errors return retry |
classifyError: third-party-auth-error → fatal |
APNs auth failure returns fatal |
classifyError: unknown → retry |
Safe default for unknown errors |
stringifyAll: all values to string, null/undefined dropped |
{ a: 1, c: null } → { a: '1' } |
Test Design Principles¶
- No real I/O: Redis, PostgreSQL, and FCM are all mocked inline —
npm testworks without any running services - Dependency injection:
processJob(fields, { pool, fcm })accepts mock deps as second argument - SQL routing: fake pool implementations route queries by SQL string prefix (e.g.,
INSERT INTO,SELECT id, "token",UPDATE notifications.notification_log) - FCM isolation:
src/fcm.jsexports pure functions (classifyError,stringifyAll) that do not require Firebase to be initialized
Key Scenarios Verified¶
- Message with no registered device tokens: logged, ACK'd, no FCM call
- Stale token (
invalid-registration-token): deleted frompublic.device_token, message ACK'd - All tokens succeed: log status set to
sent - XAUTOCLAIM retry logic: tested via
parseFields(stream field parsing is the XAUTOCLAIM code path) - Error classification: all documented FCM error codes map to correct actions
SIT / Staging¶
When testing against a real Redis + Firebase stack:
- Publish a test message to the stream:
redis-cli XADD stream.notifications.jobs '*' \
userId 1 \
channel system \
title "Test notification" \
body "Integration test"
- Watch service logs for
notifications.processedevent - Check
notifications.notification_logfor a row with the expecteduser_idandstatus
Device tokens in public.device_token must exist for userId=1 for FCM sends to occur.