This is a continuation of Articles I wrote in When. I was addicted to writing test code for login-related processing, so I will leave it as a memorandum. Also, the explanation of the login process itself is described in here, so if you don't mind.
If you have any mistakes, please let me know (╹◡╹)
By reading this article, you will (probably) understand the following:
The environment is as follows. Dependent libraries etc. are the same as the previous article.
Also, the Source Code can be found on GitHub.
The login app is a simple one with the following functions.
A rough diagram of this is shown in Fig. 1.
Figure 1. Processing flow
The function itself is simple, but if you take this and that into consideration, the number of packages will increase and it will be difficult to grasp the whole picture, so Fig. 2 shows the ones that are softly classified by role. ..
Figure 2. Overview of the package
The most important thing to keep in mind when testing, whether coded or not, is to be clear about what you want to verify.
This time, it means that the function defined in Spring Security is working as expected
.
This is stiff and a little difficult to grasp the atmosphere, so let's make it a little more sticky.
As shown in Fig. 3, if you say Do you do your best as Spring Security requested
, I think it will be easier to imagine.
Figure 3. Spring Security verification target
~~ The characters are dirty, so ~~ It's important, so let's show the above figure in a list as well.
Below, we'll look at how to write test code to verify that they're working properly.
Let's start with a simple login screen. The screen image should look like Figure 4.
Figure 4. Login screen
To verify that the login process is working properly, you need to test the following:
In other words, it can be expressed as Is Spring Security only passing people as expected
?
Now, let's verify whether Spring Security will actually do the job.
First, as a simple case, if you log in as a user existing on the DB, we will verify whether you can log in. SpringSecurity executes the following as a login process.
The code of the process itself is explained in another article, so I will omit it here and look at the test code immediately.
LoginControllerTest.java
@DbUnitConfiguration(dataSetLoader = CsvDataSetLoader.class)
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class,
})
@AutoConfigureMockMvc
@SpringBootTest(classes = {LoginUtApplication.class})
@Transactional
public class LoginControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
You can log in as a user who exists on void DB() throws Exception {
this.mockMvc.perform(formLogin("/sign_in")
.user("top_user")
.password("password"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/top_user/init"));
}
}
I wrote the test code roughly in the previous article, so here I will touch on the new elements.
formLogin
As an argument to be passed to the perfrom method of mockMvc
, formLogin
is specified.
Since the login process is familiar, you can imagine the operation in the atmosphere, but the important points here are as follows.
If you understand the login process to some extent, you may be stumbled.
As stated in Official, Spring Security works to redirect you to the specified URL on successful / unsuccessful logins. Therefore, here, we will verify whether the redirect destination URL is as expected. As I will touch on later, it is enough if you can understand that if the login is successful, you will be taken to the URL that looks like the user's top screen.
In the test code above, it turns out that Spring Security goes through the user. However, this alone leaves the possibility that anyone may be welcome and secure. Here, we will verify that security is ensured, that is, whether or not people who do not want to pass through are passed through.
The following is an excerpt of the target test code.
LoginControllerTest(Excerpt)
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
void If a user existing on the DB has the wrong password, the user will be redirected to the failure screen.() throws Exception {
this.mockMvc.perform(formLogin("/sign_in")
.user("top_user")
.password("wrongpassword"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/?error"));
}
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
void If you log in as a user that does not exist on the DB, you will be redirected to the error URL.() throws Exception {
this.mockMvc.perform(formLogin("/sign_in")
.user("nobody")
.password("password"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/?error"));
}
There is no big difference from the normal system, but the point is that the redirect destination is /? Error
. If this works correctly, you can verify that not only can log-in users be able to log in, but users who should not be able to log in fail to log in.
This is a minimal level, but I was able to write test code for the login process. The Dao and Service layers overlap with the previous article, so I will omit them. If you are interested, please refer to the source code.
By the way, the Web application does not end after logging in, but uses various screens while logged in. However, it is very troublesome to log in to the test code of the screen that requires login every time, and then write the test code of the screen.
To solve this, it would be very convenient if you could manually create a logged in state
.
Now, before looking at the test code on the screen after login, let's touch on how to create a logged-in state.
Well, I wrote that you should create a logged in state
, but what exactly should you do?
As a pre-process, issue a session ID, create a user object to be linked, and do this in a form that Spring Security can handle ... and create it even if you reproduce the process that Spring Security is doing one by one. You can, but it seems a little difficult.
Actually, it is processed by annotation as introduced in Official You can create a simulated logged-in state by just writing a little.
Here, the logged-in state
is more accurately called the SecurityContext
. For a description of context, here should be helpful.
Suddenly the story became abstract. To make it a little easier to imagine, Figure 5 shows a simple graphical representation of the context.
Figure 5. Security context
The context allows you to `code create the same state as when you log in on the screen. It's convenient.
Let's look at the code to actually create the context.
First, let's take a look at how the logged-in status is described in the test code before going into the contents. By writing the code that I will touch on, it will be easier to understand how the test code will be written, and if you know the merits first, it will be easier to understand.
Example of test code using logged-in status
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="top_user", password="password")
User top is passed as view in void init processing() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(view().name("top_user"));
}
The important thing is the WithMockCustomUser
annotation. Pass the user name and password as parameters. With just this, as a logged-in user, you can run test code for screens that require you to log in. Thank you.
By the way, this annotation is a custom annotation, and you have to work a little hard to make it, but once you make it, you can reuse it in applications that require login. Let's do our best to make it easier.
First is the code for the WithMockCustomUser
annotation that was written earlier.
For custom annotations, go to here.
WithMockCustomUser.java
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String username();
String password();
}
The annotation itself is a simple one with two fields, but the annotation with a very long parameter called WithSecurityContext
has a difficult atmosphere.
This is an annotation for defining SpringSecurityContext
. It doesn't come out very well, so to put it another way, it's like a memo for Spring Security to remember the information "Remember this person !!" in advance.
WithMockCustomUser, as the name suggests, only defines user information, so let's take a look at the process that creates the context.
Even if it's called a context factory, it's pretty refreshing, so let's take a look at the actual code.
WithMockCustomUserSecurityContextFactory.java
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser>{
@Autowired
private AuthenticationManager authenticationManager;
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
//Issue a token for authentication with user name and password
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(customUser.username(), customUser.password());
//Login process
Authentication auth = authenticationManager.authenticate(authToken);
context.setAuthentication(auth);
return context;
}
}
I've written a lot of code, but it's pretty simple to do, creating a logged-in state
or SecurityContext.
The following are roughly performed as individual processes.
SecurityContextHolder is an object for managing context, and it manages various things around threads, but it is out of the main line, so I will omit it this time. I think that there is no problem if you think that it is the origin of creating context.
And, importantly, the authentication token
here is completely different from what is often contrasted with cookies or called API tokens.
This seems to be closer to "Hard Token" in terms of the fact that the user name and password are the keys for authentication.
It's a little off topic, but by defining custom annotations, you can easily create a logged-in state, that is, SecurityContext
by just specifying the user name and password on the test code. You did it.
It was a little difficult, but now that you can create a logged-in state, you can write test code for screens that require login much easier.
Now, let's take a look at the top screen for users as an example of a screen that requires login. The screen image is shown in Figure 6.
Figure 6. Top screen for users
It's a simple screen that just greets the user. It's a sample screen, so don't worry about the design ...
By the way, this time the purpose is to verify login-related processing, so for this screen, we will verify whether the following is operating as expected.
As with the login screen, it seems good to verify based on whether you can do something wrong
.
Now let's look at the actual test code.
As with the login process, we will first look at the operation on the regular route. The number of annotations I introduced earlier has increased, so I think you can understand it quickly.
TopUserControllerTest.java
@DbUnitConfiguration(dataSetLoader = CsvDataSetLoader.class)
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class,
WithSecurityContextTestExecutionListener.class
})
@AutoConfigureMockMvc
@SpringBootTest(classes = {LoginUtApplication.class})
@Transactional
public class TopUserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="top_user", password="password")
User top is passed as view in void init processing() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(view().name("top_user"));
}
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="top_user", password="password")
The login username is passed to the model in the void init process() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(model().attribute("loginUsername", "top_user"));
}
}
As for WithMockCustomUser
, as mentioned above, by specifying the user name and password, the logged-in state is created.
Here you can see that the WithSecurityContextTestExecutionListener
, a class that seems to be related, is specified in TestExecutionListeners
.
This is a setting that allows TestContextManager
to handle SecurityContext
.
Writing context for context can be a bit confusing, so let's organize it here as well.
Figure 7 shows a rough representation of TestContextManager.
Figure 7. TestContextManager
In addition to the context, it is also responsible for pre-processing and post-processing, but what is important here is that it manages the context required to execute the test. It may be difficult to grasp the image at first glance, but it is a necessary process to create a logged-in state in advance before executing the test, so it is good to be aware of it.
Next, let's see if we can prevent people who are plotting bad things
.
Now, to sort out, let's look again at what the bad thing
means.
Since the implementation itself to prevent the above is defined in the Config class of Spring Security, here, let's check if it can really be prevented by sending a test request
.
TopUserControllerTest.java(Excerpt)
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
void Unlogged-in users cannot transition to the user top screen by directly typing the URL() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/"));
}
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="admin_user", password="password")
void Users with administrator privileges cannot transition to the user top screen by directly typing the URL.() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(status().isForbidden());
}
The point is what to do if you prevent wrongdoing.
If you are not logged in, you want to be redirected to the login screen, so it describes that the status code is redirect (starting with 3) and the transition destination is the login screen.
And in the case of an unauthorized user (it seems more natural for the administrator to see it ...), it states that 403 Forbidden will be returned as the status code.
These test codes show that Spring Security is doing the job it was supposed to do without any problems. Finally, as a post-processing, I would like to take a look at logout.
Logout process
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="top_user", password="password")
void Transition to the login screen by logout processing() throws Exception {
this.mockMvc.perform(post("/logout")
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/?logout"));
}
The logout process is also just sending a POST request to the URL for logout set in Spring Security, so you can understand it in an atmosphere.
However, unlike the formLogin method
, a normal POST request does not grant a CSRF token. Therefore, you need to add the CSRF token to the request with the wirh (SecurityMockMvcRequestPostProcessors.csrf)
method.
This alone doesn't seem to be a problem to verify that the logout process is working, but let's take it one step further.
If you log out, you can say that you have changed to not logged in
. Therefore, the correct operation is that you cannot transition to a screen that requires login after logging out.
I would like to take a look at this as well, just in case. That said, it's not difficult, I just added screen transition processing after logging out. If you look at Figures 8 and 9 below, you can see that the logout process created a state in which you are not logged in.
Figure 8. After logging out, it fails because it was done before logging out.
Figure 9. You will be prompted to log in again after logging out
It's been a bit long, but now I can verify the login process with the test code as well. I did it.
Before getting into the summary, it will be a little different from the login process, but I would like to briefly touch on the user registration process
that is addicted to it as a supplement.
Now, regarding the user registration process, the process itself is almost the same as creating a SecurityContext
.
See Source Code for implementation.
What we want to cover here is the validation process. The image of the movement is as shown in Fig. 10.
Figure 10. User registration
I've been addicted to how to write test code for validation processing, so I'll write the points as a memorandum.
Validation process test code
@Test
void AuthInpuType error occurs when POSTing symbols other than half-width alphanumeric hyphen underscore() throws Exception {
this.mockMvc.perform(post("/signup/register")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("username", "<script>alert(1)</script>")
.param("rawPassword", ";delete from ut_user")
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(model().hasErrors())
.andExpect(model().attributeHasFieldErrorCode("userForm", "username", "AuthInputType"))
.andExpect(model().attributeHasFieldErrorCode("userForm", "rawPassword", "AuthInputType"));
}
@DatabaseSetup(value = "/controller/signup/setUp/")
@Test
A UniqueUsername error occurs when POSTing with a username that already exists in the void DB() throws Exception {
this.mockMvc.perform(post("/signup/register")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("username", "test_user")
.param("rawPassword", "password")
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(model().hasErrors())
.andExpect(model().attributeHasFieldErrorCode("userForm", "username", "UniqueUsername"));
}
It's written a lot, but all you're doing is setting the values and manually sending the POST request. The important thing here is the ʻattributeHasFieldErrorCode method. We are passing
name, fieldName, error` as arguments.
Each of these has the following correspondence.
Regarding name and fieldName, if you have touched the MVC app, you will be surprised at the image. As for error, you can see it by actually looking at the field.
UserForm.java
@Data
public class UserForm {
@AuthInputType
@UniqueUsername
private String username;
@AuthInputType
private String rawPassword;
}
Here, what is passed as an argument as an error corresponds to the name of the annotation.
The specified annotation is a custom annotation, and since the Constraint
annotation is added, it behaves as a Constraint annotation
.
If there is a constraint violation, BindingResult
will detect it as an error, so it can be handled by the validation process.
For more information, please see Official.
I didn't fully understand the correlation check, and I was confused by the password matching test code, so if anyone is familiar with it, I would be grateful if you could provide information. (::
Regarding the login process, although it is a rough idea, it is now possible to write test code and verify the operation. By looking at simple CRUD in the previous article and important login processes in web applications in this article, I think that if it is a simple application, you can develop it while writing test code firmly.
When I'm writing code, I suddenly come up with a super nice refactoring and want to change the code around, but if I write test code like this, Is the verified behavior broken?
Is a button. You will be able to verify it from the beginning.
Besides, you will be able to execute tests efficiently and with guaranteed reproducibility without having to log in every time and click the screen to display an error screen. It costs a lot to learn how to write test code, but it will give you more time to write fun code, so I think it's a good idea to incorporate it little by little while having fun.
Ella I say so, but I'm still studying test code, so I'd like to do my best to study more.
Recommended Posts