You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
130 lines
8.3 KiB
130 lines
8.3 KiB
package com.mydreamjob.common.exception;
|
|
|
|
import com.mydreamjob.common.api.ApiResponse;
|
|
import com.mydreamjob.common.logging.RequestIdAccessor;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.params.ParameterizedTest;
|
|
import org.junit.jupiter.params.provider.EnumSource;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.mock.web.MockHttpServletRequest;
|
|
|
|
import java.util.Set;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
/** Locks the pre-R01-B public HTTP contract for every existing business error code. */
|
|
class GlobalExceptionHandlerStatusContractTest {
|
|
|
|
private static final Set<String> FORBIDDEN = Set.of(
|
|
"FORBIDDEN", "INTERVIEW_ACCESS_DENIED", "APPLICATION_OWNER_MISMATCH",
|
|
"MATCH_REQUIRED_FOR_CONVERSATION", "OFFER_COMPARE_NOT_OWNER", "COMPANY_NOT_VERIFIED", "IDENTITY_SELECTION_REQUIRED");
|
|
|
|
private static final Set<String> NOT_FOUND = Set.of(
|
|
"COMPANY_NOT_FOUND", "PROFILE_NOT_FOUND", "JOB_NOT_FOUND", "APPLICATION_NOT_FOUND",
|
|
"OFFER_NOT_FOUND", "OFFER_COUNTER_OFFER_NOT_FOUND", "RESUME_VERSION_NOT_FOUND",
|
|
"RESUME_EXPORT_TASK_NOT_FOUND", "USER_NOT_FOUND", "REVIEW_NOT_FOUND",
|
|
"FEEDBACK_NOT_FOUND", "CONVERSATION_NOT_FOUND", "MESSAGE_NOT_FOUND",
|
|
"APPEAL_NOT_FOUND", "DATA_EXPORT_TASK_NOT_FOUND", "REVIEW_REPLY_NOT_FOUND",
|
|
"TALENT_INVITATION_NOT_FOUND", "VERIFICATION_NOT_FOUND", "INTERVIEW_SCORE_NOT_FOUND",
|
|
"REPORT_NOT_FOUND", "ASSESSMENT_TYPE_NOT_FOUND", "ASSESSMENT_ATTEMPT_NOT_FOUND",
|
|
"ABILITY_TYPE_NOT_FOUND", "ABILITY_ATTEMPT_NOT_FOUND", "ABILITY_RESULT_NOT_FOUND",
|
|
"RISK_RULE_NOT_FOUND", "RISK_EVENT_NOT_FOUND");
|
|
|
|
private static final Set<String> CONFLICT = Set.of(
|
|
"COMPANY_ALREADY_EXISTS", "APPLICATION_DUPLICATE", "INVALID_APPLICATION_STATUS_TRANSITION",
|
|
"INVALID_JOB_STATUS_TRANSITION", "DUPLICATE_REVIEW", "DUPLICATE_FEEDBACK",
|
|
"DUPLICATE_OFFER", "INVALID_OFFER_STATUS_TRANSITION", "OFFER_EXPIRED",
|
|
"OFFER_NOT_EDITABLE", "OFFER_REMINDER_DUPLICATE", "OFFER_COUNTER_OFFER_EXISTS",
|
|
"OFFER_COUNTER_OFFER_NOT_PENDING", "OFFER_COUNTER_OFFER_EXPIRED",
|
|
"DEFAULT_RESUME_DELETE_NOT_ALLOWED", "RESUME_EXPORT_IN_PROGRESS",
|
|
"DUPLICATE_DEFAULT_RESUME", "RESUME_EXPORT_NOT_READY", "MATCH_CLOSED",
|
|
"MATCH_SCORES_LOCKED", "APPLICATION_NOT_MATCHED", "REVIEW_ALREADY_DECIDED",
|
|
"INVALID_INTERVIEW_STATUS_TRANSITION", "INTERVIEW_COMPLETE_NOT_READY",
|
|
"INTERVIEW_ALREADY_RESPONDED", "REVIEW_REPLY_ALREADY_EXISTS",
|
|
"REVIEW_REPLY_INVALID_STATE", "REVIEW_REPLY_CONCURRENT_UPDATE",
|
|
"REVIEW_REPLY_IDEMPOTENCY_CONFLICT", "MESSAGE_RECALL_WINDOW_EXPIRED",
|
|
"MESSAGE_RECALL_CONCURRENT_UPDATE", "TALENT_INVITATION_CANDIDATE_UNAVAILABLE",
|
|
"TALENT_INVITATION_ALREADY_ACTIVE", "TALENT_INVITATION_INVALID_STATE",
|
|
"TALENT_INVITATION_EXPIRED", "TALENT_INVITATION_CONCURRENT_UPDATE",
|
|
"TALENT_INVITATION_IDEMPOTENCY_CONFLICT", "VERIFICATION_ALREADY_PENDING",
|
|
"VERIFICATION_ALREADY_APPROVED", "INVALID_VERIFICATION_STATUS_TRANSITION",
|
|
"REVIEW_WEIGHT_CONCURRENT_UPDATE", "PRIVACY_SETTINGS_CONCURRENT_UPDATE",
|
|
"INVALID_DELETION_STATE", "ACCOUNT_DELETION_PENDING", "ACCOUNT_DELETED",
|
|
"ACCOUNT_DELETION_CONCURRENT_UPDATE", "ACCOUNT_DELETION_CANCEL_DEADLINE_PASSED",
|
|
"LAST_ADMIN_CANNOT_DELETE", "APPEAL_DUPLICATE", "APPEAL_ALREADY_DECIDED",
|
|
"APPEAL_ROLLBACK_LIMIT_EXCEEDED", "APPEAL_INVALID_TRANSITION",
|
|
"APPEAL_IDEMPOTENCY_CONFLICT", "DATA_EXPORT_DUPLICATE", "DATA_EXPORT_NOT_READY",
|
|
"DATA_EXPORT_EXPIRED", "DATA_EXPORT_FAILED", "DATA_EXPORT_FILE_NOT_FOUND",
|
|
"INTERVIEW_SCORE_CONCURRENT_UPDATE", "INTERVIEW_NOT_COMPLETED",
|
|
"INTERVIEW_SCORE_SUBMITTED", "REPORT_DUPLICATE", "REPORT_ALREADY_DECIDED",
|
|
"REPORT_INVALID_TRANSITION", "REPORT_IDEMPOTENCY_CONFLICT",
|
|
"ASSESSMENT_ATTEMPT_INVALID_STATE", "ASSESSMENT_CONCURRENT_UPDATE",
|
|
"ASSESSMENT_DUPLICATE_ACTIVE", "ASSESSMENT_IDEMPOTENCY_CONFLICT",
|
|
"ABILITY_ATTEMPT_INVALID_STATE", "ABILITY_CONCURRENT_UPDATE", "ABILITY_DUPLICATE_ACTIVE",
|
|
"ABILITY_IDEMPOTENCY_CONFLICT", "ABILITY_RETAKE_COOLDOWN",
|
|
"RISK_RULE_CONCURRENT_UPDATE", "RISK_EVENT_CONCURRENT_UPDATE", "RISK_INVALID_ACTION",
|
|
"COMPANY_REGISTRATION_CONFLICT", "COMPANY_ONBOARDING_INCONSISTENT_STATE", "IDENTITY_SELECTION_CONFLICT");
|
|
|
|
private static final Set<String> TOO_MANY_REQUESTS = Set.of(
|
|
"REPORT_RATE_LIMITED", "ABILITY_RETAKE_LIMIT", "TALENT_INVITATION_RATE_LIMITED",
|
|
"PASSWORD_RATE_LIMITED");
|
|
|
|
private static final Set<String> UNAUTHORIZED = Set.of("CREDENTIALS_INVALID");
|
|
private static final Set<String> SERVICE_UNAVAILABLE = Set.of(
|
|
"COMPANY_REGISTRATION_DISABLED", "COMPANY_REGISTRATION_DEPENDENCY_UNAVAILABLE", "IDENTITY_SELECTION_DEPENDENCY_UNAVAILABLE");
|
|
|
|
private final GlobalExceptionHandler handler = new GlobalExceptionHandler(new RequestIdAccessor(), new ErrorHttpStatusRegistry());
|
|
|
|
@ParameterizedTest(name = "{0}")
|
|
@EnumSource(ErrorCode.class)
|
|
void everyExistingErrorCodeKeepsItsHttpStatusAndResponseFields(ErrorCode errorCode) {
|
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
|
request.setRequestURI("/api/v1/contract");
|
|
request.setAttribute("requestId", "r01b-contract");
|
|
|
|
ResponseEntity<ApiResponse<Void>> response =
|
|
handler.handleBusinessException(new BusinessException(errorCode), request);
|
|
|
|
assertEquals(expectedStatus(errorCode), response.getStatusCode(), errorCode.name());
|
|
assertNotNull(response.getBody());
|
|
assertEquals(errorCode.getCode(), response.getBody().getCode());
|
|
assertEquals(errorCode.getMessage(), response.getBody().getMessage());
|
|
assertEquals("r01b-contract", response.getBody().getRequestId());
|
|
}
|
|
|
|
@Test
|
|
void unknownBusinessCodeKeepsBadRequestFallback() {
|
|
// R01-B/B2:解析入口由 GlobalExceptionHandler 的私有方法收束到 ErrorHttpStatusRegistry,
|
|
// 断言目标(未知码仍回退 400)保持不变,不再依赖反射。
|
|
assertEquals(HttpStatus.BAD_REQUEST, new ErrorHttpStatusRegistry().resolve(123456789));
|
|
}
|
|
|
|
private static final java.util.Map<String, HttpStatus> MODULE_ADDITIONS = java.util.Map.ofEntries(
|
|
java.util.Map.entry("ONBOARDING_CHECKLIST_ITEM_NOT_FOUND", HttpStatus.NOT_FOUND),
|
|
java.util.Map.entry("SALARY_CITY_NOT_FOUND", HttpStatus.NOT_FOUND),
|
|
java.util.Map.entry("SALARY_HISTORY_NOT_FOUND", HttpStatus.NOT_FOUND),
|
|
java.util.Map.entry("ONBOARDING_CHECKLIST_ITEM_CONCURRENT_UPDATE", HttpStatus.CONFLICT),
|
|
java.util.Map.entry("ONBOARDING_ITEM_LIMIT_EXCEEDED", HttpStatus.CONFLICT),
|
|
java.util.Map.entry("ONBOARDING_IDEMPOTENCY_CONFLICT", HttpStatus.CONFLICT),
|
|
java.util.Map.entry("ONBOARDING_PROGRESS_CONCURRENT_UPDATE", HttpStatus.CONFLICT),
|
|
java.util.Map.entry("ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE", HttpStatus.CONFLICT),
|
|
java.util.Map.entry("SALARY_IDEMPOTENCY_CONFLICT", HttpStatus.CONFLICT),
|
|
java.util.Map.entry("SALARY_POLICY_VERSION_CONFLICT", HttpStatus.CONFLICT),
|
|
java.util.Map.entry("RESUME_MESSAGE_UNAVAILABLE", HttpStatus.CONFLICT),
|
|
java.util.Map.entry("SALARY_INVALID_PARAMETER", HttpStatus.BAD_REQUEST));
|
|
|
|
private HttpStatus expectedStatus(ErrorCode errorCode) {
|
|
String name = errorCode.name();
|
|
if (MODULE_ADDITIONS.containsKey(name)) return MODULE_ADDITIONS.get(name);
|
|
if (FORBIDDEN.contains(name)) return HttpStatus.FORBIDDEN;
|
|
if (NOT_FOUND.contains(name)) return HttpStatus.NOT_FOUND;
|
|
if (CONFLICT.contains(name)) return HttpStatus.CONFLICT;
|
|
if (TOO_MANY_REQUESTS.contains(name)) return HttpStatus.TOO_MANY_REQUESTS;
|
|
if ("PASSWORD_STATE_CONFLICT".equals(name)) return HttpStatus.CONFLICT;
|
|
if (UNAUTHORIZED.contains(name)) return HttpStatus.UNAUTHORIZED;
|
|
if (SERVICE_UNAVAILABLE.contains(name)) return HttpStatus.SERVICE_UNAVAILABLE;
|
|
return HttpStatus.BAD_REQUEST;
|
|
}
|
|
}
|
|
|