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 data-only alert-created message with string-only data', 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', 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', title: 'Автобус задерживается', body: 'Задержка около 15 минут' }, android: { priority: 'high' } }); assert.equal('notification' in messages[0], false); assert.equal('notification' in messages[0].android, false); assert.equal(messages[0].android.priority, 'high'); assert.equal(messages[0].data.title, 'Автобус задерживается'); assert.equal(messages[0].data.body, 'Задержка около 15 минут'); 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, ''); assert.equal(message.data.title, 'Новость'); assert.equal(message.data.body, 'Важная информация'); assert.equal('notification' in message, false); assert.equal('notification' in message.android, false); }); 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.data.title, expectedTitle); assert.equal(message.data.body, 'Актуальная информация по маршруту №5'); } const withoutRoute = buildAlertCreatedMessage({ id: 72, alert_type: 'info', title: '', message: '' }); assert.equal(withoutRoute.data.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' }); });