Files
portal/app/tests/Unit/External/ProxyMarketProbeDverTest.php
T
Дмитрий 459947f76b Р126 третья дверь: работа оборванного круга сохранена и принята, дыра переадресации найдена
Круг смены 6 оборвали средой, работа помощника лежала в дереве несохранённой и её снёс бы
первый же чужой откат. Замерил целостность, прочёл его отчёт, прогнал своей рукой, вынес
приговор. Заново ничего не переделывал.

ПРИНЯТО. Третья и последняя дверь с ключом рендера закрыта той же проверенной дверью.
Список разрешённых машин общий с рендером и это доказано работой, а не доводом. Закрытая
дверь красит СЕРЫМ, а не красным, и ключ при этом физически никуда не уходит, замерено мной
двумя случаями по отдельному запуску на каждый.

Помощник сверх задания нашёл и починил ложь, которой никто не искал: подпись плитки называла
серый словами не отвечает, то есть гнала владельца продлевать исправные прокси.

Мои ошибки называю первыми, их три.

Пятьдесят девятая. Я назвал adminDashboardHelpers.ts чужим файлом соседней смены прямо в
промте смене 7, в списке не трогать и в коммит не брать. Взял из головы, не замерив. Вся
правка в этом файле - работа моего же помощника. Исполни следующая смена мой промт дословно,
починка подписи плитки не попала бы в коммит и сгорела.

Шестидесятая. Мой снимок оборванного круга был неполон: записал шесть файлов, их семь.
Перечень собрал командой, но раньше, чем помощник закончил, и не перемерил перед тем, как
назвать его окончательным.

Шестьдесят первая. Мой датчик прогона искал не то слово и я чуть не объявил состоявшийся
прогон несостоявшимся. Спас узкий прогон, которым я прибор перепроверил.

Вырезание второго рода дало дыру. Допущение защиты: дверь проверяет адрес из настройки и
считает, что запрос уйдёт именно туда. Случай мимо: разрешённая машина отвечает иди на другой
адрес. Замерено на двух подставных машинах: тайный ключ уехал на машину, которой в списке
разрешённых НЕТ, а плитка осталась ЗЕЛЁНОЙ и показала чужой адрес как свой. Перемерил на
живой двери боевого Поиска клиентов тем же ножом, отдельным запуском - то же самое. Причина
прочитана в исходнике библиотеки: при уходе на чужой адрес снимаются только два заголовка,
и нашего среди них нет. Вторым прибором замерено, что отключения переадресации нет нигде во
всём боевом коде. Это не отменяет Р126, дверь делает обещанное на своих случаях, но обещание
шире дела и про это не сказано нигде. Отдельным кругом.

Датчики: полный прогон моей рукой на своей базе 5344 проверки, 5340 зелёных, 4 пропущено,
красных 0, арифметика сходится. Проверки интерфейса 3 из 3. Разметка 0 ошибок.
2026-08-07 09:29:23 +03:00

