- Published on
「Spring Boot API 開發:從 0 到 1」Day 34 Spring Security 測試
因為加入了 Spring Security
,所以我們需要調整相關的測試程式碼,來加入相關的認證和授權
spring-security-test
主要用途
- 模擬認證:允許你在測試中輕鬆模擬已認證的使用者
- 模擬授權:讓你可以為測試設置特定的權限或角色
- CSRF 支援:提供在測試中處理 CSRF(跨站請求偽造)保護的工具
- 安全相關的 Mock 物件:提供一些模擬的安全相關物件,方便測試
常用的註解和方法
@WithMockUser
這是最常用的註解之一,用於模擬一個已認證的使用者
你可以指定使用者名稱、角色,甚至是權限
@Test
@WithMockUser(username = "user", roles = "USER")
// @WithMockUser(username = "admin", roles = {"USER", "ADMIN"})
public void testUserAccess() {
// 測試程式碼
}
@WithAnonymousUser
用於模擬匿名使用者:
@Test
@WithAnonymousUser
public void testAnonymousAccess() {
// 測試程式碼
}
@WithUserDetails
如果你有自訂的 UserDetailsService
,可以使用這個註解
@Test
@WithUserDetails("[email protected]")
public void testWithUserDetails() {
// 測試程式碼
}
SecurityMockMvcRequestPostProcessors
這個類別提供了一些有用的方法,特別是在使用 MockMvc 進行測試時
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
這裡的 csrf()
方法會為請求添加一個有效的 CSRF token
@Test
public void testWithCsrf() throws Exception {
mockMvc.perform(post("/api/data").with(csrf()))
.andExpect(status().isOk());
}
RequestPostProcessor
模擬使用者
使用 除了使用註解,你也可以在特定的請求中模擬使用者
mockMvc.perform(get("/api/data").with(user("admin").roles("ADMIN")))
.andExpect(status().isOk());
修改 TodoController 測試
在每個測試方法上加上 @WithMockUser
註解,模擬一個已登入的使用者。
在 POST
、PUT
和 DELETE
請求中,增加 .with(csrf())
來模擬 CSRF token
@WebMvcTest(TodoController.class)
public class TodoControllerTest {
@Test
@WithMockUser(username = "user")
public void createTodo() throws Exception {
Todo todo = new Todo(null, "新待辦事項", false);
Todo savedTodo = new Todo(1L, "新待辦事項", false);
when(todoService.save(any(Todo.class))).thenReturn(savedTodo);
mockMvc.perform(post("/api/todos")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(todo)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.id").value(1))
.andExpect(jsonPath("$.data.title").value("新待辦事項"))
.andExpect(jsonPath("$.data.completed").value(false));
verify(todoService, times(1)).save(any(Todo.class));
}
@Test
@WithMockUser(username = "user")
public void getAllTodos() throws Exception {
// 測試內容保持不變
}
@Test
@WithMockUser(username = "user")
public void getTodo() throws Exception {
// 測試內容保持不變
}
@Test
@WithMockUser(username = "user")
public void updateTodo() throws Exception {
Todo updatedTodo = new Todo(1L, "更新的待辦事項", true);
when(todoService.updateTodo(eq(1L), any(Todo.class))).thenReturn(Optional.of(updatedTodo));
mockMvc.perform(put("/api/todos/1")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updatedTodo)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.id").value(1))
.andExpect(jsonPath("$.data.title").value("更新的待辦事項"))
.andExpect(jsonPath("$.data.completed").value(true));
verify(todoService, times(1)).updateTodo(eq(1L), any(Todo.class));
}
@Test
@WithMockUser(username = "user")
public void deleteTodo() throws Exception {
when(todoService.deleteTodo(1L)).thenReturn(true);
mockMvc.perform(delete("/api/todos/1").with(csrf()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
verify(todoService, times(1)).deleteTodo(1L);
}
}
修改 TodoEndToEnd 測試
在 setUp
中建立一個測試的使用者
在每個測試方法中,我們使用 REST Assured 提供的 .auth().basic(username, password)
來設定 HTTP 基本認證
這會在每個請求的標頭中添加 Authorization: Basic ...
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.properties")
public class TodoEndToEndTest {
@LocalServerPort
private int port;
@Autowired
private TodoRepository todoRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
private String username = "testuser";
private String password = "testpassword";
@BeforeEach
void setUp() {
RestAssured.port = port;
todoRepository.deleteAll();
userRepository.deleteAll();
// 建立測試用戶
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setRoles("USER");
userRepository.save(user);
}
@Test
void createTodo() {
Todo newTodo = new Todo(null, "測試待辦事項", false);
given()
.auth().basic(username, password)
.contentType(ContentType.JSON)
.body(newTodo)
.when()
.post("/api/todos")
.then()
.statusCode(200)
.body("success", equalTo(true))
.body("data.title", equalTo("測試待辦事項"))
.body("data.completed", equalTo(false));
assertThat(todoRepository.findAll()).hasSize(1);
}
@Test
void getAllTodos() {
Todo todo1 = new Todo(null, "測試待辦事項", false);
Todo todo2 = new Todo(null, "測試待辦事項2", true);
todoRepository.saveAll(Arrays.asList(todo1, todo2));
given()
.auth().basic(username, password)
.when()
.get("/api/todos")
.then()
.statusCode(200)
.body("success", equalTo(true))
.body("data", hasSize(2))
.body("data[0].title", equalTo("測試待辦事項"))
.body("data[0].completed", equalTo(false))
.body("data[1].title", equalTo("測試待辦事項2"))
.body("data[1].completed", equalTo(true));
}
@Test
void getTodo() {
Todo savedTodo = todoRepository.save(new Todo(null, "測試待辦事項", false));
given()
.auth().basic(username, password)
.when()
.get("/api/todos/{id}", savedTodo.getId())
.then()
.statusCode(200)
.body("success", equalTo(true))
.body("data.title", equalTo("測試待辦事項"))
.body("data.completed", equalTo(false));
}
@Test
void updateTodo() {
Todo savedTodo = todoRepository.save(new Todo(null, "原始待辦事項", false));
Todo updatedTodo = new Todo(savedTodo.getId(), "更新後的待辦事項", true);
given()
.auth().basic(username, password)
.contentType(ContentType.JSON)
.body(updatedTodo)
.when()
.put("/api/todos/{id}", savedTodo.getId())
.then()
.statusCode(200)
.body("success", equalTo(true))
.body("data.title", equalTo("更新後的待辦事項"))
.body("data.completed", equalTo(true));
Todo actualTodo = todoRepository.findById(savedTodo.getId()).orElseThrow();
assertThat(actualTodo.getTitle()).isEqualTo("更新後的待辦事項");
assertThat(actualTodo.isCompleted()).isTrue();
}
@Test
void deleteTodo() {
Todo savedTodo = todoRepository.save(new Todo(null, "要刪除的待辦事項", false));
given()
.auth().basic(username, password)
.when()
.delete("/api/todos/{id}", savedTodo.getId())
.then()
.statusCode(200)
.body("success", equalTo(true));
assertThat(todoRepository.findAll()).isEmpty();
}
}
測試
測試程式碼都修改完後,執行所有的測試,應該都是呈現綠燈
結論
在這篇文章中,我們深入探討了如何調整 Spring Boot 應用程式的測試,以適應加入 Spring Security 後的變化
透過這些調整,我們不僅確保了應用程式的安全性,還保證了測試的完整性和有效性
同步刊登於 iTHome 鐵人賽 「Spring Boot API 開發:從 0 到 1」Day 34 Spring Security 測試
圖片來源:AI 產生