- 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`.
260 lines
7.9 KiB
JavaScript
260 lines
7.9 KiB
JavaScript
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'
|
||
});
|
||
});
|