重构底层框架

This commit is contained in:
2026-09-03 17:59:56 +08:00
parent 5a52b44d46
commit c7ddaecb76
24 changed files with 955 additions and 99 deletions
@@ -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).
}
}
@@ -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