Files
ridebus-admin/backend/test/alertNotificationService.test.js
Zakhar Timoshenko f7f0b039dd Add Telegram cancellation import functionality with tests and documentation
- Implemented Telegram cancellation parsing logic in `telegramCancellationParser.js`.
- Created unit tests for cancellation parsing in `telegramCancellationParser.test.js`.
- Developed integration tests for Telegram import service in `telegramImport.integration.test.js`.
- Added API tests for Telegram import endpoints in `telegramImportApi.test.js`.
- Introduced service tests for Telegram import logic in `telegramImportService.test.js`.
- Documented the Telegram cancellation import process in `telegram-cancellation-import.md`.
- Built a React component for managing Telegram imports in `TelegramImportPanel.jsx`.
2026-10-03 00:45:41 +03:00

260 lines
7.9 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');
test('expires a cancellation at Minsk midnight and does not send a stale push', async () => {
const now = new Date('2026-10-02T20:59:00Z');
const alert = { id: 1, alert_type: 'cancellation', end_time: '2026-10-02T21:00:00Z' };
assert.equal(buildAlertCreatedMessage(alert, { now }).android.ttl, 60_000);
let sends = 0;
const service = new AlertNotificationService({ enabled: true, now: () => new Date('2026-10-02T21:00:00Z'),
getMessaging: async () => { sends++; }, logger: createLogger() });
assert.deepEqual(await service.sendAlertCreated(alert), { sent: false, skipped: true, reason: 'expired' });
assert.equal(sends, 0);
});
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',
now: () => new Date('2026-07-11T08:30:00Z'),
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',
ttl: 3_600_000
}
});
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'
});
});