feat: 用户 UUID 持久化与统一异常契约(Flyway baseline / UUIDv7 / 错误码透传)
- Flyway V1 baseline:identity 全部 5 表 + media.assets + platform 最小硬依赖(set_updated_at/regions),无 fixture 凭据;dev 种子独立为 afterMigrate 回调且默认不执行 - patbond-user 迁移到 PostgreSQL:UUIDv7 主键、JdbcClient 仓储、bcrypt 密码、唯一性依赖 DB 约束翻译为 409、E.164 手机号校验对齐 ck_users_phone - 统一异常契约:common 新增 ErrorCode/BusinessException,两服务 GlobalExceptionHandler;auth 经 ApiErrorDecoder 原码透传下游错误,修复状态码折叠(审计 M1) - 门禁:JAVA_HOME=jdk17 ./mvnw clean test,37 个测试 0 失败(含 Testcontainers postgres:16 集成测试),BUILD SUCCESS Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package com.patbond.patbond.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.patbond.patbond.common.error.BusinessException;
|
||||
import com.patbond.patbond.common.error.ErrorCode;
|
||||
import com.patbond.patbond.common.response.ApiResponse;
|
||||
import feign.Response;
|
||||
import feign.Util;
|
||||
import feign.codec.ErrorDecoder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Translates non-2xx replies from downstream services back into
|
||||
* {@link BusinessException}, so the business code, message and HTTP status
|
||||
* the user service chose reach the client unchanged instead of collapsing
|
||||
* into 500/400 (audit issue M1). Anything that does not carry a readable
|
||||
* {@code {code, message}} envelope is reported as DOWNSTREAM_UNAVAILABLE.
|
||||
*/
|
||||
public class ApiErrorDecoder implements ErrorDecoder {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ApiErrorDecoder(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Exception decode(String methodKey, Response response) {
|
||||
try {
|
||||
if (response.body() != null) {
|
||||
String body = Util.toString(response.body().asReader(Util.UTF_8));
|
||||
ApiResponse<?> envelope = objectMapper.readValue(body, ApiResponse.class);
|
||||
if (envelope != null && envelope.getCode() != null && envelope.getCode() != 0) {
|
||||
return new BusinessException(envelope.getCode(), response.status(), envelope.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (IOException | RuntimeException ignored) {
|
||||
// Not a Patbond envelope; fall through to the generic error below.
|
||||
}
|
||||
return new BusinessException(ErrorCode.DOWNSTREAM_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.patbond.patbond.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class FeignConfig {
|
||||
|
||||
@Bean
|
||||
public ErrorDecoder apiErrorDecoder(ObjectMapper objectMapper) {
|
||||
return new ApiErrorDecoder(objectMapper);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
package com.patbond.patbond.auth.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AuthTokenResponse {
|
||||
|
||||
private String tokenType;
|
||||
private String accessToken;
|
||||
private LocalDateTime expiresAt;
|
||||
private Long userId;
|
||||
private UUID userId;
|
||||
private String username;
|
||||
private String nickname;
|
||||
|
||||
public AuthTokenResponse(String tokenType, String accessToken, LocalDateTime expiresAt,
|
||||
Long userId, String username, String nickname) {
|
||||
UUID userId, String username, String nickname) {
|
||||
this.tokenType = tokenType;
|
||||
this.accessToken = accessToken;
|
||||
this.expiresAt = expiresAt;
|
||||
@@ -33,7 +34,7 @@ public class AuthTokenResponse {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
public UUID getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.patbond.patbond.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class RegisterRequest {
|
||||
@@ -16,7 +17,8 @@ public class RegisterRequest {
|
||||
@Size(max = 32, message = "昵称长度不能超过32位")
|
||||
private String nickname;
|
||||
|
||||
@Size(max = 20, message = "手机号长度不能超过20位")
|
||||
// Aligned with the identity.users ck_users_phone CHECK constraint (E.164).
|
||||
@Pattern(regexp = "^\\+[1-9][0-9]{7,14}$", message = "手机号必须为 E.164 格式,例如 +8613800138000")
|
||||
private String phone;
|
||||
|
||||
public String getUsername() {
|
||||
|
||||
@@ -4,14 +4,14 @@ import com.patbond.patbond.auth.client.UserClient;
|
||||
import com.patbond.patbond.auth.dto.AuthTokenResponse;
|
||||
import com.patbond.patbond.auth.dto.LoginRequest;
|
||||
import com.patbond.patbond.auth.dto.RegisterRequest;
|
||||
import com.patbond.patbond.common.error.BusinessException;
|
||||
import com.patbond.patbond.common.error.ErrorCode;
|
||||
import com.patbond.patbond.common.response.ApiResponse;
|
||||
import com.patbond.patbond.common.user.CreateUserRequest;
|
||||
import com.patbond.patbond.common.user.UserProfile;
|
||||
import com.patbond.patbond.common.user.VerifyPasswordRequest;
|
||||
import com.patbond.patbond.common.user.VerifyPasswordResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
@@ -44,7 +44,7 @@ public class AuthService {
|
||||
return buildToken(user.getUserId(), user.getUsername(), user.getNickname());
|
||||
}
|
||||
|
||||
private AuthTokenResponse buildToken(Long userId, String username, String nickname) {
|
||||
private AuthTokenResponse buildToken(UUID userId, String username, String nickname) {
|
||||
return new AuthTokenResponse(
|
||||
"Bearer",
|
||||
UUID.randomUUID().toString().replace("-", ""),
|
||||
@@ -55,10 +55,15 @@ public class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downstream business failures arrive as BusinessException via the Feign
|
||||
* ErrorDecoder and never reach this method; this only guards against a
|
||||
* 2xx reply with a malformed envelope.
|
||||
*/
|
||||
private <T> T requireData(ApiResponse<T> response, String defaultMessage) {
|
||||
if (response == null || !response.isSuccess() || response.getData() == null) {
|
||||
String message = response == null || response.getMessage() == null ? defaultMessage : response.getMessage();
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||
throw new BusinessException(ErrorCode.INTERNAL_ERROR, message);
|
||||
}
|
||||
return response.getData();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.patbond.patbond.auth.web;
|
||||
|
||||
import com.patbond.patbond.common.error.BusinessException;
|
||||
import com.patbond.patbond.common.error.ErrorCode;
|
||||
import com.patbond.patbond.common.response.ApiResponse;
|
||||
import feign.FeignException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
/**
|
||||
* Auth-side error contract. BusinessException covers both local failures and
|
||||
* downstream errors re-raised by {@code ApiErrorDecoder}; a raw FeignException
|
||||
* only remains for transport-level failures (connect refused, timeout), which
|
||||
* are reported as 503 instead of leaking a stack trace as 500.
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException e) {
|
||||
return ResponseEntity.status(e.getHttpStatus())
|
||||
.body(ApiResponse.failure(e.getCode(), e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(FeignException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleFeign(FeignException e) {
|
||||
log.error("User service call failed", e);
|
||||
return failure(ErrorCode.DOWNSTREAM_UNAVAILABLE, ErrorCode.DOWNSTREAM_UNAVAILABLE.getDefaultMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException e) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.findFirst()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.orElse(ErrorCode.VALIDATION_ERROR.getDefaultMessage());
|
||||
return failure(ErrorCode.VALIDATION_ERROR, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentTypeMismatchException.class})
|
||||
public ResponseEntity<ApiResponse<Void>> handleMalformedRequest(Exception e) {
|
||||
return failure(ErrorCode.VALIDATION_ERROR, ErrorCode.VALIDATION_ERROR.getDefaultMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleNoResource(NoResourceFoundException e) {
|
||||
return ResponseEntity.status(404).body(ApiResponse.failure(40400, "资源不存在"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleUnexpected(Exception e) {
|
||||
log.error("Unhandled exception", e);
|
||||
return failure(ErrorCode.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getDefaultMessage());
|
||||
}
|
||||
|
||||
private static ResponseEntity<ApiResponse<Void>> failure(ErrorCode errorCode, String message) {
|
||||
return ResponseEntity.status(errorCode.getHttpStatus())
|
||||
.body(ApiResponse.failure(errorCode.getCode(), message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.patbond.patbond.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.patbond.patbond.common.error.BusinessException;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ApiErrorDecoderTest {
|
||||
|
||||
private final ApiErrorDecoder decoder = new ApiErrorDecoder(new ObjectMapper());
|
||||
|
||||
private static Response response(int status, String body) {
|
||||
Request request = Request.create(Request.HttpMethod.POST, "/internal/users",
|
||||
Map.of(), null, StandardCharsets.UTF_8, null);
|
||||
Response.Builder builder = Response.builder().status(status).request(request);
|
||||
if (body != null) {
|
||||
builder.body(body, StandardCharsets.UTF_8);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void passesThroughDownstreamBusinessCodeStatusAndMessage() {
|
||||
Exception decoded = decoder.decode("UserClient#createUser",
|
||||
response(409, "{\"code\":40900,\"message\":\"用户名已存在\",\"data\":null}"));
|
||||
|
||||
assertThat(decoded).isInstanceOfSatisfying(BusinessException.class, e -> {
|
||||
assertThat(e.getCode()).isEqualTo(40900);
|
||||
assertThat(e.getHttpStatus()).isEqualTo(409);
|
||||
assertThat(e.getMessage()).isEqualTo("用户名已存在");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToDownstreamUnavailableForNonEnvelopeBody() {
|
||||
Exception decoded = decoder.decode("UserClient#createUser",
|
||||
response(500, "<html>gateway error</html>"));
|
||||
|
||||
assertThat(decoded).isInstanceOfSatisfying(BusinessException.class, e -> {
|
||||
assertThat(e.getCode()).isEqualTo(50300);
|
||||
assertThat(e.getHttpStatus()).isEqualTo(503);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToDownstreamUnavailableForEmptyBody() {
|
||||
Exception decoded = decoder.decode("UserClient#verifyPassword", response(500, null));
|
||||
|
||||
assertThat(decoded).isInstanceOfSatisfying(BusinessException.class,
|
||||
e -> assertThat(e.getCode()).isEqualTo(50300));
|
||||
}
|
||||
}
|
||||
+52
-29
@@ -1,6 +1,8 @@
|
||||
package com.patbond.patbond.auth.controller;
|
||||
|
||||
import com.patbond.patbond.auth.client.UserClient;
|
||||
import com.patbond.patbond.common.error.BusinessException;
|
||||
import com.patbond.patbond.common.error.ErrorCode;
|
||||
import com.patbond.patbond.common.response.ApiResponse;
|
||||
import com.patbond.patbond.common.user.UserProfile;
|
||||
import com.patbond.patbond.common.user.VerifyPasswordResponse;
|
||||
@@ -16,10 +18,10 @@ 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.time.OffsetDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
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;
|
||||
@@ -28,10 +30,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* a Mockito mock so no user-service process is required. Downstream business
|
||||
* failures are simulated as the BusinessException the ApiErrorDecoder raises,
|
||||
* so these tests pin the FIXED error contract: 409/401/400 pass through to
|
||||
* the client with stable business codes instead of collapsing to 400/500
|
||||
* (audit issue M1); transport-level Feign failures answer 503.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@@ -43,8 +46,10 @@ class AuthControllerTest {
|
||||
@MockitoBean
|
||||
private UserClient userClient;
|
||||
|
||||
private static final UUID USER_ID = UUID.fromString("019212aa-0000-7000-8000-000000000001");
|
||||
|
||||
private static final String REGISTER_BODY = """
|
||||
{"username":"alice","password":"secret123","nickname":"Alice","phone":"13800000000"}
|
||||
{"username":"alice","password":"secret123","nickname":"Alice","phone":"+8613800138000"}
|
||||
""";
|
||||
private static final String LOGIN_BODY = """
|
||||
{"username":"alice","password":"secret123"}
|
||||
@@ -52,7 +57,7 @@ class AuthControllerTest {
|
||||
|
||||
@Test
|
||||
void registerReturnsTokenWhenUserServiceSucceeds() throws Exception {
|
||||
UserProfile profile = new UserProfile(1L, "alice", "Alice", null, LocalDateTime.now());
|
||||
UserProfile profile = new UserProfile(USER_ID, "alice", "Alice", null, OffsetDateTime.now());
|
||||
when(userClient.createUser(any())).thenReturn(ApiResponse.success(profile));
|
||||
|
||||
mockMvc.perform(post("/auth/register")
|
||||
@@ -62,18 +67,21 @@ class AuthControllerTest {
|
||||
.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.userId").value(USER_ID.toString()))
|
||||
.andExpect(jsonPath("$.data.username").value("alice"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerReturnsBadRequestWhenUserServiceReportsFailure() throws Exception {
|
||||
when(userClient.createUser(any())).thenReturn(ApiResponse.failure(409, "用户名已存在"));
|
||||
void registerPropagatesDuplicateUsernameAsConflict() throws Exception {
|
||||
when(userClient.createUser(any()))
|
||||
.thenThrow(new BusinessException(ErrorCode.USERNAME_EXISTS));
|
||||
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(REGISTER_BODY))
|
||||
.andExpect(status().isBadRequest());
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value(40900))
|
||||
.andExpect(jsonPath("$.message").value("用户名已存在"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -81,13 +89,25 @@ class AuthControllerTest {
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"username\":\"ab\",\"password\":\"123\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value(40000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerRejectsNonE164Phone() throws Exception {
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"username":"alice","password":"secret123","phone":"13800138000"}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value(40000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginReturnsTokenWhenPasswordVerified() throws Exception {
|
||||
when(userClient.verifyPassword(any()))
|
||||
.thenReturn(ApiResponse.success(new VerifyPasswordResponse(1L, "alice", "Alice")));
|
||||
.thenReturn(ApiResponse.success(new VerifyPasswordResponse(USER_ID, "alice", "Alice")));
|
||||
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -95,17 +115,20 @@ class AuthControllerTest {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.userId").value(1));
|
||||
.andExpect(jsonPath("$.data.userId").value(USER_ID.toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginReturnsBadRequestWhenVerificationReportsFailure() throws Exception {
|
||||
when(userClient.verifyPassword(any())).thenReturn(ApiResponse.failure(401, "用户名或密码错误"));
|
||||
void loginPropagatesWrongPasswordAsUnauthorized() throws Exception {
|
||||
when(userClient.verifyPassword(any()))
|
||||
.thenThrow(new BusinessException(ErrorCode.INVALID_CREDENTIALS));
|
||||
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(LOGIN_BODY))
|
||||
.andExpect(status().isBadRequest());
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.code").value(40100))
|
||||
.andExpect(jsonPath("$.message").value("用户名或密码错误"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -113,22 +136,22 @@ class AuthControllerTest {
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"username\":\"\",\"password\":\"\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value(40000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginPropagatesFeignExceptionUnhandled() throws Exception {
|
||||
void loginAnswersServiceUnavailableOnTransportLevelFeignFailure() 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);
|
||||
FeignException transportFailure = FeignException.errorStatus("UserClient#verifyPassword",
|
||||
Response.builder().status(502).request(request).build());
|
||||
when(userClient.verifyPassword(any())).thenThrow(transportFailure);
|
||||
|
||||
// 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);
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(LOGIN_BODY))
|
||||
.andExpect(status().isServiceUnavailable())
|
||||
.andExpect(jsonPath("$.code").value(50300));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user