Files
portal/app/tests/Unit/External/ProxyMarketProbeDverTest.php
T

201 lines
13 KiB
PHP
Raw Normal View History

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