git diff origin/develop -- actions/authentication.test.jse67067aa7) ведут на форк MaurerAnton/projectforge (ветка draft43npm). Ссылки на старый код (SHA 9ed5fbe0f) — на основной репозиторий micromata/projectforge (develop).
-import fetchMock from 'fetch-mock/es5/client'-import cookies from 'react-cookies'Что было: Библиотека для чтения/записи кук в браузере react-cookies@0.1.1 (добавлена 2019-03-09, package.json develop [source on GitHub]). Использовалась в старых тестах для проверки KEEP_SIGNED_IN (строка 94, строка 132, строка 183).
Почему удалён: Куки — HTTP-only, JavaScript их не читает (а в тесте читали cookies.loadAll(), который вызывает cookie.parse(document.cookie) — это реальные куки браузера, не мок). fetch-mock кладёт Set-Cookie в заголовки Response, но в document.cookie они не попадают. Значит cookies.loadAll() всегда возвращает {} — пустоту. Тест проходил не потому что кук не было, а потому что fetch-mock и react-cookies живут в разных мирах. react-cookies больше нет в зависимостях.
+import { vi } from 'vitest'Что стало: Vitest'овый API для моков. Аналог jest.fn() [Jest docs], но из самого Vitest [vitest.dev] (версия ^4.1.5 [package.json] ⚡).
Основные методы, используемые в тесте:
vi.fn() — создаёт мок-функцию (подмена global.fetch) — L49, L78, L95, L118, L143fn.mockResolvedValueOnce(value) — следующий вызов возвращает Promise.resolve(value) (каскад из двух) — L50, L53fn.mockResolvedValue(value) — все вызовы возвращают Promise.resolve(value) — L79, L119, L144fn.mockRejectedValue(error) — вызов бросает исключение (сеть упала) — L96vi.restoreAllMocks() — сброс всех моков между тестами — L42, L110-import thunk … +import { thunk }Что изменилось: Импорт redux-thunk с дефолтного (import thunk from) на именованный (import { thunk } from).
В redux-thunk версии 3.x (^3.1.0 [source: export const thunk]) экспорт по умолчанию удалён — теперь нужен именованный { thunk }.
configureMockStore([thunk]) — middleware, который позволяет диспатчить функции (thunk'и) вместо простых объектов. Без него store.dispatch(login(...)) не сработает.
Удалены из импорта:
authentication.js этой функции нет — она была переименована в loadUserStatus (строка 30, переименован 2019-03-18).describe('logout'). Функция logout не существует в текущем authentication.js [исходник] (удалён 2019-07-13).authentication.js [только 3 типа] (удалён 2019-07-13).Добавлены в импорт:
loadSessionIfAvailable. Реальная функция в authentication.js:30.authentication.js (loadSessionIfAvailable, logout, USER_LOGOUT, userLogout, storeLoginSession). Тест не проходил 7 лет — символы были удалены из исходника (userLogout/logout удалены Jul 2019, loadSessionIfAvailable переименован Mar 2019), но тест не обновили.
mockStore вынесен на уровень модуля — был создан 3 раза (внутри login, logout, check session), теперь один раз в начале файла. [redux-mock-store]
Object.freeze() — [MDN]. Старый тест замораживал username и password (ни к чему — примитивы неизменяемы). В новом файле Object.freeze используется только на объектах состояния в reducer-тесте, где это действительно важно (защита от мутации в pure function).
afterEach(() => fetchMock.restore()) → beforeEach(() => vi.restoreAllMocks()) — замена специфичного для fetch-mock вызова на универсальный Vitest-овый vi.restoreAllMocks(). Сброс перенесён с «после» на «до» — гарантия что моки не перетекают между тестами.
Три теста action creator'ов, сгруппированные в describe('action creators').
| Тест | Старый | Новый | Разница |
|---|---|---|---|
userLoginBegin |
11 строк, переменная expectedAction | 3 строки, inline | Только стиль. Логика не изменилась. |
userLoginSuccess |
Вызов без аргументов: userLoginSuccess(). Ожидание: { type: USER_LOGIN_SUCCESS } [строка 45] |
Вызов с 4 аргументами. Ожидание: полный payload с user, version, buildTimestamp, alertMessage [новый]⚡ | Исправление: userLoginSuccess принимает 4 обязательных параметра (authentication.js:11). Старый тест проверял неполное поведение. |
userLoginFailure |
10 строк, описание с опечаткой «mark the login as success» [строка 49] | 5 строк, корректное описание | Исправлена опечатка. Логика без изменений. |
| Строка 40 — SUCCESS ✓ | Строка 49 — FAILURE, но название «success» ✗ |
|---|---|
it('should create an action to mark
the login as success', () => {
const expectedAction = {
type: USER_LOGIN_SUCCESS
};
expect(userLoginSuccess())
.toEqual(expectedAction);
}); |
it('should create an action to mark
the login as success', () => {
const expectedAction = {
type: USER_LOGIN_FAILURE,
payload: { error: 'Some uncool...' }
};
expect(userLoginFailure('Some...'))
.toEqual(expectedAction);
}); |
Так и висело 7 лет — название врало о содержимом. В новой версии имена приведены в соответствие.
Что такое keepSignedIn / stayLoggedIn: Флажок «оставаться в системе» — чекбокс на форме логина. Если пользователь его отмечает, происходит следующая цепочка:
login(username, password, keepSignedIn) (authentication.js:65) → тело POST: { stayLoggedIn: keepSignedIn } (строка 77)LoginPageRest.kt принимает PostData<LoginData> (строка 96), поле stayLoggedIn в LoginData.kt:34LoginService.authenticate() проверяет if (loginData.stayLoggedIn == true) (LoginService.kt:175), генерирует токен STAY_LOGGED_IN_KEY (UserTokenType.kt:31) и вызывает addStayLoggedInCookie() (строка 177-178)CookieService ставит куку с именем "stayLoggedIn" (CookieService.kt:201) и сроком 30 дней (строка 200)JSESSIONID истекла, LoginService.checkStayLoggedIn() (LoginService.kt:250) читает куку через CookieService (CookieService.kt:65-75) — сервер авторизует пользователя без пароляСтарый тест пытался проверить этот флажок через cookies.loadAll() — но кука не попадает в document.cookie при mock-окружении (см. секцию 2).
Проблемы старого теста:
fetchMock.mock(matcher, response) — громоздкий API: функция-матчер сама парсит body запроса и проверяет URL. [строка 62-80] [fetch-mock source]headers: { 'Set-Cookie': '...' } — симулирует установку сессионной куки, но fetch-mock не умеет реально их устанавливать [строка 77]. Почему: fetch-mock возвращает объект Response с заголовками — но не трогает document.cookie. А react-cookies.loadAll() читает именно document.cookie = cookie.parse(document.cookie). Цепочка: fetch-mock → Set-Cookie в заголовках → document.cookie не обновлён → loadAll() возвращает {} → тест проходит, но проверяет пустоту, а не реальное поведение..catch({ throws: new Error('mock failed') }) — если запрос не совпал ни с одним матчером — бросить ошибку. Костыль, который маскирует настоящие проблемы [строка 80].login() диспатчит BEGIN, потом вызывает loadUserStatus()(dispatch), который диспатчит ещё один BEGIN и потом SUCCESS. Итого должно быть 3 action'а, а не 2. [authentication.js:65-84] — login → loadUserStatus()(dispatch) внутри .then().cookies.loadAll() → toEqual({}) — тестирует браузерную библиотеку react-cookies, а не логику приложения [строка 94].Тест на неправильном слое абстракции. Старый тест пытался проверить поведение кук — но куки управляются браузером, а не fetch():
Два несовместимых мира моков. fetch-mock и react-cookies работают на принципиально разных уровнях браузерного стека и никак не связаны в тестовом окружении. fetch-mock подменяет HTTP-слой (сеть), а react-cookies читает DOM-слой (document.cookie). В реальном браузере между ними стоит cookie jar — компонент браузера, который обрабатывает Set-Cookie из HTTP-ответа и записывает в document.cookie. В jsdom с замоканным fetch этого слоя нет — и тест проверял иллюзию.
Тест проходил по совпадению. Браузер не обрабатывал Set-Cookie → document.cookie пуст → cookies.loadAll() возвращал {}. Но тест и ожидал {}! Совпадение двух нулей — пустота совпала с пустотой — создавало иллюзию работающего теста.
Граница ответственности. Юнит-тест фронтенда должен проверять какие Redux-действия диспатчатся при разных ответах fetch, а не как браузер обрабатывает HTTP-заголовки. Правильный тест: «fetch вернул 200 → диспатчнули [BEGIN, BEGIN, SUCCESS]». Неправильный: «fetch вернул Set-Cookie → проверим что кука записалась в document.cookie» — это тест браузера, не кода приложения.
Критический баг в оригинале:
if (url !== '/√/login' || options.method !== 'POST') — URL написан как /√/login (символ квадратного корня U+221A), хотя правильный URL — /rsPublic/login. [строка 103]
Этот тест никогда не срабатывал — запрос POST /rsPublic/login не совпадал с матчером /√/login, поэтому падал в .catch({ throws: new Error('mock failed') }).
Однако тест мог проходить, если ошибка подавлялась в цепочке .then().catch() — catchError в authentication.js:84 диспатчит FAILURE, а не пробрасывает исключение.
Проблемы:
fetchMock.mock('/rsPublic/login', 401) — старый API, просто URL и статус [строка 141]. Возвращал { status: 401 } без ok: false, а handleHTTPErrors проверяет response.ok [rest.js:32].payload: { error: 'Unauthorized' } — в реальном коде handleHTTPErrors кидает Error('Fetch failed: Error 401') [rest.js:34]. Тест ожидает неверное сообщение.async/await — замена .then() на async/await. Короче, читаемее, ошибки не проглатываются.
global.fetch = vi.fn().mockResolvedValueOnce() — два вызова fetch подменяются по очереди:
POST /rsPublic/login — пустой {}GET /rs/userStatus (из loadUserStatus() внутри login()) — с userData, systemData, alertMessage[BEGIN, BEGIN, SUCCESS] — ИСПРАВЛЕНО. Три action'а:
BEGIN — от login() (authentication.js:65)BEGIN — от loadUserStatus() (authentication.js:30)SUCCESS — от loadUserStatus() (authentication.js:42)Старый тест ожидал 2 ([BEGIN, SUCCESS]) — не хватало второго BEGIN.
Неверный пароль (401):
mockResolvedValue (без Once) — все вызовы возвращают 401. loadUserStatus() не вызывается (login упал раньше).error: 'Fetch failed: Error 401' — потому что handleHTTPErrors кидает Error('Fetch failed: Error ${status}').'Unauthorized' — неверно.Сеть упала — НОВЫЙ ТЕСТ (не было в старом файле):
mockRejectedValue(new Error('Network error')) — [vitest docs]. Симулирует ошибку сети/DNS. fetch не возвращает ответ, а бросает исключение. login() → fetch(...) → сеть упала → .catch(catchError(dispatch)) → dispatch(USER_LOGIN_FAILURE('Network error')). [authentication.js:84]
Удалён целиком (2 теста, ~30 строк).
Причины:
userLogout() возвращает { type: USER_LOGOUT } — тривиально, тот же уровень что и action creator. USER_LOGOUT не экспортится из authentication.js [только 3 типа].logout() диспатчит один action — нет асинхронного кода.cookies.loadAll() (react-cookies), который удалён.USER_LOGIN_BEGIN (сброс состояния).Ключевой TODO: 7 лет.
// TODO: ADD AUTHENTICATION TEST ENDPOINT [строка 211] — добавлен 2019-03-17, висел 7 лет до PR (удалён в 7e78f3741). Старый тест loadSessionIfAvailable возвращал только статус 200, без JSON body. response.json() ломался. Тест никогда не проходил.
Удалён «creates no action at all» — проверял что loadSessionIfAvailable() возвращает null без мока. В текущем коде loadUserStatus() всегда делает fetch.
describe('loadUserStatus') — валидная сессия (строки 108–140)Правильный JSON ответ: json: () => Promise.resolve({ userData, systemData, alertMessage }) — структура соответствует тому что ожидает loadUserStatus() в authentication.js:42.
alertMessage: 'Some alert' — проверяем проброс системного сообщения (в login-тесте было undefined — разные ветки).
describe('loadUserStatus') — сессия истекла (строки 142–157)Новый тест — не было в старом файле.
payload: { error: undefined } — особенность loadUserStatus(): в catch-обработчике вызывается catchError(dispatch)({ message: undefined }). catchError = (dispatch) => (error) => dispatch(userLoginFailure(error.message)) — error.message это undefined, потому что передан объект { message: undefined }.
Проверка через actions[0] и actions[1] (не toEqual всего массива) — потому что loadUserStatus() при ошибке делает window.location.href = (редирект на /login), который в jsdom может не отработать.
(dispatch, getState) — может делать fetch и диспатчить несколько action'ов. login() и loadUserStatus() — это thunk'и: они возвращают function(dispatch) { ... }. Без middleware redux-thunk Redux отклонил бы их с ошибкой. [документация]userLoginBegin()) возвращает объект { type, payload } — диспатчится синхронно. Thunk (login(username, password)) возвращает функцию — middleware запускает её асинхронно, она делает fetch и диспатчит несколько action'ов. Это два разных слоя: creator'ы — синхронная фабрика объектов, thunk'и — асинхронный оркестратор.request.getSession(true). ProjectForge лишь управляет её жизненным циклом:
// Session Fixation: Change JSESSIONID after login
request.getSession(false)?.let { session ->
if (!session.isNew) { session.invalidate() }
}
val session = request.getSession(true) // ← новый JSESSIONID
session.setAttribute(SESSION_KEY_USER, userContext)fetch() шлёт куку через credentials: 'include' в authentication.js:37:fetch(getServiceURL('userStatus'), {
method: 'GET',
credentials: 'include', // ← отправляет JSESSIONID на сервер
})GET /rs/userStatus возвращает 401. Кука HTTP-only: JavaScript не может прочитать её через document.cookie. [Wikipedia: Session ID]
keepSignedIn в authentication.js:65 → отправляется как { stayLoggedIn: true } в теле POST (строка 77). Сервер генерирует токен STAY_LOGGED_IN_KEY и ставит куку с именем "stayLoggedIn" (CookieService.kt:201). KEEP_SIGNED_IN — старое имя константы из client-side кода, удалена в Mar 2019. Старый тест использовал именно его (cookies.save('KEEP_SIGNED_IN', ...)) — неверное имя + fetch-mock не устанавливает куки = тест проверял пустоту.HttpOnly. Браузер отправляет её на сервер, но JavaScript не имеет к ней доступа — document.cookie её не содержит. JSESSIONID — HTTP-only (защита от XSS-атак). Именно поэтому cookies.loadAll() (который читает document.cookie) никогда не видит сессионную куку. [MDN]jest.fn(). mockResolvedValueOnce(x) — следующий вызов вернёт Promise.resolve(x) (каскад). mockResolvedValue(x) — все вызовы вернут Promise.resolve(x). mockRejectedValue(e) — вызов бросит исключение (сеть упала). Разница Once / без Once критична: в тесте логина два вызова fetch подменяются по очереди. [vitest]process.env.NODE_ENV. import.meta.env.DEV — true в dev-режиме. import.meta.env.MODE — строка: 'development', 'production', или 'test'. Замена понадобилась потому что CRA-переменные process.env.NODE_ENV не существуют в Vite. В rest.js:10: DEV && MODE !== 'test' — dev-сервер, но не тестовый раннер. [vite docs]connect(mapStateToProps)(Component) — HOC-паттерн (до 2019), оборачивает компонент. useSelector(state => state.user) + useDispatch() — хуки (React 16.8+), проще, работают с TypeScript, tree-shakeable. loggedIn: boolean сменился на user: object|null именно из-за перехода на хуки: селектор state => state.authentication.user !== null читает конкретное поле вместо флага. [react-redux]GET /rs/userStatus с кукой (credentials: 'include'). Сервер проверяет сессию: если жива — возвращает { userData, systemData, alertMessage } (200). Если истекла — 401 → редирект на /react/public/login. Это первое что делает React при загрузке (см. ProjectForge.jsx): «кто я?» [authentication.js:30]| Аспект | Старый файл (237 строк) | Новый файл (157 строк) |
|---|---|---|
| Mock fetch | fetch-mock [строка 2] |
vi.fn() [vitest] |
| Куки | react-cookies [строка 3], тесты stayLoggedIn |
Удалены — браузер не тестируется |
| Async стиль | Promise .then() [MDN] |
async/await [MDN] |
| Структура describe | 3 блока: login, logout, check session |
3 блока: action creators, login, loadUserStatus |
| login валидный | 2: [BEGIN, SUCCESS] — неверно [строка 82] |
3: [BEGIN, BEGIN, SUCCESS] — верно [auth.js:62-84] |
| login неверные данные | 'Unauthorized' — баг URL /√/login [строка 103] |
'Fetch failed: Error 401' — правильный URL [rest.js:32-38] |
| Сеть упала | ❌ Нет теста | ✅ mockRejectedValue [строка 96] |
| loadUserStatus валидная сессия | TODO: 7 лет, не работал [строка 211] | ✅ Полноценный тест [строки 108-140] |
| loadUserStatus истекшая сессия | ❌ Нет теста | ✅ 401 → FAILURE [строки 142-157] |
| logout | 2 теста, символ не существует в исходнике [удалён Jul 2019] | Удалён — тривиальный action, покрыт USER_LOGIN_BEGIN |
⚡ — ссылка на код в ветке fix/vite-eslint-upgrade, не слитой в develop. Код может быть не доступен на GitHub до слияния PR.
Что было: Старый тест использовал
fetch-mock— внешнюю библиотеку для подменыfetch(добавлен 2019-03-15). Версияes5/clientбыла нужна для совместимости со старым Jest/CRA. [исходник develop]Что стало: Удалён. Три причины:
vi.fn()— не нужно ставить отдельный пакет.fetch-mockверсии^12.6.0[package.json develop] имеет API, несовместимый с современными fetch-стандартами и требует полифиллов [fetch-mock source on GitHub].fetch-mockотсутствует — на один пакет меньше, меньше уязвимостей.