201 lines
13 KiB
PHP
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.
<?php
declare(strict_types=1);
use App\Services\External\LivenessReading;
use App\Services\External\ProxyMarketProbe;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
uses(TestCase::class);
// ─────────────────────────────────────────────────────────────────────────────
// Р126, ТРЕТЬЯ ДВЕРЬ. Проба живости прокси шлёт ТОТ ЖЕ тайный ключ SELF_RENDER_KEY
// заголовком X-Render-Key, что и обе уже закрытые двери рендера, — но адрес берёт из
// ДРУГОЙ настройки, PROXY_MARKET_CHECK_URL. Опечатка в ней увела бы ключ чужому, а
// http:// увёз бы его открытым текстом.
//
// 🔴 Эти проверки смотрят на РАБОТУ, а не на слова: главный признак «ключ не уехал» —
// Http::assertNothingSent(), то есть запроса не было вовсе. Текст причины проверяется
// отдельно и только там, где он что-то решает для человека у плитки.
// ─────────────────────────────────────────────────────────────────────────────
/** Боевой адрес ручки прокси на виртуалке-рендере. Замер: config/services.php + ПИЛОТ.md 06.07.2026. */
const PM_BOEVOY = 'https://51-250-1-97.sslip.io/proxy-check';
/** Тайный ключ в этих проверках. Ни одна причина закрытия не имеет права его назвать. */
const PM_KLYUCH = 'sekret-renderа-123';
beforeEach(function () {
config()->set('services.proxy_market.check_url', PM_BOEVOY);
config()->set('services.proxy_market.expires_at', now()->addDays(30)->format('Y-m-d'));
config()->set('services.proxy_market.amber_days', 7);
config()->set('services.self_render.key', PM_KLYUCH);
// 🔴 Список НЕ выдумываем — берём настоящее умолчание config/services.php, то самое,
// что поедет на боевой.
config()->set('services.self_render.allowed_hosts', (array) config('services.self_render.allowed_hosts'));
});
function pmProba(): LivenessReading
{
return (new ProxyMarketProbe)->check();
}
// ─────────────────────────────────────────────────────────────────────────────
// 1. Дверь ВООБЩЕ применена: чужой хост и http:// наружу не выпускают
// ─────────────────────────────────────────────────────────────────────────────
it('чужой хост в PROXY_MARKET_CHECK_URL: запроса не было — ключ никуда не уехал', function () {
Http::fake();
config()->set('services.proxy_market.check_url', 'https://51-250-1-97.sslip.io.zloumyshlennik.ru/proxy-check');
expect(pmProba()->light)->toBe('grey');
Http::assertNothingSent();
});
it('опечатка в одной букве хоста закрывает дверь', function () {
Http::fake();
// sslip → ssIip (заглавная I вместо l) — ровно та опечатка, ради которой дверь заведена.
config()->set('services.proxy_market.check_url', 'https://51-250-1-97.ssIip.io/proxy-check');
expect(pmProba()->light)->toBe('grey');
Http::assertNothingSent();
});
it('http на разрешённый хост: ключ не поехал открытым текстом', function () {
Http::fake();
config()->set('services.proxy_market.check_url', 'http://51-250-1-97.sslip.io/proxy-check');
$r = pmProba();
expect($r->light)->toBe('grey');
expect($r->detail)->toContain('без шифрования');
Http::assertNothingSent();
});
it('адрес без схемы вовсе не принимается', function () {
Http::fake();
config()->set('services.proxy_market.check_url', '51-250-1-97.sslip.io/proxy-check');
expect(pmProba()->light)->toBe('grey');
Http::assertNothingSent();
});
it('пустой тайный ключ закрывает дверь ДО запроса, а не ловится ответом 401', function () {
Http::fake();
config()->set('services.self_render.key', '');
expect(pmProba()->light)->toBe('grey');
Http::assertNothingSent();
});
// ─────────────────────────────────────────────────────────────────────────────
// 2. 🔴 СЕРЫЙ, А НЕ КРАСНЫЙ. Красный на этой плитке значит «прокси кончились» —
// владелец побежит их продлевать. Соврать им нельзя.
// ─────────────────────────────────────────────────────────────────────────────
it('закрытая дверь красит серым, а живой отказ прокси — по-прежнему красным', function () {
Http::fake();
config()->set('services.proxy_market.check_url', 'http://51-250-1-97.sslip.io/proxy-check');
expect(pmProba()->light)->toBe('grey');
// Та же плитка, но дверь открыта и прокси ответил отказом — красный сохранился.
config()->set('services.proxy_market.check_url', PM_BOEVOY);
Http::fake([PM_BOEVOY => Http::response(['ok' => false, 'error' => 'no proxies'], 200)]);
expect(pmProba()->light)->toBe('red');
});
it('вышедший срок остаётся красным — закрытая дверь его не съела', function () {
config()->set('services.proxy_market.expires_at', now()->subDay()->format('Y-m-d'));
Http::fake([PM_BOEVOY => Http::response(['ok' => true, 'ip' => '178.9.9.9'], 200)]);
expect(pmProba()->light)->toBe('red');
});
it('причина закрытия называет настройку PROXY_MARKET_CHECK_URL и НЕ называет ключ', function () {
Http::fake();
config()->set('services.proxy_market.check_url', 'http://51-250-1-97.sslip.io/proxy-check');
$detail = pmProba()->detail;
expect($detail)->toContain('PROXY_MARKET_CHECK_URL');
expect($detail)->not->toContain(PM_KLYUCH);
});
// ─────────────────────────────────────────────────────────────────────────────
// 3. 🔴 Живая плитка не сломана: боевой адрес проходит на НАСТОЯЩЕМ умолчании
// настроек и тогда, когда приложение считает себя боевым.
// ─────────────────────────────────────────────────────────────────────────────
it('боевой адрес прокси открыт на настоящем умолчании и на боевом окружении', function () {
app()->detectEnvironment(fn () => 'production');
Http::fake([PM_BOEVOY => Http::response(['ok' => true, 'ip' => '178.9.9.9'], 200)]);
$r = pmProba();
expect($r->light)->toBe('green');
expect($r->detail)->toContain('178.9.9.9');
Http::assertSent(fn ($req) => $req->url() === PM_BOEVOY && $req->hasHeader('X-Render-Key', PM_KLYUCH));
});
it('пробелы по краям адреса не мешают: ходим ровно по проверенному адресу', function () {
config()->set('services.proxy_market.check_url', ' '.PM_BOEVOY.' ');
Http::fake([PM_BOEVOY => Http::response(['ok' => true, 'ip' => '178.9.9.9'], 200)]);
expect(pmProba()->light)->toBe('green');
Http::assertSent(fn ($req) => $req->url() === PM_BOEVOY);
});
// ─────────────────────────────────────────────────────────────────────────────
// 4. 🔴 Список разрешённых машин — ОБЩИЙ с рендером. Доказывается работой:
// трогаем ТОЛЬКО services.self_render.allowed_hosts и смотрим, меняется ли
// поведение проксишной пробы. Свой список у неё завести молча не получится.
// ─────────────────────────────────────────────────────────────────────────────
it('опустошение списка машин РЕНДЕРА закрывает и проксишную дверь', function () {
Http::fake();
config()->set('services.self_render.allowed_hosts', []);
expect(pmProba()->light)->toBe('grey');
Http::assertNothingSent();
});
it('машина, добавленная в список РЕНДЕРА, открывает и проксишную дверь', function () {
config()->set('services.self_render.allowed_hosts', ['novaya-vm.example.ru']);
config()->set('services.proxy_market.check_url', 'https://novaya-vm.example.ru/proxy-check');
Http::fake(['novaya-vm.example.ru/*' => Http::response(['ok' => true, 'ip' => '178.9.9.9'], 200)]);
expect(pmProba()->light)->toBe('green');
Http::assertSent(fn ($req) => $req->hasHeader('X-Render-Key', PM_KLYUCH));
});
it('умолчание списка машин НЕ пусто — иначе плитка посерела бы в момент выката', function () {
expect((array) config('services.self_render.allowed_hosts'))->not->toBeEmpty();
});
// ─────────────────────────────────────────────────────────────────────────────
// 5. 🔴 ФОРМЫ АДРЕСА, какие могут стоять в боевом .env — что увидит владелец.
// Считаем на БОЕВОМ окружении: петля на проде послаблением не пользуется.
// ─────────────────────────────────────────────────────────────────────────────
it('формы адреса боевого .env дают ровно тот цвет, который принял владелец', function (string $adres, string $cvet) {
app()->detectEnvironment(fn () => 'production');
config()->set('services.proxy_market.check_url', $adres);
Http::fake(['*' => Http::response(['ok' => true, 'ip' => '178.9.9.9'], 200)]);
expect(pmProba()->light)->toBe($cvet);
})->with([
'боевой https по имени' => [PM_BOEVOY, 'green'],
'боевой https по адресу машины' => ['https://51.250.1.97/proxy-check', 'green'],
'https с явным портом 443' => ['https://51-250-1-97.sslip.io:443/proxy-check', 'green'],
'HTTPS заглавными' => ['HTTPS://51-250-1-97.sslip.io/proxy-check', 'green'],
'https с пробелами по краям' => [' https://51-250-1-97.sslip.io/proxy-check ', 'green'],
'петля по https' => ['https://127.0.0.1/proxy-check', 'green'],
'🔴 http — ЦЕНА, принятая владельцем' => ['http://51-250-1-97.sslip.io/proxy-check', 'grey'],
'🔴 http на петлю, но окружение боевое' => ['http://127.0.0.1/proxy-check', 'grey'],
'опечатка в имени машины' => ['https://51-250-1-97.ssIip.io/proxy-check', 'grey'],
'чужой хост с боевым именем внутри' => ['https://51-250-1-97.sslip.io.chuzhoy.ru/proxy-check', 'grey'],
'вовсе без схемы' => ['51-250-1-97.sslip.io/proxy-check', 'grey'],
'пусто' => ['', 'grey'],
]);