vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    Автентифікація та авторизація

    Зміст

    Коли автотест «логіниться» в застосунок, за цим стоять два різні процеси, які легко сплутати, поки вони працюють, і дуже боляче плутати, коли вони ламаються. Цей розділ розбирає, як застосунок розуміє, хто ти і що тобі дозволено, у яких формах цей стан живе (cookie, токен, JWT), як він передається в кожному запиті — і чому для тестів майже завжди вигідніше отримати цей стан через API, а не клікати форму входу.

    Автентифікація проти авторизації

    Це дві окремі стадії, і плутанина між ними — джерело як багів у продукті, так і хибних діагнозів у тестах.

    Автентифікація (authentication) — відповідь на питання «хто ти?». Користувач доводить свою особу: логін і пароль, токен, відбиток, код із застосунку-автентифікатора. Результат — застосунок упевнений, що по той бік саме той, за кого себе видають.

    Авторизація (authorization) — відповідь на питання «що тобі можна?». Автентифікований користувач намагається виконати дію, а система вирішує, чи має він на це право: чи може читач редагувати проєкт, чи бачить менеджер чужі рахунки.

    Порядок завжди такий: спершу автентифікація, потім авторизація. Не можна вирішити, що людині дозволено, поки не знаєш, хто вона.

    Найчіткіше різницю видно у кодах відповіді HTTP:

    КодНазваНасправді означаєКоли
    401Unauthorized«Я тебе не впізнав» — проблема автентифікаціїТокен відсутній, протермінований, невалідний
    403Forbidden«Я знаю, хто ти, але тобі сюди не можна» — проблема авторизаціїВалідний користувач без потрібних прав

    Назва 401 Unauthorized історично невдала: за змістом це «unauthenticated». За специфікацією (RFC 9110, §15.5.2) відповідь 401 мусить нести заголовок WWW-Authenticate щонайменше з одним викликом (challenge), який підказує клієнту, яку схему автентифікації застосувати. Для AQA це практичне правило діагностики: 401 — копай у бік логіну (протух токен, не підставилась cookie); 403 — копай у бік ролей і прав. Плутати їх у перевірках не можна, бо це різні вимоги.

    Історично перший підхід у вебі. HTTP не має пам’яті — кожен запит незалежний. Щоб сервер «пам’ятав» залогіненого користувача між запитами, придумали сесії (sessions).

    Механіка така:

    1. Користувач надсилає логін і пароль.
    2. Сервер перевіряє їх, створює запис сесії у себе (у пам’яті, Redis, базі) і генерує випадковий ідентифікатор сесії (session ID).
    3. Сервер повертає цей ID у заголовку Set-Cookie.
    4. Браузер зберігає cookie і автоматично додає її до кожного наступного запиту на цей домен.
    POST /login HTTP/1.1
    Host: app.example.com
    Content-Type: application/json
    
    {"email": "user@example.com", "password": "secret"}
    HTTP/1.1 200 OK
    Set-Cookie: session_id=8f2a...e91; HttpOnly; Secure; SameSite=Lax; Path=/

    Далі браузер сам підставляє cookie:

    GET /api/projects HTTP/1.1
    Host: app.example.com
    Cookie: session_id=8f2a...e91

    Ключова ідея: сам ID нічого не означає, це просто випадковий ключ. Уся інформація про користувача лежить на сервері, тобто стан зберігається на сервері (stateful) — і це водночас сила і слабкість. Сила: сесію можна миттєво відкликати, просто видаливши запис. Слабкість: усі сервери в кластері мусять мати спільне сховище сесій, інакше користувача «викидатиме» щоразу, як його запит потрапить на іншу машину.

    Атрибути cookie, які зобов’язаний розуміти AQA:

    АтрибутЩо робитьЧому важливо для тестів
    HttpOnlyCookie недоступна для JavaScript (document.cookie її не бачить)Захист від крадіжки через XSS; але й твій page.evaluate її не прочитає
    SecureНадсилається лише через HTTPS (на реальних хостах)На звичайному http:// cookie не встановиться; сучасні Chrome/Firefox роблять виняток для localhost як secure context — джерело «загадкових» падінь локально
    SameSiteЧи слати cookie в крос-сайтових запитах: Strict / Lax / NoneВпливає на CSRF і на сценарії з iframe/редіректами (OAuth); None вимагає ще й Secure
    Domain / PathНа які домени й шляхи діє cookieCookie з піддомену може не підставитись на інший піддомен
    Expires / Max-AgeЧас життяБез них — сесійна cookie, зникає із закриттям браузера

    Нюанс. За MDN, https-вимогу атрибута Secure браузери ігнорують для localhost — тому Secure-кукі на http://localhost зазвичай працюють. А от поведінка префіксів __Host-/__Secure- на http://localhost і різниця між localhost та 127.0.0.1 між браузерами відрізняються — для конкретного стенду перевір емпірично.

    Важлива деталь про замовчування: браузери на базі Chromium (Chrome, Edge) за відсутності явного SameSite трактують cookie як SameSite=Lax — це діє з Chrome 80 (2020). Firefox і Safari до Lax-за-замовчуванням НЕ переходять: Firefox спирається на Total Cookie Protection (партиціювання сторонніх cookie), а Safari блокує більшість сторонніх cookie через Intelligent Tracking Prevention незалежно від SameSite. Тобто той самий сценарій на різних рушіях може поводитися по-різному, і це варто тримати в голові.

    CSRF-токен

    Автоматичне підставляння cookie, яке робить сесії такими зручними, породжує окремий клас атак — CSRF (Cross-Site Request Forgery, міжсайтова підробка запиту).

    Уяви: користувач залогінений у bank.example.com, його сесійна cookie живе в браузері. Він відкриває сторонній сайт, на якому захована форма, що автоматично надсилає POST на bank.example.com/transfer. Браузер, як і завжди, сам чіпляє до цього запиту сесійну cookie — бо запит іде на банківський домен. Сервер бачить валідну сесію й виконує переказ. Користувач нічого не натискав свідомо; за нього все зробив чужий сайт його ж автентифікованим браузером.

    bank.example.comБраузерЧужий сайтКористувачbank.example.comБраузерЧужий сайтКористувачБраузер сам чіпляє cookieВідкриває сторінкуПрихована форма шле POST /transferPOST /transfer + сесійна cookie200 OK, переказ виконаноbank.example.comБраузерЧужий сайтКористувачbank.example.comБраузерЧужий сайтКористувачБраузер сам чіпляє cookieВідкриває сторінкуПрихована форма шле POST /transferPOST /transfer + сесійна cookie200 OK, переказ виконано

    Корінь проблеми: cookie доводить, що запит іде від браузера користувача, але не доводить, що його ініціювала справжня сторінка застосунку. Саме цей розрив закриває CSRF-токен (synchronizer token pattern):

    1. Сервер генерує непередбачуваний токен, прив’язаний до сесії, і вкладає його у видану сторінку — у прихованому полі форми або в <meta>-тег.
    2. Клієнт зобов’язаний повернути цей токен разом із небезпечною дією — у полі форми або в заголовку на кшталт X-CSRF-Token.
    3. Сервер звіряє надісланий токен зі збереженим для цієї сесії; не збіглося — 403.

    Захист працює завдяки Same-Origin Policy: чужий сайт не може прочитати вміст сторінки застосунку, а отже не знає токена й не може його підставити. CSRF-токен доповнює SameSite-cookie, а не замінює її — це два різні рубежі.

    Для AQA це часте джерело «непояснимих» 403. Коли тест б’є по ендпоінту з мутацією напряму (в обхід форми), він мусить спершу дістати CSRF-токен і передати його в заголовку:

    // Витягуємо CSRF-токен зі сторінки й шлемо його в мутаційному запиті
    const csrf = await page.getAttribute('meta[name="csrf-token"]', 'content');
    await page.request.post('/api/projects', {
      headers: { 'X-CSRF-Token': csrf },
      data: { title: 'New project' },
    });

    Важлива деталь: чисто token-based API (без cookie, з Authorization: Bearer) до CSRF зазвичай не вразливе, бо токен не підставляється автоматично — його треба свідомо додати в заголовок, а чужий сайт цього зробити не може. CSRF — насамперед проблема автентифікації через cookie.

    Token-based авторизація

    Альтернатива сесіям, що домінує в API та SPA. Замість тримати стан у себе, сервер запаковує потрібний контекст у токен і віддає клієнту. Клієнт зберігає токен сам і додає його до кожного запиту — зазвичай у заголовку Authorization, а не в cookie.

    Головна відмінність від сесій — сервер не тримає стан (stateless). Отримавши токен, сервер перевіряє його підпис і читає вміст, нічого не шукаючи у себе в базі. Це чудово масштабується: будь-яка машина в кластері перевірить токен самостійно.

    Зворотний бік тієї ж медалі: токен важко відкликати достроково. Поки він не протермінувався, він валідний — навіть якщо користувача вже заблокували. Це компенсують коротким часом життя токена (див. розділ про refresh/access нижче).

    КритерійSession-basedToken-based
    Де станНа серверіУ токені (в клієнта)
    МасштабуванняПотрібне спільне сховище сесійКожен вузол самодостатній
    ВідкликанняМиттєве (видалити запис)Складне (чекати закінчення / чорні списки)
    Типове сховище в клієнтаCookie (автоматично)localStorage / пам’ять + заголовок вручну
    Типове застосуванняКласичні server-rendered застосункиSPA, мобільні клієнти, API

    JWT і його три частини

    Найпоширеніший формат токена — JWT (JSON Web Token), специфікований у RFC 7519. Це рядок із трьох частин, розділених крапками:

    <header>.<payload>.<signature>
    

    Кожну частину закодовано в base64url (варіант base64, де + і / замінено на - і _, а хвостові = відкинуто — щоб рядок був безпечний для URL). Класичний зразок токена з jwt.io:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    

    1. Header (заголовок). Метадані токена — алгоритм підпису та тип:

    {
      "alg": "HS256",
      "typ": "JWT"
    }

    2. Payload (корисне навантаження). Твердження (claims) про користувача. Частина полів стандартизована (registered claims):

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    ClaimЗначення
    issвидавець (issuer)
    subсуб’єкт — зазвичай ID користувача
    audаудиторія (audience) — для кого токен
    expчас закінчення (expiration), Unix-час у секундах
    iatчас видачі (issued at)
    nbfне дійсний раніше (not before)
    jtiунікальний ID токена

    3. Signature (підпис). Ось що робить токен захищеним. Підпис обчислюється над base64url(header) + "." + base64url(payload) за допомогою секрету (для HMAC, напр. HS256) або приватного ключа (для RSA/ECDSA, напр. RS256). Сервер перевіряє підпис і так переконується, що payload не підмінили.

    Критично важлива річ для AQA: header і payload не зашифровані, а лише закодовані. Будь-хто, у кого є токен, прочитає payload звичайним base64url-декодером. JWT захищає від підробки (зміниш payload — зламається підпис), але не від читання. Тому в payload не кладуть паролі чи секрети. У тестах це навпаки зручно: з payload можна дістати exp і перевірити, чи не протух токен, ще до запиту.

    // Декодуємо payload JWT без жодної бібліотеки
    function decodeJwtPayload(token) {
      const payload = token.split('.')[1];
      const json = Buffer.from(payload, 'base64url').toString('utf8');
      return JSON.parse(json);
    }
    
    const claims = decodeJwtPayload(token);
    const expiresAt = new Date(claims.exp * 1000); // exp у секундах

    Де зберігати JWT і які ризики

    Класичне питання без ідеального рішення — кожен варіант має свій вектор атаки, і вибір впливає на те, як тест взагалі дістане токен.

    СховищеПлюсРизикДоступність із тесту
    localStorageПросто, переживає перезавантаженняВразливе до XSS: будь-який чужий скрипт прочитає токенЛегко читається/пишеться через page.evaluate
    sessionStorageТе саме, але живе лише в межах вкладкиТой самий XSSТак само доступне
    Cookie з HttpOnlyJS не має доступу — XSS не вкрадеВразливе до CSRF (шлеться автоматично)Недоступне для JS, лише через контекст браузера
    Пам’ять (JS-змінна)XSS-вікно вужчеВтрачається при F5, потрібен refresh-механізмНе персистується — тест мусить логінитись щоразу

    Спрощено: localStorage вразливий до XSS, HttpOnly-cookie — до CSRF. XSS означає, що зловмисник виконав свій JS на твоїй сторінці й прочитав усе, до чого дотягнеться JavaScript, — а до HttpOnly-cookie він не дотягнеться. CSRF, як ми бачили вище, — навпаки: браузер сам чіпляє cookie до підробленого крос-сайтового запиту. Тому найзахищеніша схема часто така: короткий access-токен у пам’яті, довгий refresh-токен у HttpOnly-cookie.

    Для автоматизації це прямо визначає підхід: токен у localStorage тест підставляє через page.evaluate/addInitScript; HttpOnly-cookie — лише через context.addCookies, бо зі сторінки JS до неї не має доступу.

    Схема Bearer

    Коли токен передають у заголовку, використовують схему Bearer (RFC 6750). «Bearer» — «пред’явник»: хто пред’явив токен, той і має доступ, жодного додаткового доказу не треба. Тому токен = ключ від квартири, і поводитися з ним слід відповідно.

    GET /api/projects HTTP/1.1
    Host: api.example.com
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxw...

    Формат простий: слово Bearer, пробіл, токен. У Playwright це задають один раз на весь контекст, щоб не дублювати в кожному запиті:

    const context = await request.newContext({
      extraHTTPHeaders: {
        Authorization: `Bearer ${token}`,
      },
    });

    Типова помилка, що дає 401: забути слово Bearer або поставити зайвий пробіл. Якщо API віддає 401 на явно валідному токені — перше, що перевіряють, це точний рядок заголовка.

    Basic Auth і як формується заголовок

    Basic Authentication (RFC 7617) — найпростіша схема HTTP-автентифікації. Логін і пароль об’єднують через двокрапку, кодують у base64 і кладуть у той самий заголовок Authorization зі схемою Basic:

    Authorization: Basic base64(username + ":" + password)
    

    Канонічний приклад із RFC — користувач Aladdin, пароль open sesame:

    printf '%s' 'Aladdin:open sesame' | base64
    # QWxhZGRpbjpvcGVuIHNlc2FtZQ==
    GET /secret HTTP/1.1
    Host: example.com
    Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

    Найважливіше усвідомити: base64 — це кодування, а не шифрування. Його тривіально розкодувати назад. Пароль тут фактично передається відкритим текстом, просто в іншому вигляді, тому Basic Auth безпечний лише поверх HTTPS. Якщо доступ без заголовка, сервер відповідає 401 із WWW-Authenticate: Basic realm="...", і браузер показує рідний діалог логіну.

    Для тестів це часом дуже зручно: закриті staging-стенди нерідко захищені саме Basic Auth. У Playwright креденшали передають на рівні контексту:

    // Playwright: httpCredentials на рівні контексту
    const context = await browser.newContext({
      httpCredentials: { username: 'staging', password: 's3cret' },
    });

    API key проти session token

    Обидва — рядки, які кладуть у запит, тому їх плутають, хоча призначення різне.

    КритерійAPI keySession/user token
    Кого ідентифікуєЗастосунок/інтеграцію/проєктКонкретного користувача
    Час життяДовгий, часто безстроковийКороткий, прив’язаний до сесії
    ПраваЗазвичай широкі, фіксованіУспадковують роль користувача
    Де живеУ конфізі сервісу, CI-секретахУ браузері користувача
    Як передаєтьсяAuthorization, кастомний заголовок (напр. X-API-Key) або query-параметрAuthorization: Bearer або cookie

    API key відповідає на «який сервіс стукає», session token — на «який користувач». Для AQA це означає різні набори тестів: API-ключем зазвичай ганяють інтеграційні/бекенд-сценарії від імені проєкту, а user-токеном перевіряють поведінку під конкретною роллю (права, видимість даних). Плутати їх у тесті небезпечно: ключ проєкту може мати ширші права, ніж реальний користувач, і тест «позеленіє» там, де жива людина отримала б 403.

    OAuth: вхід через Google/GitHub

    OAuth 2.0 (RFC 6749) — протокол делегованого доступу. Він відповідає на питання: як дати застосунку A доступ до частини даних у сервісі B, не віддаючи A твій пароль від B. Саме на ньому побудований «Увійти через Google/GitHub».

    Оглядово потік авторизаційного коду (authorization code flow) виглядає так:

    1. Користувач тисне «Увійти через Google» у застосунку (client).
    2. Застосунок перенаправляє браузер на сторінку Google (authorization server).
    3. Користувач логіниться в Google і погоджується поділитися даними (consent).
    4. Google перенаправляє браузер назад на застосунок із одноразовим кодом (authorization code) у URL.
    5. Бекенд застосунку обмінює цей код на access-токен, звертаючись до Google уже напряму (сервер-до-сервера); для конфіденційних клієнтів обмін підтверджують секретом клієнта.
    6. Застосунок отримує токен і/або дані профілю.
    GoogleЗастосунок (бекенд)БраузерGoogleЗастосунок (бекенд)БраузерТисне «Увійти через Google»Редірект на GoogleЛогін і згода (consent)Редірект з authorization codeПовертає codeОбмін code на токен (сервер-до-сервера)Access-токен і профільGoogleЗастосунок (бекенд)БраузерGoogleЗастосунок (бекенд)БраузерТисне «Увійти через Google»Редірект на GoogleЛогін і згода (consent)Редірект з authorization codeПовертає codeОбмін code на токен (сервер-до-сервера)Access-токен і профіль

    Ключова ідея: пароль від Google ніколи не потрапляє до застосунку. Застосунок бачить лише код, а потім токен з обмеженим набором дозволів (scopes) — наприклад, «читати email і ім’я».

    Термінологічно варто розрізняти OAuth 2.0 (авторизація, доступ до ресурсів) і OpenID Connect — надбудову над OAuth для власне автентифікації (доведення особи). «Вхід через Google» технічно спирається на OpenID Connect.

    Сучасні реалізації для публічних клієнтів (SPA, мобільні), у яких немає безпечного місця для секрету, додають PKCE (RFC 7636) — механізм, що захищає обмін коду від перехоплення. Для AQA важливо, що цей потік багатокроковий, з редіректами між доменами, зовнішнім UI Google/GitHub і залежністю від їхньої доступності. Це прямо веде до наступних двох тем.

    Refresh token проти access token

    Токен-based авторизація балансує між безпекою і зручністю через пару токенів.

    Access token — короткоживучий (хвилини–години). Ним підписують кожен запит до API. Якщо його вкрадуть, вікно зловживання мале, бо він швидко протухне.

    Refresh token — довгоживучий (дні–тижні). Ним нічого не запитують у ресурсів; його єдина роль — отримати новий access-токен, коли старий закінчився, без повторного вводу пароля.

    POST /oauth/token HTTP/1.1
    Host: api.example.com
    Content-Type: application/json
    
    {"grant_type": "refresh_token", "refresh_token": "def50200a1b2..."}
    {
      "access_token": "eyJhbGciOi...",
      "token_type": "Bearer",
      "expires_in": 900,
      "refresh_token": "def50200c3d4..."
    }

    Логіка розподілу така: access-токен часто «світиться» в заголовках, тож його роблять коротким; refresh-токен цінніший, тож його ховають надійніше (часто в HttpOnly-cookie) і використовують рідко. Це компроміс: не змушувати користувача логінитись щогодини, але й не давати одному вкраденому токену довгого життя.

    Що робити, коли токен протермінувався під час прогону

    Це один із найпідступніших джерел флаку (flakiness): тест стабільно зелений, а вночі в CI раптом падає з 401 — бо токен, згенерований на старті прогону, дожив своє посеред довгої сюїти.

    Стратегії, від найгіршої до найкращої:

    1. Ігнорувати — сподіватись, що прогін коротший за час життя токена. Крихко: варто додати тестів або сповільнитись CI — і воно посиплеться.
    2. Логінитись у кожному тесті — надійно щодо токена, але повільно й навантажує сервер автентифікації.
    3. Перевіряти exp перед використанням — прочитати payload, і якщо до закінчення лишилось менше запасу (напр. 60 с), оновити токен наперед. Уникає гонки на межі.
    4. Ловити 401 і оновлювати з ретраєм — обгорнути транспортний шар так, щоб на 401 він тихо викликав refresh і повторив запит один раз.
    async function apiRequest(path, ctx) {
      let res = await fetch(path, { headers: authHeader(ctx.accessToken) });
      if (res.status === 401) {
        ctx.accessToken = await refresh(ctx.refreshToken); // оновили
        res = await fetch(path, { headers: authHeader(ctx.accessToken) }); // один ретрай
      }
      return res;
    }

    Ні

    Так

    Запит до API з access-токеном

    Відповідь 401?

    Готово

    Оновити токен через refresh

    Повторити запит один раз

    Ні

    Так

    Запит до API з access-токеном

    Відповідь 401?

    Готово

    Оновити токен через refresh

    Повторити запит один раз

    Головне правило: зберігай токен централізовано, а не копіюй його по тестах. Тоді оновлення в одному місці бачать усі. І пильнуй межу експірації в довгих сюїтах — саме там ховається «плаваючий» 401.

    Автоматизація OAuth-логіну і підводні камені

    Прогнати повний OAuth-потік через реальний UI Google чи GitHub у тесті — погана ідея, і ось чому:

    • Захист від ботів. Google/GitHub цілеспрямовано детектують автоматизацію: капчі, перевірка «підозрілого входу», блокування headless-браузерів. Твій тест — саме те, що вони блокують.
    • Зовнішня залежність. Ти ставиш зелений колір своєї сюїти в залежність від доступності й незмінності чужого UI, на який не маєш впливу.
    • Крос-доменні редіректи. Потік стрибає між доменами; cookie з SameSite і popup-вікна легко ламають сценарій.
    • 2FA на тестовому Google-акаунті. Реальний акаунт майже завжди тягне за собою другий фактор (див. нижче).

    Практичні обхідні шляхи:

    1. Не тестувати чужого провайдера. OAuth Google — це відповідальність Google. Тобі треба переконатися, що твій застосунок правильно обробляє повернутий код/токен. Тому провайдера мокають: перехоплюють редірект і підставляють заздалегідь відомий код або токен.

    2. Мок на рівні мережі. У Playwright перехоплюють callback-запит і повертають підготовану відповідь:

      await page.route('**/oauth/callback*', route =>
        route.fulfill({ status: 200, body: JSON.stringify({ token: TEST_TOKEN }) })
      );
    3. Тестовий IdP. Підняти власний OpenID-провайдер у тестовому середовищі (напр. локальний mock-сервер), який видає передбачувані токени без капч і 2FA.

    4. Обійти UI взагалі. Якщо мета — перевірити застосунок після входу, отримати сесію через бекенд-ендпоінт чи прямий обмін токена, а UI-логін лишити для одного окремого smoke-тесту.

    2FA/MFA і чому ускладнює тести

    Двофакторна / багатофакторна автентифікація (2FA/MFA) вимагає другого доказу особи, крім пароля: код із SMS, код із застосунку-автентифікатора (TOTP), пуш-підтвердження, апаратний ключ. Для безпеки — чудово; для автоматизації — стіна.

    Проблеми випливають із самої суті другого фактора:

    • SMS/пуш приходять на пристрій, якого в CI немає. Автоматизувати їх приймання дорого і крихко.
    • Апаратні ключі (WebAuthn/FIDO) фізично неможливо емулювати штатним браузером без спеціальних віртуальних автентифікаторів.
    • Одноразовість. Другий фактор навмисне непередбачуваний і короткоживучий — тобто протилежність тому, що потрібно стабільному тесту.

    Практично найкраще піддається автоматизації TOTP (Time-based One-Time Password) — саме на ньому працюють Google Authenticator та подібні. Секрет (shared secret) відомий обом сторонам, а код — детермінована функція від секрету й поточного часу. Якщо тестовий акаунт налаштований на TOTP і секрет доступний тесту, код можна порахувати:

    // otplib генерує той самий код, що й застосунок-автентифікатор
    const { authenticator } = require('otplib');
    const code = authenticator.generate(process.env.TOTP_SECRET);

    TOTP стандартизовано в RFC 6238 (розширення HOTP, RFC 4226). За замовчуванням крок — 30 секунд, а код — 6 цифр. Конкретний застосунок, однак, може налаштувати інший крок, довжину коду чи геш-алгоритм — а згенерований код прямо залежить від цих параметрів, тож для точного збігу їх треба знати наперед.

    Але навіть із TOTP найздоровіший підхід у більшості сюїт — вимкнути 2FA для тестових акаунтів або мати окремий шлях входу для тестового середовища. Другий фактор перевіряють кількома спеціальними тестами, а не тягнуть його крізь кожен сценарій, де він лише заважає.

    Чому API-логін і підстановка стану надійніші за UI-логін

    Логінитись через форму в кожному тесті — повільно, крихко й дублює перевірку, якій місце в одному тесті. Замість цього стан автентифікації здобувають один раз через API і підставляють у браузер напряму.

    АспектUI-логін у кожному тестіAPI-логін + підстановка стану
    ШвидкістьПовільно (рендер, кліки, редіректи)Швидко (один HTTP-запит)
    СтабільністьЛамається зі зміною форми, локаторів, капчіНе залежить від UI логіну
    ІзоляціяКожен тест повторює той самий шляхСтан готується окремо, тести чисті
    2FA/OAuthВпирається в другий фактор і провайдераОбходиться на рівні API
    Що перевіряєДомішує «а чи працює логін» до всьогоПеревіряє лише свою фічу

    Ключова причина глибша за швидкість: логін і цільова фіча — різні вимоги, і змішувати їх у перевірці не можна. Якщо тест кабінету щоразу проходить форму входу, то падіння логіну «фарбує в червоне» тест кабінету, хоча кабінет ні до чого. Один тест перевіряє одну вимогу; форму логіну перевіряє окремий тест логіну, решта отримує стан в обхід.

    Технічно це роблять так. Спершу — один раз — отримують стан:

    // global-setup: логін через API, збереження стану
    const context = await request.newContext();
    const res = await context.post('https://app.example.com/api/login', {
      data: { email: USER, password: PASS },
    });
    const token = (await res.json()).token;
    // зберегти cookie + localStorage контексту у файл
    await context.storageState({ path: 'state.json' });

    Далі кожен тест стартує вже залогіненим, підвантаживши цей стан:

    // у кожному тесті — старт із готовим станом, без форми входу
    const context = await browser.newContext({ storageState: 'state.json' });

    Якщо токен живе в localStorage, його підставляють скриптом ще до завантаження сторінки:

    await context.addInitScript(token => {
      window.localStorage.setItem('access_token', token);
    }, TOKEN);

    Якщо ж стан — це HttpOnly-cookie, JS до неї не дотягнеться, тож підставляють на рівні контексту:

    await context.addCookies([{
      name: 'session_id', value: SESSION,
      domain: 'app.example.com', path: '/', httpOnly: true, secure: true,
    }]);

    Саме тут сходиться все з попередніх підрозділів: щоб грамотно підставити стан, треба знати, у якій формі він живе (cookie чи токен), де саме (localStorage, HttpOnly-cookie, пам’ять) і як передається (Bearer, cookie, X-API-Key). А якщо застосунок захищений CSRF-токеном, підстановка сесії ще й потребує витягти цей токен зі сторінки перед першою мутацією — інакше замість зеленого тесту отримаєш 403. Розуміння моделі автентифікації тут не абстрактна теорія, а прямий інструмент, що робить сюїту швидкою, стабільною й чесно сфокусованою на тому, що кожен тест насправді має перевіряти.