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`.
This commit is contained in:
89
backend/test/telegramCancellationParser.test.js
Normal file
89
backend/test/telegramCancellationParser.test.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { parseCancellation, matchCancellationRoutes, buildCancellationAlert, dayBounds } = require('../src/services/telegramCancellationParser');
|
||||
|
||||
const text = `Уважаемые пассажиры!
|
||||
Не будут выполняться рейсы:
|
||||
ПО МАРШРУТУ № 27 С ОСТАНОВКИ АВ/ВОКЗАЛ НА 15.43,16.43.
|
||||
С ОСТАНОВКИ ЗОДЧЕГО НА 15.58,16.58.
|
||||
С УВАЖЕНИЕМ
|
||||
Филиал "Автобусный парк №2 г.Полоцка" ОАО "Витебскоблавтотранс"`;
|
||||
const parse = (value = text, publishedAt = '2026-10-02T12:52:00Z') => parseCancellation({ text: value, publishedAt });
|
||||
|
||||
test('carrier post preserves four individual departures and canonicalizes АВ/ВОКЗАЛ', () => {
|
||||
const result = parse();
|
||||
assert.equal(result.status, 'ready');
|
||||
assert.equal(result.serviceDate, '2026-10-02');
|
||||
assert.deepEqual(result.groups, [{ routeNumber: '27', departures: [
|
||||
{ stop: 'Автовокзал', times: ['15:43', '16:43'] }, { stop: 'ЗОДЧЕГО', times: ['15:58', '16:58'] }
|
||||
] }]);
|
||||
});
|
||||
|
||||
test('implicit date comes from publication in Minsk, never from import day or edit date', () => {
|
||||
assert.equal(parse(text, '2026-10-02T22:30:00Z').serviceDate, '2026-10-03');
|
||||
});
|
||||
|
||||
test('tomorrow, numeric and Russian dates take precedence over publication day', () => {
|
||||
for (const prefix of ['Завтра\n', '03.10.2026\n', 'На 03.10\n', '3 октября 2026 года\n']) {
|
||||
assert.equal(parse(prefix + text).serviceDate, '2026-10-03', prefix);
|
||||
}
|
||||
assert.equal(parse('Завтра\n' + text, '2026-12-31T17:00:00Z').serviceDate, '2027-01-01');
|
||||
assert.equal(parse('На 01.01\n' + text, '2026-12-31T17:00:00Z').serviceDate, '2027-01-01');
|
||||
});
|
||||
|
||||
test('09.10 in a departure list remains a time', () => {
|
||||
assert.deepEqual(parse(text.replace('15.43', '09.10')).groups[0].departures[0].times, ['09:10', '16:43']);
|
||||
});
|
||||
|
||||
test('ambiguous dates, conditions, invalid times and unspecified departures require review', () => {
|
||||
for (const value of [
|
||||
'Сегодня и завтра\n' + text, 'На 31.02.2026\n' + text,
|
||||
'С 02.10 по 05.10\n' + text, 'По пятницам\n' + text,
|
||||
text.replace('15.43', '25.43'), text.replace('15.43', '15.99'),
|
||||
'Не будут выполняться рейсы по маршруту №27.', text + '\nКроме отправления на 18:00.',
|
||||
'Послезавтра\n' + text
|
||||
]) assert.equal(parse(value).status, 'review', value);
|
||||
});
|
||||
|
||||
test('news is ignored; a picture without text stays on review', () => {
|
||||
assert.equal(parse('С праздником, уважаемые пассажиры!').status, 'ignored');
|
||||
assert.equal(parseCancellation({ text: '', hasMedia: true, publishedAt: '2026-10-02T10:00:00Z' }).status, 'review');
|
||||
});
|
||||
|
||||
test('review retains a recognized future date; attached media cannot be silently auto-published', () => {
|
||||
const result = parse('Завтра\nНе будут выполняться рейсы по маршруту №27.');
|
||||
assert.equal(result.status, 'review');
|
||||
assert.equal(result.serviceDate, '2026-10-03');
|
||||
assert.equal(parseCancellation({ text, hasMedia: true, publishedAt: '2026-10-02T12:00:00Z' }).status, 'review');
|
||||
});
|
||||
|
||||
test('different route numbers in one post are separate groups', () => {
|
||||
const result = parse(text + '\nПО МАРШРУТУ № 5А С ОСТАНОВКИ ЦЕНТР НА 18:00.');
|
||||
assert.equal(result.status, 'ready');
|
||||
assert.deepEqual(result.groups.map(g => g.routeNumber), ['27', '5а']);
|
||||
});
|
||||
|
||||
test('route number with two directions is matched by origin stop, with no guess on missing origin', () => {
|
||||
const routes = [
|
||||
{ id: 1, route_number: '27', origin_stop: 'Автовокзал' },
|
||||
{ id: 2, route_number: '27', origin_stop: 'Зодчего' },
|
||||
{ id: 3, route_number: '5', origin_stop: 'Центр' }
|
||||
];
|
||||
const matched = matchCancellationRoutes(parse(), routes);
|
||||
assert.deepEqual(matched.plans.map(p => p.routeId), [1, 2]);
|
||||
assert.equal(matched.reasons.length, 0);
|
||||
assert.equal(matchCancellationRoutes(parse(), routes.map(r => ({ ...r, origin_stop: '' }))).plans.length, 0);
|
||||
assert.equal(matchCancellationRoutes(parse(), []).reasons.length, 1);
|
||||
});
|
||||
|
||||
test('midnight expiry is an explicit UTC instant, including year boundary', () => {
|
||||
assert.deepEqual(dayBounds('2026-10-02'), { start: '2026-10-01T21:00:00.000Z', end: '2026-10-02T21:00:00.000Z' });
|
||||
assert.equal(dayBounds('2026-12-31').end, '2026-12-31T21:00:00.000Z');
|
||||
const plan = matchCancellationRoutes(parse(), [{ id: 1, route_number: '27' }]).plans[0];
|
||||
const alert = buildCancellationAlert(plan, '2026-10-02', 'https://t.me/avp2Polotsk/1166', '2026-10-02T12:52:00Z', 1);
|
||||
assert.equal(alert.end_time, '2026-10-02T21:00:00.000Z');
|
||||
assert.match(alert.message, /Автовокзал.*15:43, 16:43/);
|
||||
assert.match(alert.title, /отдельных рейсов/);
|
||||
});
|
||||
|
||||
module.exports = { carrierText: text };
|
||||
Reference in New Issue
Block a user