Files
ridebus-admin/backend/test/alertNotificationService.test.js
Zakhar Timoshenko 6804ca5cdb feat: add AppLayout, BrandLogo, ProfileMenu, and RouteBadge components
- Implemented AppLayout component for application shell with navigation and sidebar.
- Created BrandLogo component to display the application logo.
- Developed ProfileMenu component for user profile management with dropdown functionality.
- Added RouteBadge component to visually represent route numbers with color coding.

style: add CSS styles for Dashboard and Routes pages

- Introduced Dashboard.css for styling the dashboard layout and components.
- Created RoutesPage.css for styling the routes page, including filters and table layout.
- Added SchedulesPage.css for styling the schedules layout and route options.
2026-09-15 22:11:10 +03:00

247 lines
7.2 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 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'
});
});