diff --git a/backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java b/backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java new file mode 100644 index 0000000..6640cb8 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java @@ -0,0 +1,29 @@ +package com.xly.erp.module.usr.controller; + +import com.xly.erp.common.response.Result; +import com.xly.erp.module.usr.dto.LoginReq; +import com.xly.erp.module.usr.service.LoginService; +import com.xly.erp.module.usr.vo.LoginVo; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 认证入口。REQ-USR-001:POST /api/v1/auth/login。 + */ +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthController { + + private final LoginService loginService; + + @PostMapping("/login") + public Result login(@RequestBody @Valid LoginReq req) { + LoginVo vo = loginService.login(req.getUsername(), req.getPassword(), req.getCompanyCode()); + return Result.ok(vo); + } +} diff --git a/backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerTest.java b/backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerTest.java new file mode 100644 index 0000000..c864275 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerTest.java @@ -0,0 +1,115 @@ +package com.xly.erp.module.usr.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xly.erp.common.response.ErrorCode; +import com.xly.erp.module.usr.dto.LoginReq; +import com.xly.erp.module.usr.support.LoginTestSeeder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class AuthControllerTest { + + @Autowired private MockMvc mvc; + @Autowired private ObjectMapper json; + @Autowired private LoginTestSeeder seeder; + @Autowired private JdbcTemplate jdbc; + + @BeforeEach + void setUp() { + seeder.reset(); + } + + private String body(String username, String password, String companyCode) throws Exception { + LoginReq r = new LoginReq(); + r.setUsername(username); + r.setPassword(password); + r.setCompanyCode(companyCode); + return json.writeValueAsString(r); + } + + @Test + void post_login_success_returns200_andLoginVo() throws Exception { + mvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body(LoginTestSeeder.USER_OK, LoginTestSeeder.DEFAULT_PASSWORD, + LoginTestSeeder.COMPANY_OK))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(ErrorCode.OK)) + .andExpect(jsonPath("$.data.accessToken").isNotEmpty()) + .andExpect(jsonPath("$.data.tokenType").value("Bearer")) + .andExpect(jsonPath("$.data.expiresInSec").value(7200)) + .andExpect(jsonPath("$.data.userInfo.username").value(LoginTestSeeder.USER_OK)) + .andExpect(jsonPath("$.data.userInfo.companyCode").value(LoginTestSeeder.COMPANY_OK)); + } + + @Test + void post_login_badCredentials_returns401_code40101() throws Exception { + mvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body(LoginTestSeeder.USER_OK, "WrongPass1!", LoginTestSeeder.COMPANY_OK))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_CREDENTIALS)); + } + + @Test + void post_login_unknownUser_returns401_code40101() throws Exception { + mvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body("nobody", "any", LoginTestSeeder.COMPANY_OK))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_CREDENTIALS)); + } + + @Test + void post_login_lockedAccount_returns423_code42301() throws Exception { + jdbc.update("UPDATE sys_user SET iFailedLoginCount=5, tLockUntil=DATE_ADD(NOW(), INTERVAL 30 MINUTE) WHERE sUsername=?", + LoginTestSeeder.USER_OK); + mvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body(LoginTestSeeder.USER_OK, LoginTestSeeder.DEFAULT_PASSWORD, + LoginTestSeeder.COMPANY_OK))) + .andExpect(status().isLocked()) + .andExpect(jsonPath("$.code").value(ErrorCode.ACCOUNT_LOCKED)); + } + + @Test + void post_login_deletedAccount_returns401_code40103() throws Exception { + mvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body(LoginTestSeeder.USER_DELETED, LoginTestSeeder.DEFAULT_PASSWORD, + LoginTestSeeder.COMPANY_OK))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(ErrorCode.ACCOUNT_DELETED)); + } + + @Test + void post_login_unknownCompany_returns400_code40004() throws Exception { + mvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body(LoginTestSeeder.USER_OK, LoginTestSeeder.DEFAULT_PASSWORD, "NOPE"))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.COMPANY_NOT_FOUND)); + } + + @Test + void post_login_blankUsername_returns400_code40001() throws Exception { + mvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body("", LoginTestSeeder.DEFAULT_PASSWORD, LoginTestSeeder.COMPANY_OK))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST)); + } +}