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'],
|
|||
|
|
]);
|