CodeGym /Courses /Spring Security /Modeling a user with user()

Modeling a user with user()

Spring Security
Level 26, Lesson 2
Available

1. user() in MockMvc

When we write security tests, we almost always want to verify a rule like “only after authentication can you access the personal area” or “only an administrator can access the admin area.” Sounds simple... right up until the moment you realize that the test somehow needs to “become” a user. Quickly, without a real login and without dancing around with passwords.

In a real application, a user goes through a full auth flow: filters, AuthenticationManager, providers, password verification, filling the SecurityContext. But in authorization tests, we often want to check something else: “If the user is already authenticated and has the EDITOR role, then the request to the editor area goes through.” At this point, user() is like a developer cheat code, but an honest one: it does not break security; it lets us quickly model the input data for the authorization decision.

On the Secure Content Platform API project, this is especially useful because our access matrix is pretty “alive”: there are public endpoints (/api/public/**), a personal area (/api/me/**), editor endpoints (/api/editor/**), and admin endpoints (/api/admin/**). And we want to test this as systematically as we designed it.

RequestPostProcessor and user()

Before writing code, it is useful to understand where user() actually “slots in” to the overall picture. In MockMvc, a request is created by builders (get(...), post(...), etc.), and then it goes through a whole chain of infrastructure, very similar to how Spring MVC handles a request in real life. And right here, between creating the request and running it through the filters, we have a point where we can tweak that request a little.

That point is called RequestPostProcessor. It is an object that receives a MockHttpServletRequest, can add headers and attributes to it, and — most importantly for us — can prepare the security context as if the request came from a specific user. That is exactly what user() from spring-security-test does.

If we draw it as a simple diagram, it looks roughly like this:

flowchart TD
    A["MockMvc: build request"] --> B["RequestPostProcessor .with(user(...))"]
    B --> C["SecurityFilterChain (real!)"]
    C --> D["Controller / Handler"]
    D --> E["Assertions (status, headers, body)"]

The key idea: user() does not “bypass” the SecurityFilterChain. On the contrary, it helps prepare the request so that the security chain sees it as a request from an authenticated user and makes a normal authorization decision.

2. Basic scenario: /api/me with user()

Let’s start with the most practical case: the /api/me endpoint in our access model is a personal area, which means anonymous users must not be able to access it. In the first tests, we already checked 401 for anonymous. Now we need to verify the positive scenario: a regular user (USER) can open /api/me and receive 200 OK.

Suppose that in your test class you already have an injected MockMvc mockMvc (we did that in the previous lecture). Then the most straightforward user() example looks like this:

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(
            // Build a GET request to the personal area
            get("/api/me")
                    // Plug an authenticated user into the request
                    .with(user("anna").roles("USER"))
    )
    // Verify that the access rule let the request through
    .andExpect(status().isOk()); // Expect HTTP 200
}

Here it is worth pausing and spelling out what exactly happens. The user("anna") method creates a test user with the name anna, and roles("USER") adds the user role. Then .with(...) applies this post-processor to one specific request: other requests in the same test or in neighboring tests are not “contaminated” by it.

As a result, the security chain sees: “yes, the user is authenticated,” then applies your access rules (in the SecurityFilterChain and, if method security is enabled, also at the service level), and the endpoint returns 200.

If you like tests that read like a rule, then the name meIsOkForAuthenticatedUser is already half the battle. That is how a regression pack should sound: not “test1,” but “condition → expected result.”

3. Roles and the ROLE_ prefix in tests

When you first meet Spring Security, it seems like a role is just a string. Then you learn that inside Spring, a role often turns into an authority with the ROLE_ prefix. And then in tests you start wondering: should you write ADMIN or ROLE_ADMIN? And here user().roles(...) tries to save your sanity, but with one condition: you do not get in its way.

roles(...) has an important rule: roles are specified without the prefix ROLE_. So the correct usage is: roles("ADMIN"), roles("EDITOR"), roles("USER"). Spring Security Test will add the prefix where needed, and the SecurityContext will contain the authority ROLE_ADMIN (or ROLE_EDITOR, ROLE_USER).

If you write roles("ROLE_ADMIN"), you will most likely get the authority ROLE_ROLE_ADMIN. Yes, that is not a joke; that is the direct result of “the prefix was already there, and we added it again.” And then you sit there wondering why hasRole("ADMIN") does not work. Spoiler: because you do not have ROLE_ADMIN, you have ROLE_ROLE_ADMIN.

To reinforce the logic, it is useful to keep a small table in mind:

How the rule is defined in security What it expects in Authentication How it is easier to set up in a test with 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"))

Now let’s apply this to the project. For example, the admin endpoint /api/admin/users should be available to an administrator, but forbidden to a regular user. A test with user() immediately highlights the difference between 401 and 403: the user is there, but the permissions are not enough.

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(
            // Try to enter the admin area as a regular user
            get("/api/admin/users").with(user("anna").roles("USER"))
    )
    // The user is authenticated, but the permissions are not enough → 403
    .andExpect(status().isForbidden()); // Expect HTTP 403
}

This test actually does something very important: it locks in the fact that the admin area is closed not “by luck,” but strictly. And if someone moves the matchers in SecurityFilterChain into an unfortunate order tomorrow, the test will bring you back to reality pretty quickly.

4. Authorities in user()

Roles are convenient, human-friendly, and fairly coarse. In our project, these are USER, EDITOR, and ADMIN. But in a real application (and in our educational project closer to the end), access rules are often expressed through more precise permissions, that is, authorities like draft:publish, draft:review, user:manage. And that raises the question: how do you model this in a test?

The answer is simple: through authorities(...). The only important thing is to remember that an authority is a string “as is,” without prefix magic. If the security rule expects draft:review, then the test must provide exactly draft:review.

Below is an example of what this looks like if your endpoint (or service method) is protected by an 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(
            // Go to the editor endpoint, giving the user a specific permission
            get("/api/editor/review-queue")
                    .with(user("ed")
                            // Authority is set "as is", without ROLE_ magic
                            .authorities(new SimpleGrantedAuthority("draft:review")))
    )
    // If the configuration really depends on the authority, expect 200
    .andExpect(status().isOk()); // Expect HTTP 200
}

There is a subtle point that beginners often trip over. If your real configuration protects /api/editor/** through hasRole("EDITOR"), then this test may return 403, because the user does not have ROLE_EDITOR. And that is not a “bad test”; it is a signal that you are modeling the wrong rights for the actual requirement.

Sometimes you need both a role and permissions. In that case, it is better not to try to “glue together” .roles(...) and .authorities(...) in one call chain, because it is easy to overwrite one with the other. A more reliable approach is to list everything explicitly in authorities(...), including the role as an authority with the prefix:

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")
                            // Explicitly set both the role (as authority) and the permission
                            .authorities(
                                    new SimpleGrantedAuthority("ROLE_EDITOR"),
                                    new SimpleGrantedAuthority("draft:review")))
    )
    .andExpect(status().isOk()); // Expect HTTP 200
}

Yes, here we wrote ROLE_EDITOR by hand, and that is exactly the case where it makes sense: we are working with authorities “as is” and controlling the final set of permissions ourselves.

5. Helpers for test users

The first couple of tests with .with(user("anna").roles("USER")) usually look cute. By the tenth test, you start suspecting that Anna is about to become a team lead, because she is everywhere. By the twentieth, you realize that copy-pasted users turn tests into noise: they stop being “access rules” and become “request layout.”

So a good style is to move repeated test users into small helper methods that return a RequestPostProcessor. That preserves clarity and readability, while not forcing you to write 300 identical lines.

For example, like this:

import org.springframework.test.web.servlet.request.RequestPostProcessor;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;

private RequestPostProcessor regularUser() {
    // Helper returns a post-processor that can be reused in tests
    return user("anna").roles("USER");
}

And then the test becomes more pleasant to read:

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(
            // The test keeps only the essence of the access rule
            get("/api/me").with(regularUser())
    )
    .andExpect(status().isOk()); // Expect HTTP 200
}

The important thing is not to overdo it. Our goal is to make tests readable, not to build a mini-framework on top of MockMvc. Usually 2–4 helper methods (user, editor, admin) are already enough for the regression pack to stop looking like “copy and paste.”

6. The limits of using user()

This is the most important point in the lecture, because this is where honest expectations about tests are formed. user() is a tool for modeling an already authenticated user. It does not verify the password, does not go to the database for UserAccount, does not start AuthenticationManager, and does not run DaoAuthenticationProvider. If you expect it to check login, you are expecting a microwave to learn how to cook borscht.

But user() is excellent at testing what we need in a basic regression pack: authorization rules and access boundaries. That is: which endpoints are available to anonymous users, which require authentication, which require ROLE_EDITOR or ROLE_ADMIN, and which check an authority. These are fast, cheap tests that catch regressions in configuration or access rules.

A useful mental model is this: user() is like handing the security guard a pass and saying, “Assume this person is already inside the system; just check whether you let them through this door.” Login verification is a different story: there you check how the person got that pass in the first place, who issued it, and whether it is fake.

In the context of Spring Security, this distinction corresponds to two questions: authentication and authorization. user() primarily helps test the authorization layer: access, roles, permissions, 401/403 behavior. And that is perfectly normal, because without such a layer you would have to recheck access “by hand” in Postman every time and hope it still works.

7. Typical mistakes when using user()

Mistake #1: expecting user() to test login and password.
It is very easy to start thinking that because we “created a user,” we have verified authentication. In reality, user() only creates the context in which the request looks authenticated. That is good for checking 403/200, but it does not verify your real UserDetailsService, PasswordEncoder, or account states.

Mistake #2: confusing ROLE_ and writing roles("ROLE_ADMIN").
This is a classic, worthy of a museum display next to “why am I getting a NullPointerException.” In roles(...), you write the role without the prefix; otherwise you risk getting ROLE_ROLE_ADMIN and then spending a long time convincing yourself that “Spring Security is broken.” It is not broken; it is simply executing your command very honestly.

Mistake #3: modeling the wrong rights that the configuration actually requires.
If an endpoint is protected through hasRole("EDITOR"), and you only added the draft:review authority, you will get 403. And that is correct. In such cases, you either need to provide the correct role through .roles("EDITOR"), or add ROLE_EDITOR to the authority list if you are intentionally testing authority-based rules.

Mistake #4: copy-pasting users in every test until “Anna becomes a global variable.”
When .with(user("anna").roles("USER")) is scattered across 30 tests, they become noisy and hard to read. Moving this into helper methods makes the regression pack much more stable: you change the user name or role in one place and do not trigger an avalanche of edits.

Mistake #5: trying to check too much in one test.
Sometimes people want to go through the public, me, editor, and admin areas in a single test to “save time.” When that test fails, it becomes unclear what exactly broke. It is much more reliable to keep one test for one access rule: that way the regression pack is more precise and easier to diagnose.

1
Task
Spring Security, level 26, lesson 2
Locked
Personal endpoint via user()
Personal endpoint via user()
1
Task
Spring Security, level 26, lesson 2
Locked
Authority-based access via user()
Authority-based access via user()
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION