Powrót do bloga
InżynieriaNovember 6, 20267 min czytania

Robot kliknął każdy przycisk. Znalazł osiem luk bezpieczeństwa.

Przestaliśmy testować kawałki, zaczęliśmy testować strony. Prawdziwa przeglądarka, prawdziwy serwer, prawdziwe kliknięcia - na każdym endpoincie, którego dotyka klient.

Nasz kod przechodził sprawdzanie typów, lintera i testy jednostkowe. A mimo to wypuszczał bugi. Nie egzotyczne - formularz resetu, który ignorował potwierdzenie hasła, linki do faktur prowadzące donikąd, feed pokazujący usunięte wątki.

Każdy z nich siedział w szczelinie między kawałkami. Funkcja była dobra. Strona wokół niej - nie. Dlatego zbudowaliśmy framework testowy, który obchodzi tylko jedno: co klient naprawdę widzi i wysyła.

Po co testować prawdziwą stronę?

Każda warstwa testów łapie inny rodzaj błędów. Tylko ostatnia widzi to, co widzą Twoi użytkownicy.

Test jednostkowy

Sprawdza jedną funkcję

Szybki i tani. Ślepy na połączenia: zły URL, zła nazwa pola, przycisk, który nigdy nic nie wysyła.

Test API

Sprawdza jedno żądanie

Dowodzi, że serwer odpowiada. Nigdy - że strona to żądanie wysyła albo pokazuje odpowiedź.

End-to-end

Sprawdza całą drogę

Otwiera stronę w Chromium, pisze jak człowiek, klika wyślij, czyta odpowiedź prosto z sieci. Jeśli działa tutaj, działa dla klientów.

Jak działa harness

Playwright sterowany z bun test, zbudowany na definicjach endpointów next-vibe. Definicja zna każde pole - test nie musi.

1

Otwórz prawdziwą stronę

Test otwiera stronę klienta - /user/login, /story/framework - a nie atrapę. Pisze dopiero wtedy, gdy React przejmie formularz.

2

Wypełnij typowanymi wartościami

Podajesz wartości, nie selektory. Sterowniki pól czytają definicję i obsługują widoczne UI - piszą, wybierają opcję po etykiecie, dołączają plik.

3

Złap żądanie z sieci

Submit zwraca prawdę: treść żądania, status i odpowiedź. Harness sprawdza, że żądanie niesie dokładnie to, co wpisano.

4

Nagraj raz, odtwarzaj zawsze

Płatni dostawcy - modele AI, obraz, muzyka, wideo - są nagrywani per test przy pierwszym uruchomieniu i potem odtwarzani. Zero niespodzianek na rachunku, powtarzalne wyniki.

5

Raport pokrycia

Raport liczy każdy publiczny endpoint: jeden sukces i jedno odrzucenie. Luki widać, a nie się ich domyśla.

contact.e2e.test.ts
describe("E2E: contact form", () => {
  const e2e = useE2E("contact");

  e2e.it("sends a message", async ({ open, uniqueEmail }) => {
    const form = await open(contactEndpoints.POST, {
      path: "/story/framework",
    });
    await form.fill({
      name: "E2E",
      email: uniqueEmail("contact"),
      subject: ContactSubject.BUG_REPORT,
      message: "...",
    });
    const result = await form.submit();
    expect(result.sent).toBe(true);
  });
});

Cała specyfikacja. Bez selektorów, bez mocków.

Wynik

50
zestawów testów
47/56
publicznych endpointów w pełni pokrytych
8
zamkniętych luk bezpieczeństwa

Pozostałe endpointy potrzebują płatnych kluczy dostawców wyszukiwania, scrapingu i mowy, zanim da się je nagrać.

Luki, które znalazł

Żadna nie wyszła w sprawdzaniu typów ani testach jednostkowych. Każda wyszła, gdy prawdziwa przeglądarka przeszła prawdziwą ścieżkę.

Open redirect na /track

Każdy mógł zbudować link, który przez naszą domenę przekierowywał na dowolną stronę. Zewnętrzne cele wymagają teraz podpisu HMAC.

SSRF przez adresy od użytkownika

Adresy, które dajesz AI, mogły wskazywać na wewnętrzne serwery. Strażnik rozwiązuje teraz DNS i sprawdza każdy krok przekierowania.

Cache serwera dzielony między żądaniami

Cache zapytań przy renderowaniu na serwerze mógł przenieść dane jednego użytkownika na stronę innego. Teraz jest izolowany per żądanie.

Debug system promptu czytał każdy wątek

Endpoint debugowania nie sprawdzał, do kogo należy wątek. Teraz sprawdza.

Publiczny feed pokazywał ukryte wątki

Ukryte i usunięte wątki pojawiały się w publicznym feedzie. Teraz są odfiltrowane.

Opcje logowania zdradzały konta

Odpowiedź opcji logowania zdradzała, czy dany e-mail ma konto. Już nie zdradza.

Reset ignorował potwierdzenie

Krok potwierdzenia resetu hasła ignorował pole potwierdzenia. Niezgodności są teraz odrzucane.

Prywatne pliki w publicznym cache

Prywatne pliki szły z nagłówkiem Cache-Control: public, więc współdzielone cache mogły je przechować. Teraz są prywatne.

I to, co było po prostu zepsute

Nic groźnego - ale po cichu psuło życie prawdziwym ludziom:

  • Adresy załączników w czacie i raporty błędów klienta zwracały 404.
  • Uprawnienia root nigdy się nie wczytywały - klienci nie mogli pisać w Shared.
  • Widget do osadzania nie startował.
  • Linki w mailach z fakturami zwracały 404.
  • Strona modeli była bezużyteczna.
  • Generowanie muzyki przez OpenRouter nie działało.
  • Zakres fixture'ów w testach przepuszczał płatne wywołania na żywo.
  • Lokalne bazy PGlite zwracały złe wiersze z INSERT…RETURNING.
  • Paginacja dla odwiedzających na /tools była zepsuta.
Do tego długa lista nieprzetłumaczonych tekstów i poprawek widgetów.

Co to znaczy dla Ciebie

Każda strona, przez którą się rejestrujesz, logujesz, płacisz, czatujesz, generujesz czy zapisujesz, ma teraz test, który otwiera ją w prawdziwej przeglądarce i robi to, co Ty. Gdy coś zmieniamy, robot przechodzi tę drogę, zanim Ty to zrobisz.

Framework jest częścią next-vibe. Budujesz na nim - Twoje endpointy dostają ten sam harness za darmo.

Przeczytaj harness. Użyj go we własnym kodzie.

Framework E2E jest open source i należy do next-vibe. Jeden plik specyfikacji na funkcję, tuż obok definicji.

Powrót do bloga