1. user() в MockMvc
Когда мы пишем security-тесты, у нас почти всегда есть желание проверить правило вида «в личную зону можно только после аутентификации» или «в admin-зону можно только администратору». Звучит просто… ровно до момента, пока вы не понимаете, что тесту нужно как-то «стать» пользователем. Причём быстро, без реального логина и без танцев с паролями.
В реальном приложении пользователь проходит полноценный auth flow: фильтры, AuthenticationManager, провайдеры, проверка пароля, заполнение SecurityContext. Но в тестах на authorization мы часто хотим проверить другое: «Если пользователь уже аутентифицирован и у него роль EDITOR, то запрос в editor-зону проходит». В этом месте user() — как чит-код разработчика, но честный: он не ломает security, он позволяет быстро смоделировать входные данные для authorization-решения.
На проекте Secure Content Platform API это особенно полезно, потому что наша access matrix довольно «живая»: есть public endpoints (/api/public/**), личная зона (/api/me/**), editor endpoints (/api/editor/**) и admin endpoints (/api/admin/**). И тестировать это хочется так же системно, как и проектировали.
RequestPostProcessor и user()
Прежде чем писать код, полезно понять, где user() вообще «вставляется» в картину мира. В MockMvc запрос создаётся билдерами (get(...), post(...) и т.д.), а потом проходит через целую цепочку инфраструктуры, очень похожую на реальную обработку запроса в Spring MVC. И вот прямо между созданием запроса и запуском его через фильтры у нас есть точка, где можно этот запрос немного «докрутить».
Эта точка и называется RequestPostProcessor. Это объект, который получает MockHttpServletRequest, может добавить туда заголовки, атрибуты, и — главное для нас — может подготовить security-контекст так, будто запрос пришёл от конкретного пользователя. Ровно это и делает user() из spring-security-test.
Если нарисовать это в виде простой схемы, получится примерно так:
flowchart TD
A["MockMvc: build request"] --> B["RequestPostProcessor .with(user(...))"]
B --> C["SecurityFilterChain (реальная!)"]
C --> D["Controller / Handler"]
D --> E["Assertions (status, headers, body)"]
Ключевая мысль: user() не «обходит» SecurityFilterChain. Наоборот, он помогает подготовить запрос так, чтобы цепочка безопасности увидела его как запрос от аутентифицированного пользователя и приняла нормальное решение об авторизации.
2. Базовый сценарий: /api/me с user()
Начнём с самого прикладного кейса: endpoint /api/me по нашей модели доступа является личной зоной, а значит anonymous туда ходить не должен. В первых тестах мы уже проверяли 401 для anonymous. Теперь нам нужно проверить позитивный сценарий: обычный пользователь (USER) может открыть /api/me и получить 200 OK.
Предположим, что в тестовом классе у вас уже есть внедрённый MockMvc mockMvc (мы сделали это в прошлой лекции). Тогда самый «в лоб» вариант с user() выглядит так:
import org.junit.jupiter.api.Test;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void meIsOkForAuthenticatedUser() throws Exception {
mockMvc.perform(
// Собираем GET-запрос к личной зоне
get("/api/me")
// Подставляем в запрос аутентифицированного пользователя
.with(user("anna").roles("USER"))
)
// Проверяем, что правило доступа пропустило запрос
.andExpect(status().isOk()); // Ожидаем HTTP 200
}
Здесь полезно остановиться и проговорить, что именно происходит. Метод user("anna") создаёт тестового пользователя с именем anna, а roles("USER") добавляет ему роль пользователя. Дальше .with(...) применяет этот post-processor к одному конкретному запросу: другие запросы в этом же тесте или в соседних тестах от этого не «заражаются».
В результате security-цепочка видит: «да, пользователь аутентифицирован», затем применяет ваши правила доступа (в SecurityFilterChain и, если у вас включена method security, ещё и на уровне сервисов), и endpoint возвращает 200.
Если вы любите, чтобы тест читался как правило, то название meIsOkForAuthenticatedUser — уже половина успеха. Примерно так и должен звучать regression pack: не «test1», а «условие → ожидаемый результат».
3. Роли и префикс ROLE_ в тестах
Когда вы впервые встречаете Spring Security, кажется, что роль — это просто строка. Потом вы узнаёте, что роль внутри Spring часто превращается в authority с префиксом ROLE_. А потом в тестах вы начинаете гадать: писать ADMIN или ROLE_ADMIN? И вот здесь user().roles(...) пытается спасти вашу психику, но при одном условии: вы не будете ему мешать.
У roles(...) есть важное правило: роли указываются без префикса ROLE_. То есть правильно так: roles("ADMIN"), roles("EDITOR"), roles("USER"). Spring Security Test сам добавит префикс туда, где он нужен, и в SecurityContext окажется authority ROLE_ADMIN (или ROLE_EDITOR, ROLE_USER).
Если же написать roles("ROLE_ADMIN"), вы, скорее всего, получите authority ROLE_ROLE_ADMIN. Да, это не шутка, это прямой результат «префикс уже был, а мы ещё раз добавили». И потом вы сидите и думаете, почему hasRole("ADMIN") не работает. Спойлер: потому что у вас не ROLE_ADMIN, а ROLE_ROLE_ADMIN.
Чтобы закрепить логику, удобно держать в голове маленькую табличку:
| Как правило задано в security | Что оно ожидает в Authentication | Как удобнее задать в тесте через user() |
|---|---|---|
| hasRole("ADMIN") | authority ROLE_ADMIN | .roles("ADMIN") |
| hasRole("EDITOR") | authority ROLE_EDITOR | .roles("EDITOR") |
| hasAuthority("user:read") | authority user:read | .authorities(new SimpleGrantedAuthority("user:read")) |
Теперь применим это к проекту. Например, admin endpoint /api/admin/users должен быть доступен администратору, но запрещён обычному пользователю. Тест с user() сразу подчёркивает разницу между 401 и 403: пользователь есть, но прав не хватает.
import org.junit.jupiter.api.Test;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void adminUsersForbiddenForRegularUser() throws Exception {
mockMvc.perform(
// Пытаемся зайти в admin-зону под обычным пользователем
get("/api/admin/users").with(user("anna").roles("USER"))
)
// Пользователь аутентифицирован, но прав не хватает → 403
.andExpect(status().isForbidden()); // Ожидаем HTTP 403
}
Этот тест на самом деле делает очень важное дело: он фиксирует, что admin-зона закрыта не «на удачу», а строго. И если кто-то завтра переставит matcher’ы в SecurityFilterChain в неудачном порядке, тест довольно быстро вернёт вас в реальность.
4. Authorities в user()
Роли — удобные, человеческие и довольно грубые. В нашем проекте это USER, EDITOR, ADMIN. Но в реальном приложении (и в нашем учебном проекте ближе к финалу) правила доступа часто выражаются и через более точные permissions, то есть authorities вроде draft:publish, draft:review, user:manage. И тут возникает вопрос: как это моделировать в тесте?
Ответ простой: через authorities(...). Важно лишь помнить, что authority — это строка «как есть», без магии префиксов. Если security-правило ожидает draft:review, то в тесте нужно дать именно draft:review.
Ниже пример того, как это выглядит, если ваш endpoint (или метод сервиса) защищён именно authority:
import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void reviewQueueOkForDraftReviewAuthority() throws Exception {
mockMvc.perform(
// Идём в editor-endpoint, выдавая пользователю точечное permission
get("/api/editor/review-queue")
.with(user("ed")
// Authority задаётся «как есть», без ROLE_-магии
.authorities(new SimpleGrantedAuthority("draft:review")))
)
// Если конфигурация действительно завязана на authority, ожидаем 200
.andExpect(status().isOk()); // Ожидаем HTTP 200
}
Здесь есть тонкость, о которую часто спотыкаются новички. Если ваша реальная конфигурация защищает /api/editor/** через hasRole("EDITOR"), то такой тест может вернуть 403, потому что у пользователя нет ROLE_EDITOR. И это не «плохой тест», это сигнал: вы моделируете не те права, которые реально требуются.
Иногда вам нужны и роль, и permissions. В таком случае лучше не пытаться «склеить» .roles(...) и .authorities(...) в одну цепочку вызовов, потому что вы легко перезатрёте одно другим. Более надёжный подход — явно перечислить всё в authorities(...), включая роль как authority с префиксом:
import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void editorRoleAndPermissionWorkTogether() throws Exception {
mockMvc.perform(
get("/api/editor/review-queue")
.with(user("ed")
// Явно задаём и роль (как authority), и permission
.authorities(
new SimpleGrantedAuthority("ROLE_EDITOR"),
new SimpleGrantedAuthority("draft:review")))
)
.andExpect(status().isOk()); // Ожидаем HTTP 200
}
Да, тут мы руками написали ROLE_EDITOR, и это как раз тот случай, когда это уместно: мы работаем с authorities «как есть» и сами контролируем итоговый набор прав.
5. Хелперы для тестовых пользователей
Первые пару тестов с .with(user("anna").roles("USER")) обычно выглядят мило. На десятом тесте вы начинаете подозревать, что Анна вот-вот получит роль тимлида, потому что она везде. На двадцатом — вы понимаете, что копипаста пользователей превращает тесты в шум: они перестают быть «правилами доступа», а становятся «вёрсткой запроса».
Поэтому хороший стиль — вынести повторяющихся тестовых пользователей в маленькие helper-методы, которые возвращают RequestPostProcessor. Это сохраняет и явность, и читабельность, и при этом не заставляет вас писать 300 одинаковых строк.
Например, так:
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
private RequestPostProcessor regularUser() {
// Хелпер возвращает post-processor, который можно переиспользовать в тестах
return user("anna").roles("USER");
}
И тогда тест становится приятнее глазу:
import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void meIsOkForRegularUser() throws Exception {
mockMvc.perform(
// В тесте остаётся только суть правила доступа
get("/api/me").with(regularUser())
)
.andExpect(status().isOk()); // Ожидаем HTTP 200
}
Здесь важно не перегнуть палку. Наша цель — сделать тесты читаемыми, а не построить мини-фреймворк поверх MockMvc. Обычно 2–4 helper-метода (user, editor, admin) уже достаточно, чтобы regression pack перестал выглядеть как «скопируй и вставь».
6. Границы применения user()
Это самый важный момент лекции, потому что именно здесь рождаются честные ожидания от тестов. user() — это инструмент для моделирования уже аутентифицированного пользователя. Он не проверяет пароль, не ходит в БД за UserAccount, не запускает AuthenticationManager, не прогоняет DaoAuthenticationProvider. Если вы ждёте от него проверки логина — вы ждёте от микроволновки, что она научится варить борщ.
Зато user() отлично тестирует то, что нам нужно в базовом regression pack: правила авторизации и границы доступа. То есть: какие endpoints доступны anonymous, какие требуют аутентификацию, какие требуют ROLE_EDITOR или ROLE_ADMIN, какие проверяют authority. Это быстрые, дешёвые тесты, которые ловят регрессии в конфигурации или в правилах доступа.
Полезная ментальная модель такая: user() — это как если бы в тесте вы «вручили охраннику пропуск» и сказали: «Считай, что этот человек уже внутри системы, проверь только, пускаешь ли ты его в эту дверь». А проверка логина — это другая история: там вы проверяете, как человек вообще получил этот пропуск, кто его выдал, и не поддельный ли он.
В контексте Spring Security это различие соответствует двум вопросам: authentication и authorization. user() в первую очередь помогает тестировать authorization-слой: доступы, роли, permissions, 401/403-поведение. И это абсолютно нормально, потому что без такого слоя вы будете каждый раз “руками” перепроверять доступ через Postman и надеяться, что всё ещё работает.
7. Типичные ошибки при использовании user()
Ошибка №1: ожидание, что user() тестирует логин и пароль.
Очень легко начать думать, что раз мы «создали пользователя», то мы проверили аутентификацию. На самом деле user() лишь создаёт контекст, в котором запрос выглядит аутентифицированным. Это хорошо для проверки 403/200, но никак не проверяет ваш реальный UserDetailsService, PasswordEncoder и account states.
Ошибка №2: путаница с ROLE_ и запись roles("ROLE_ADMIN").
Это классика, достойная музейной витрины рядом с «почему у меня NullPointerException». В roles(...) вы пишете роль без префикса, иначе рискуете получить ROLE_ROLE_ADMIN и потом очень долго убеждать себя, что «Spring Security сломан». Он не сломан, он просто слишком честно выполняет вашу команду.
Ошибка №3: моделирование не тех прав, которые реально требуются конфигурацией.
Если endpoint защищён через hasRole("EDITOR"), а вы добавили только authority draft:review, вы получите 403. И это правильно. В таких случаях нужно или выдавать правильную роль через .roles("EDITOR"), или добавить ROLE_EDITOR в список authorities, если вы сознательно тестируете authority-based правила.
Ошибка №4: копипаста пользователей в каждом тесте до состояния “Анна как глобальная переменная”.
Когда .with(user("anna").roles("USER")) размазан по 30 тестам, они становятся шумными и плохо читаются. Вынесение в helper-методы делает regression pack гораздо более устойчивым: вы меняете имя пользователя или роль в одном месте и не ловите «снежную лавину» правок.
Ошибка №5: попытка проверить слишком много в одном тесте.
Иногда хочется одним тестом пройтись по public, me, editor и admin зонам, чтобы «быстрее». В итоге при падении такого теста непонятно, что именно сломалось. Гораздо надёжнее держать один тест на одно правило доступа: так regression pack будет точнее и проще в диагностике.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