Files
baiyue-firstleg/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java
T
2026-08-12 16:58:45 +08:00

134 lines
6.5 KiB
Java

package com.baiyue.firstleg.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.yaml.snakeyaml.Yaml;
import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("费用管理接口权限契约")
class FeeManagementContractTest {
@Test
@DisplayName("通过和驳回共用审核按钮权限")
void protectsReviewActions() {
for (String methodName : new String[] {"approve", "reject"}) {
Method method = Arrays.stream(PlatformInboundFeeDetailController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(methodName))
.findFirst().orElseThrow();
assertThat(method.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('firstleg:inbound:fee-detail:review')");
}
}
@Test
@DisplayName("费用项目增删改查权限彼此独立")
void separatesCrudPermissions() {
assertPermission("list", "firstleg:fee-item:view");
assertPermission("detail", "firstleg:fee-item:view");
assertPermission("create", "firstleg:fee-item:create");
assertPermission("update", "firstleg:fee-item:edit");
assertPermission("changeStatus", "firstleg:fee-item:edit");
assertPermission("delete", "firstleg:fee-item:delete");
}
@Test
@DisplayName("柜次费用新增和调整请求均携带往来单位快照")
void containerFeeRequestsCarryCounterpartySnapshot() {
for (Class<?> requestType : List.of(
com.baiyue.firstleg.dto.ContainerFeeDetailDtos.SaveRequest.class,
com.baiyue.firstleg.dto.ContainerFeeDetailDtos.AdjustRequest.class)) {
assertThat(Arrays.stream(requestType.getRecordComponents()).map(RecordComponent::getName))
.contains("partnerId", "partnerName");
}
}
@Test
@DisplayName("平台费用项目接口不接受客户作用域")
void platformFeeItemEndpointsDoNotExposeAccountId() {
Arrays.stream(PlatformFeeItemController.class.getDeclaredMethods())
.filter(method -> List.of("list", "detail", "create", "update", "changeStatus", "delete")
.contains(method.getName()))
.flatMap(method -> Arrays.stream(method.getParameters()))
.forEach(parameter -> assertThat(parameter.getName()).isNotEqualTo("accountId"));
}
@Test
@DisplayName("费用项目OpenAPI包含完整响应和空响应契约")
void openApiIsComplete() throws Exception {
String source = new ClassPathResource("openapi/fee-item-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
Map<String, Object> document = stringMap(new Yaml().load(source));
assertThat(document.get("openapi")).isEqualTo("3.0.3");
assertThat(source).doesNotContain("accountId", "AccountId");
Map<String, Object> paths = stringMap(document.get("paths"));
assertThat(paths).containsOnlyKeys(
"/api/platform/v1/fee-items",
"/api/platform/v1/fee-items/{id}",
"/api/platform/v1/fee-items/{id}/status");
assertResponseSchema(paths, "/api/platform/v1/fee-items", "get", "FeeItemPageApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items", "post", "FeeItemApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items/{id}", "get", "FeeItemApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items/{id}", "put", "FeeItemApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items/{id}", "delete", "VoidApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items/{id}/status", "patch", "VoidApiResponse");
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
assertThat(schemas).containsKeys("FeeItemSaveRequest", "FeeItemResource", "FeeItemPage",
"FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse");
for (String response : List.of("FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse")) {
assertThat(list(stringMap(schemas.get(response)).get("required"))).contains("timestamp");
assertThat(properties(schemas, response)).containsKeys(
"code", "message", "data", "requestId", "traceId", "timestamp");
}
assertThat(properties(schemas, "VoidApiResponse").get("data"))
.isInstanceOf(Map.class)
.satisfies(value -> assertThat(stringMap(value)).containsEntry("nullable", true));
assertThat(stringMap(properties(schemas, "FeeItemSaveRequest").get("feeCode")))
.containsEntry("maxLength", 50)
.containsEntry("pattern", "^[A-Za-z0-9_-]+$");
}
private static void assertPermission(String methodName, String permission) {
Method method = Arrays.stream(PlatformFeeItemController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(methodName))
.findFirst().orElseThrow();
assertThat(method.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('" + permission + "')");
}
private static void assertResponseSchema(Map<String, Object> paths, String path,
String operation, String schema) {
Map<String, Object> endpoint = stringMap(paths.get(path));
Map<String, Object> response = stringMap(stringMap(endpoint.get(operation)).get("responses"));
Map<String, Object> success = stringMap(response.get("200"));
Map<String, Object> content = stringMap(success.get("content"));
Map<String, Object> mediaType = stringMap(content.get("application/json"));
assertThat(stringMap(mediaType.get("schema")))
.containsEntry("$ref", "#/components/schemas/" + schema);
}
private static Map<String, Object> properties(Map<String, Object> schemas, String schema) {
return stringMap(stringMap(schemas.get(schema)).get("properties"));
}
@SuppressWarnings("unchecked")
private static Map<String, Object> stringMap(Object value) {
return (Map<String, Object>) value;
}
@SuppressWarnings("unchecked")
private static List<String> list(Object value) {
return (List<String>) value;
}
}