459947f76b
Круг смены 6 оборвали средой, работа помощника лежала в дереве несохранённой и её снёс бы первый же чужой откат. Замерил целостность, прочёл его отчёт, прогнал своей рукой, вынес приговор. Заново ничего не переделывал. ПРИНЯТО. Третья и последняя дверь с ключом рендера закрыта той же проверенной дверью. Список разрешённых машин общий с рендером и это доказано работой, а не доводом. Закрытая дверь красит СЕРЫМ, а не красным, и ключ при этом физически никуда не уходит, замерено мной двумя случаями по отдельному запуску на каждый. Помощник сверх задания нашёл и починил ложь, которой никто не искал: подпись плитки называла серый словами не отвечает, то есть гнала владельца продлевать исправные прокси. Мои ошибки называю первыми, их три. Пятьдесят девятая. Я назвал adminDashboardHelpers.ts чужим файлом соседней смены прямо в промте смене 7, в списке не трогать и в коммит не брать. Взял из головы, не замерив. Вся правка в этом файле - работа моего же помощника. Исполни следующая смена мой промт дословно, починка подписи плитки не попала бы в коммит и сгорела. Шестидесятая. Мой снимок оборванного круга был неполон: записал шесть файлов, их семь. Перечень собрал командой, но раньше, чем помощник закончил, и не перемерил перед тем, как назвать его окончательным. Шестьдесят первая. Мой датчик прогона искал не то слово и я чуть не объявил состоявшийся прогон несостоявшимся. Спас узкий прогон, которым я прибор перепроверил. Вырезание второго рода дало дыру. Допущение защиты: дверь проверяет адрес из настройки и считает, что запрос уйдёт именно туда. Случай мимо: разрешённая машина отвечает иди на другой адрес. Замерено на двух подставных машинах: тайный ключ уехал на машину, которой в списке разрешённых НЕТ, а плитка осталась ЗЕЛЁНОЙ и показала чужой адрес как свой. Перемерил на живой двери боевого Поиска клиентов тем же ножом, отдельным запуском - то же самое. Причина прочитана в исходнике библиотеки: при уходе на чужой адрес снимаются только два заголовка, и нашего среди них нет. Вторым прибором замерено, что отключения переадресации нет нигде во всём боевом коде. Это не отменяет Р126, дверь делает обещанное на своих случаях, но обещание шире дела и про это не сказано нигде. Отдельным кругом. Датчики: полный прогон моей рукой на своей базе 5344 проверки, 5340 зелёных, 4 пропущено, красных 0, арифметика сходится. Проверки интерфейса 3 из 3. Разметка 0 ошибок.
201 lines
13 KiB
PHP
201 lines
13 KiB
PHP
<?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'],
|
||
]);
|