const assert = require('assert'); const S = require('../shared.js'); function test(name, fn) { try { fn(); console.log('ok - ' + name); } catch (error) { console.error('not ok - ' + name); throw error; } } test('numeric rule rejects empty host when includeEmpty is disabled', function () { const rule = { enabled: true, hostRegex: '^v\\d*\\.vost\\.pw$', strategy: 'number', template: 'v{n}.vost.pw', includeEmpty: false, numberStart: 1, numberEnd: 100 }; assert.strictEqual(S.matchesRule(rule, 'v.vost.pw'), false); assert.strictEqual(S.matchesRule(rule, 'v50.vost.pw'), true); }); test('default rules use probe mode', function () { assert.strictEqual(S.createMultclubRule().mode, 'probe'); assert.strictEqual(S.createEmptyRule().mode, 'probe'); }); test('default blocked markers match the Multclub preset list', function () { const requestedMarkers = `доступ ограничен доступ к сайту ограничен сайт заблокирован заблокировано ограничение доступа ресурс заблокирован по решению органов государственной власти роскомнадзор forbidden 403 forbidden 404 not found bad gateway service unavailable cloudflare host error host error cloudflare ray id ray id cloudflare-ray cf-ray идентификатор луча cloudflare идентификатор ray cloudflare идентификатор cloudflare ray идентификатор луча web server is down origin is unreachable connection timed out время ожидания соединения истекло истекло время ожидания соединения код ошибки 520 код ошибки 521 код ошибки 522 код ошибки 523 код ошибки 524 код ошибки 525 код ошибки 526 код ошибки 530 ошибка 520 ошибка 521 ошибка 522 ошибка 523 ошибка 524 ошибка 525 ошибка 526 ошибка 530 первоначальное соединение между сетью cloudflare между сетью cloudflare и исходным веб-сервером исходным веб-сервером истекло веб-страница не может быть отображена a timeout occurred gateway timeout error 520 error 521 error 522 error 523 error 524 error 525 error 526 error 530 the web server is down the origin web server is not reachable the initial connection between cloudflare cloudflare was unable to establish a connection browser working cloudflare working`.split('\n'); assert.deepStrictEqual(S.DEFAULT_MARKERS, requestedMarkers); assert.strictEqual(S.createMultclubRule().blockedMarkers, requestedMarkers.join('\n')); }); test('numeric rule rejects hosts outside the configured range', function () { const rule = { enabled: true, hostRegex: '^v\\d+\\.vost\\.pw$', strategy: 'number', template: 'v{n}.vost.pw', includeEmpty: false, numberStart: 1, numberEnd: 100 }; assert.strictEqual(S.matchesRule(rule, 'v109.vost.pw'), false); }); test('numeric rule supports ranges that start at zero', function () { const rule = { enabled: true, hostRegex: '^mirror\\d+\\.example$', strategy: 'number', template: 'mirror{n}.example', includeEmpty: false, numberStart: 0, numberEnd: 2, numberStep: 1 }; assert.strictEqual(S.matchesRule(rule, 'mirror0.example'), true); assert.deepStrictEqual(S.buildCandidates(rule, 'mirror1.example', null), [ 'mirror2.example', 'mirror0.example' ]); }); test('numeric rule distinguishes zero from an empty number slot', function () { const rule = { enabled: true, hostRegex: '^mirror\\d*\\.example$', strategy: 'number', template: 'mirror{n}.example', includeEmpty: false, numberStart: 0, numberEnd: 2, numberStep: 1 }; assert.strictEqual(S.matchesRule(rule, 'mirror0.example'), true); assert.strictEqual(S.matchesRule(rule, 'mirror.example'), false); }); test('buildCandidates starts after current number and wraps without duplicates', function () { const rule = { enabled: true, hostRegex: '^site\\d+\\.example$', strategy: 'number', template: 'site{n}.example', includeEmpty: false, numberStart: 1, numberEnd: 5, numberStep: 1 }; assert.deepStrictEqual(S.buildCandidates(rule, 'site3.example', null), [ 'site4.example', 'site5.example', 'site1.example', 'site2.example' ]); }); test('cached host is first only when it still matches the rule', function () { const rule = { enabled: true, hostRegex: '^site\\d+\\.example$', strategy: 'number', template: 'site{n}.example', includeEmpty: false, numberStart: 1, numberEnd: 5, numberStep: 1 }; assert.deepStrictEqual(S.buildCandidates(rule, 'site3.example', 'site2.example').slice(0, 3), [ 'site2.example', 'site4.example', 'site5.example' ]); assert.strictEqual(S.buildCandidates(rule, 'site3.example', 'site9.example')[0], 'site4.example'); }); test('list strategy matches hosts from custom hosts without a host regex', function () { const rule = { enabled: true, hostRegex: '', strategy: 'list', customHosts: 'mirror1.example\nmirror2.example' }; assert.strictEqual(S.validateRule(rule), null); assert.strictEqual(S.matchesRule(rule, 'mirror1.example'), true); assert.strictEqual(S.matchesRule(rule, 'mirror3.example'), false); assert.deepStrictEqual(S.buildCandidates(rule, 'mirror1.example', null), [ 'mirror1.example', 'mirror2.example' ]); }); test('list strategy validation requires custom hosts instead of host regex', function () { assert.deepStrictEqual(S.validateRuleCode({ hostRegex: '', strategy: 'list', customHosts: '' }), { code: 'listHostsRequired', message: 'Для режима списка нужно указать хотя бы один host.' }); }); test('letter strategy rotates after the current token', function () { const rule = { enabled: true, hostRegex: '^mirror-[ab]+\\.example$', strategy: 'letters', template: 'mirror-{s}.example', letterCharset: 'ab', letterMinLen: 1, letterMaxLen: 2, maxCandidates: 6 }; assert.deepStrictEqual(S.buildCandidates(rule, 'mirror-b.example', null), [ 'mirror-aa.example', 'mirror-ab.example', 'mirror-ba.example', 'mirror-bb.example', 'mirror-a.example' ]); }); test('blocked-page detection recognizes Cloudflare and configured markers', function () { const rule = { blockedMarkers: 'custom block marker' }; assert.strictEqual(S.findBlockedMarkerInText(rule, 'Cloudflare Ray ID: abcdef1234567890'), 'cloudflare-ray-id'); assert.strictEqual(S.findBlockedMarkerInText(rule, 'Код ошибки 522 cloudflare'), 'код ошибки 522'); assert.strictEqual(S.findBlockedMarkerInText(rule, 'page says CUSTOM BLOCK MARKER here'), 'custom block marker'); }); test('probe blocked-page detection ignores hidden HTML marker text', function () { const rule = { blockedMarkers: '' }; const workingHtml = ` Working anime page

Episode page

Error 523 Cloudflare
`; const blockedHtml = ` site.example | 522: Connection timed out

Connection timed out

Error 522

Cloudflare

`; assert.strictEqual(S.responseLooksBlocked(rule, workingHtml), false); assert.strictEqual(S.responseLooksBlocked(rule, blockedHtml), true); }); test('page blocked-page detection ignores embedded Cloudflare text on a normal page', function () { const normalText = [ 'Koko wa Ore ni Makasete Saki ni Ike episode page', 'Navigation catalog comments related episodes release schedule'.repeat(80), 'Player tab 3 failed: Cloudflare 522 connection timed out', 'More normal page content'.repeat(80) ].join('\n'); assert.strictEqual(S.findBlockedMarkerInPage({}, 'Animevost episode page', normalText), ''); assert.strictEqual(S.findBlockedMarkerInPage({}, '522: Connection timed out', normalText), 'connection timed out'); }); test('page blocked-page detection ignores embedded generic forbidden text on a normal page', function () { const normalText = [ 'Multclub home page', 'Navigation catalog comments releases schedule profile search'.repeat(80), 'A player message mentions forbidden in a nested widget.', 'More normal page content'.repeat(80) ].join('\n'); assert.strictEqual(S.findBlockedMarkerInPage({}, 'Multclub', normalText), ''); assert.strictEqual(S.findBlockedMarkerInPage({}, '403 Forbidden', normalText), 'forbidden'); assert.strictEqual(S.findBlockedMarkerInPage({}, 'Multclub', 'Forbidden'), 'forbidden'); }); test('page blocked-page detection ignores every built-in marker embedded in a long normal page', function () { S.DEFAULT_MARKERS.forEach(function (marker) { const normalText = [ 'Normal mirror page', 'Navigation catalog comments releases schedule profile search'.repeat(90), 'Embedded diagnostic text: ' + marker, 'More normal page content'.repeat(90) ].join('\n'); assert.strictEqual( S.findBlockedMarkerInPage({}, 'Normal mirror page', normalText), '', 'marker should not stop a long normal page: ' + marker ); }); }); test('page blocked-page detection still accepts focused built-in error pages', function () { assert.strictEqual(S.findBlockedMarkerInPage({}, 'Multclub', 'Доступ к сайту ограничен по решению органов государственной власти'), 'доступ к сайту ограничен'); assert.strictEqual(S.findBlockedMarkerInPage({}, 'Multclub', 'Bad gateway'), 'bad gateway'); assert.strictEqual(S.findBlockedMarkerInPage({}, 'Multclub', 'Cloudflare host error. Error 522. Ray ID abcdef1234567890.'), 'cloudflare-ray-id'); }); test('URL helpers preserve path, query, and hash', function () { const rule = { protocol: 'https' }; const source = new URL('http://old.example/path/page.html?a=1#top'); assert.strictEqual(S.buildTargetUrl(rule, 'new.example', source), 'https://new.example/path/page.html?a=1#top'); }); test('rule validation rejects an empty host regex', function () { assert.strictEqual(S.validateRule({ hostRegex: '', strategy: 'number', template: 'site{n}.example' }), 'Поле hostRegex не должно быть пустым.'); }); test('rule validation exposes stable error codes for localization', function () { assert.deepStrictEqual(S.validateRuleCode({ hostRegex: '', strategy: 'number', template: 'site{n}.example' }), { code: 'hostRegexRequired', message: 'Поле hostRegex не должно быть пустым.' }); });