356 lines
12 KiB
JavaScript
356 lines
12 KiB
JavaScript
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 = `
|
|
<html>
|
|
<head><title>Working anime page</title></head>
|
|
<body>
|
|
<h1>Episode page</h1>
|
|
<script>var staleError = "Cloudflare error 522";</script>
|
|
<template>Error 522 Cloudflare Host Error</template>
|
|
<div hidden>Cloudflare Ray ID: abcdef1234567890</div>
|
|
<div style="display: none">Error 523 Cloudflare</div>
|
|
</body>
|
|
</html>`;
|
|
|
|
const blockedHtml = `
|
|
<html>
|
|
<head><title>site.example | 522: Connection timed out</title></head>
|
|
<body>
|
|
<h1>Connection timed out</h1>
|
|
<p>Error 522</p>
|
|
<p>Cloudflare</p>
|
|
</body>
|
|
</html>`;
|
|
|
|
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 не должно быть пустым.'
|
|
});
|
|
});
|