重构底层框架
This commit is contained in:
@@ -23,6 +23,23 @@
|
||||
<artifactId>patbond-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@FeignClient(name = "patbond-user")
|
||||
@FeignClient(name = "patbond-user", url = "${patbond.user-service.url}")
|
||||
public interface UserClient {
|
||||
|
||||
@PostMapping("/internal/users")
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.patbond.patbond.auth.dto.LoginRequest;
|
||||
import com.patbond.patbond.auth.dto.RegisterRequest;
|
||||
import com.patbond.patbond.auth.service.AuthService;
|
||||
import com.patbond.patbond.common.response.ApiResponse;
|
||||
import javax.validation.Valid;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.patbond.patbond.auth.dto;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class LoginRequest {
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.patbond.patbond.auth.dto;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class RegisterRequest {
|
||||
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
server:
|
||||
port: 8081
|
||||
port: ${PATBOND_AUTH_PORT:8081}
|
||||
|
||||
spring:
|
||||
config:
|
||||
import: "optional:nacos:patbond-auth.yml"
|
||||
application:
|
||||
name: patbond-auth
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
register-enabled: ${NACOS_REGISTER_ENABLED:true}
|
||||
fail-fast: ${NACOS_DISCOVERY_FAIL_FAST:false}
|
||||
config:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
|
||||
patbond:
|
||||
user-service:
|
||||
url: ${PATBOND_USER_SERVICE_URL:http://127.0.0.1:8082}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.patbond.patbond.auth;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class AuthApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// Verifies the auth service starts with the committed application.yml:
|
||||
// the Feign client resolves patbond.user-service.url from the config
|
||||
// default without Nacos or a running user service (ADR-002).
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package com.patbond.patbond.auth.controller;
|
||||
|
||||
import com.patbond.patbond.auth.client.UserClient;
|
||||
import com.patbond.patbond.common.response.ApiResponse;
|
||||
import com.patbond.patbond.common.user.UserProfile;
|
||||
import com.patbond.patbond.common.user.VerifyPasswordResponse;
|
||||
import feign.FeignException;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
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.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* MockMvc tests for the auth endpoints. The Feign UserClient is replaced with
|
||||
* a Mockito mock so no user-service process is required. Assertions follow the
|
||||
* CURRENT behaviour of the in-memory prototype (opaque token, business
|
||||
* failures folded to 400, FeignException not yet translated) — recorded here
|
||||
* as the baseline the upcoming error-contract work will change deliberately.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
class AuthControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
private UserClient userClient;
|
||||
|
||||
private static final String REGISTER_BODY = """
|
||||
{"username":"alice","password":"secret123","nickname":"Alice","phone":"13800000000"}
|
||||
""";
|
||||
private static final String LOGIN_BODY = """
|
||||
{"username":"alice","password":"secret123"}
|
||||
""";
|
||||
|
||||
@Test
|
||||
void registerReturnsTokenWhenUserServiceSucceeds() throws Exception {
|
||||
UserProfile profile = new UserProfile(1L, "alice", "Alice", null, LocalDateTime.now());
|
||||
when(userClient.createUser(any())).thenReturn(ApiResponse.success(profile));
|
||||
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(REGISTER_BODY))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.tokenType").value("Bearer"))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.userId").value(1))
|
||||
.andExpect(jsonPath("$.data.username").value("alice"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerReturnsBadRequestWhenUserServiceReportsFailure() throws Exception {
|
||||
when(userClient.createUser(any())).thenReturn(ApiResponse.failure(409, "用户名已存在"));
|
||||
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(REGISTER_BODY))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerRejectsInvalidPayloadWithoutCallingUserService() throws Exception {
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"username\":\"ab\",\"password\":\"123\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginReturnsTokenWhenPasswordVerified() throws Exception {
|
||||
when(userClient.verifyPassword(any()))
|
||||
.thenReturn(ApiResponse.success(new VerifyPasswordResponse(1L, "alice", "Alice")));
|
||||
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(LOGIN_BODY))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.userId").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginReturnsBadRequestWhenVerificationReportsFailure() throws Exception {
|
||||
when(userClient.verifyPassword(any())).thenReturn(ApiResponse.failure(401, "用户名或密码错误"));
|
||||
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(LOGIN_BODY))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginRejectsBlankCredentials() throws Exception {
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"username\":\"\",\"password\":\"\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginPropagatesFeignExceptionUnhandled() throws Exception {
|
||||
Request request = Request.create(Request.HttpMethod.POST, "/internal/users/verify-password",
|
||||
Map.of(), null, StandardCharsets.UTF_8, null);
|
||||
FeignException unauthorized = FeignException.errorStatus("UserClient#verifyPassword",
|
||||
Response.builder().status(401).request(request).build());
|
||||
when(userClient.verifyPassword(any())).thenThrow(unauthorized);
|
||||
|
||||
// Current baseline: a downstream 401 raised as FeignException is not
|
||||
// translated, so it escapes the MVC layer (a real deployment answers 500).
|
||||
assertThatThrownBy(() -> mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(LOGIN_BODY)))
|
||||
.hasCauseInstanceOf(FeignException.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Test-only configuration: keeps @SpringBootTest self-contained on a clean
|
||||
# checkout, where the git-ignored application.yml does not exist yet.
|
||||
spring:
|
||||
application:
|
||||
name: patbond-auth
|
||||
|
||||
patbond:
|
||||
user-service:
|
||||
url: http://127.0.0.1:8082
|
||||
Reference in New Issue
Block a user