Files
ridebus-admin/backend/test/alertNotificationService.test.js
Zakhar Timoshenko ddd67f438b feat: Implement alert creation service with FCM notifications
- Add AlertCreationService to handle alert creation and notifications.
- Implement idempotency key support to prevent duplicate alerts.
- Create alert notification service for sending FCM messages.
- Add tests for alert creation and notification services.
- Document FCM notification flow and alert creation process.
2026-07-11 01:18:18 +03:00

244 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const assert = require('node:assert/strict');
const test = require('node:test');
const {
ALERT_TITLE_FALLBACKS,
AlertNotificationService,
buildAlertCreatedMessage
} = require('../src/services/alertNotificationService');
const { createFirebaseMessaging, getFirebaseConfig } = require('../src/config/firebase');
function createLogger() {
const errors = [];
return {
errors,
error(message, metadata) {
errors.push({ message, metadata });
}
};
}
test('sends an exact alert-created notification and string-only data payload', async () => {
const messages = [];
const service = new AlertNotificationService({
enabled: true,
topic: 'route_alerts',
androidChannelId: 'route_alerts',
messaging: {
async send(message) {
messages.push(message);
return 'projects/test/messages/42';
}
},
logger: createLogger()
});
const result = await service.sendAlertCreated({
id: 42,
route_id: 7,
route_number: 12,
alert_type: 'delay',
title: 'Автобус задерживается',
message: 'Задержка около 15 минут',
start_time: new Date('2026-07-11T08:30:00.000Z'),
end_time: '2026-07-11T09:30:00Z',
is_active: true
});
assert.deepEqual(result, {
sent: true,
messageId: 'projects/test/messages/42'
});
assert.equal(messages.length, 1);
assert.deepEqual(messages[0], {
topic: 'route_alerts',
notification: {
title: 'Автобус задерживается',
body: 'Задержка около 15 минут'
},
data: {
event: 'alert_created',
alertId: '42',
type: 'delay',
routeId: '7',
routeNumber: '12',
startTime: '2026-07-11T08:30:00.000Z',
endTime: '2026-07-11T09:30:00.000Z'
},
android: {
priority: 'high',
notification: {
channelId: 'route_alerts',
tag: 'alert_42'
}
}
});
assert.ok(Object.values(messages[0].data).every((value) => typeof value === 'string'));
});
test('serializes a null route id and missing optional values as empty strings', () => {
const message = buildAlertCreatedMessage({
id: 9,
route_id: null,
route_number: null,
alert_type: 'info',
title: 'Новость',
message: 'Важная информация',
start_time: null,
end_time: null
});
assert.equal(message.data.routeId, '');
assert.equal(message.data.routeNumber, '');
assert.equal(message.data.startTime, '');
assert.equal(message.data.endTime, '');
});
test('does not initialize messaging or send for an inactive alert', async () => {
let messagingInitializations = 0;
const service = new AlertNotificationService({
enabled: true,
getMessaging() {
messagingInitializations += 1;
return { send: async () => 'unexpected' };
},
logger: createLogger()
});
const result = await service.sendAlertCreated({ id: 10, is_active: false });
assert.deepEqual(result, { sent: false, skipped: true, reason: 'inactive' });
assert.equal(messagingInitializations, 0);
});
test('disabled mode works without initializing Firebase or credentials', async () => {
let messagingInitializations = 0;
const service = new AlertNotificationService({
enabled: false,
getMessaging() {
messagingInitializations += 1;
throw new Error('credentials should not be read');
},
logger: createLogger()
});
const result = await service.sendAlertCreated({ id: 11, is_active: true });
assert.deepEqual(result, { sent: false, skipped: true, reason: 'disabled' });
assert.equal(messagingInitializations, 0);
});
test('logs Firebase failures with safe structured metadata and does not throw', async () => {
const testLogger = createLogger();
const firebaseError = Object.assign(new Error('Permission denied'), {
code: 'messaging/authentication-error'
});
const service = new AlertNotificationService({
enabled: true,
messaging: {
async send() {
throw firebaseError;
}
},
logger: testLogger
});
const result = await service.sendAlertCreated({
id: 57,
alert_type: 'detour',
message: 'Объезд',
is_active: true
});
assert.deepEqual(result, {
sent: false,
reason: 'fcm_error',
firebaseCode: 'messaging/authentication-error'
});
assert.deepEqual(testLogger.errors, [{
message: 'Failed to send alert-created FCM notification',
metadata: {
alertId: '57',
reason: 'Permission denied',
firebaseCode: 'messaging/authentication-error'
}
}]);
});
test('logs lazy Firebase initialization failures and does not throw', async () => {
const testLogger = createLogger();
const initializationError = Object.assign(new Error('Application default credentials unavailable'), {
code: 'app/invalid-credential'
});
const service = new AlertNotificationService({
enabled: true,
getMessaging() {
throw initializationError;
},
logger: testLogger
});
const result = await service.sendAlertCreated({
id: 58,
alert_type: 'info',
message: 'Новость',
is_active: true
});
assert.deepEqual(result, {
sent: false,
reason: 'fcm_error',
firebaseCode: 'app/invalid-credential'
});
assert.deepEqual(testLogger.errors[0].metadata, {
alertId: '58',
reason: 'Application default credentials unavailable',
firebaseCode: 'app/invalid-credential'
});
});
test('uses the requested Russian title fallbacks and a route-aware body fallback', () => {
for (const [type, expectedTitle] of Object.entries(ALERT_TITLE_FALLBACKS)) {
const message = buildAlertCreatedMessage({
id: 71,
alert_type: type,
title: ' ',
message: '',
route_number: '5'
});
assert.equal(message.notification.title, expectedTitle);
assert.equal(message.notification.body, 'Актуальная информация по маршруту №5');
}
const withoutRoute = buildAlertCreatedMessage({
id: 72,
alert_type: 'info',
title: '',
message: ''
});
assert.equal(withoutRoute.notification.body, 'Актуальная информация о движении транспорта');
});
test('reads disabled-by-default Firebase configuration and environment overrides', () => {
assert.deepEqual(getFirebaseConfig({}), {
enabled: false,
topic: 'route_alerts',
androidChannelId: 'route_alerts',
projectId: undefined
});
assert.equal(createFirebaseMessaging(getFirebaseConfig({})), null);
assert.deepEqual(getFirebaseConfig({
FCM_ENABLED: 'true',
FCM_TOPIC: 'custom_alerts',
FCM_ANDROID_CHANNEL_ID: 'custom_channel',
FIREBASE_PROJECT_ID: 'ridebus-test'
}), {
enabled: true,
topic: 'custom_alerts',
androidChannelId: 'custom_channel',
projectId: 'ridebus-test'
});
});