[JAVA] Let's write a test code for login function in Spring Boot

Overview

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 (╹◡╹)

goal

By reading this article, you will (probably) understand the following:

Environment / code

The environment is as follows. Dependent libraries etc. are the same as the previous article.

環境.png

Also, the Source Code can be found on GitHub.

Overall picture

The login app is a simple one with the following functions.

A rough diagram of this is shown in Fig. 1.

処理フロー.png

Figure 1. Processing flow

Package structure

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. ..

パッケージ全体像.png

Figure 2. Overview of the package

What you want to test

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.

SpringSecurity.png

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.

Part1. Login process

Let's start with a simple login screen. The screen image should look like Figure 4.

20191130-152837.png

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.

Those who know will let you through

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.

Test code

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"));
	}

}

Commentary

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.

Redirect processing

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.

People who do not know do not pass

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.

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"));
	}

Commentary

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.

Part1.5 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.

SecurityContext.png

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.

Login user (provisional)

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.

Annotation code

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();

}

Commentary

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.

Context factory

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;
	}

}

Commentary

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.

Part2. Usage screen

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.

user.png

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.

Can you guide someone you know

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.

Test code

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"));
	}
}

Commentary

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.

TestContextManager.png

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.

Can you prevent bad luck?

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.

Test code

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());
	}

Commentary

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.

Can I clean up by logging out?

Test code

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"));
	}

Commentary

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.

ログアウト後の画面遷移失敗.png

Figure 8. After logging out, it fails because it was done before logging out.

ログアウト後の画面遷移成功.png

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.

Extra user registration process

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.

image.png

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

Test code

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"));
			
	}

Commentary

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. (::


Summary

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

Let's write a test code for login function in Spring Boot
Write test code in Spring Boot
How to write a unit test for Spring Boot 2
Run a Spring Boot project in VS Code
[JUnit 5 compatible] Write a test using JUnit 5 with Spring boot 2.2, 2.3
Use DBUnit for Spring Boot test
Sample code to unit test a Spring Boot controller with MockMvc
"Teacher, I want to implement a login function in Spring" ① Hello World
Test controller with Mock MVC in Spring Boot
Try to implement login function with Spring Boot
How to add a classpath in Spring Boot
Make a snippet for Thymeleaf in VS Code
Sample code for DB control by declarative transaction in Spring Boot + Spring Data JPA
Let's write a Qiita article in org-mode of Emacs !!
How to create a Spring Boot project in IntelliJ
[Spring Boot] How to create a project (for beginners)
Let's write a proxy integrated Lambda function of Amazon API Gateway with Spring Cloud Function
I tried to make a login function in Java
Test field-injected class in Spring boot test without using Spring container
I wrote a test with Spring Boot + JUnit 5 now
Let's make a simple API with EC2 + RDS + Spring boot ①
I haven't understood after touching Spring Boot for a month
Introducing Spring Boot2, a Java framework for web development (for beginners)
Use Spring Test + Mockito + JUnit 4 for Spring Boot + Spring Retry unit tests
Set context-param in Spring Boot
SNS login with Spring Boot
Login function with Spring Security
Major changes in Spring Boot 1.5
NoHttpResponseException in Spring Boot + WireMock
Spring Boot for annotation learning
Let's create a TODO application in Java 4 Implementation of posting function
Let's make a book management web application with Spring Boot part1
RSocket is supported in Spring Boot 2.2, so give it a try
How to make a hinadan for a Spring Boot project using SPRING INITIALIZR
Spring Data JPA: Write a query in Pure SQL in @Query of Repository
Sample code that uses the Mustache template engine in Spring Boot
Get a proxy instance of the component itself in Spring Boot
Let's make a book management web application with Spring Boot part3
Let's create a TODO application in Java 6 Implementation of search function
I tried to write code like a type declaration in Ruby
[RSpec on Rails] How to write test code for beginners by beginners
Let's create a TODO application in Java 8 Implementation of editing function
Let's make a book management web application with Spring Boot part2
[Spring Boot] Until @Autowired is run in the test class [JUnit5]
[Beginner] Let's write REST API of Todo application with Spring Boot
Let's write a code that is easy to maintain (Part 2) Name
How to display characters entered in Spring Boot on a browser and reference links [Introduction to Spring Boot / For beginners]
Spring Boot Hello World in Eclipse
Spring Boot application development in Eclipse
Spring Boot application code review points
A memorandum for writing beautiful code
Spring Boot for the first time
Add a search function in Rails.
Java Spring environment in vs Code
Spring Boot programming with VS Code
Frequent annotations for Spring Boot tests
Implement REST API in Spring Boot
Spring profile function, and Spring Boot application.properties
Implement Spring Boot application in Gradle
A memo that touched Spring Boot
[RSpec] How to write test code