feat(admin): 网关治理小页后端——路由/目标/台账查询与行内编辑

/api/internal/admin/**(RoutingFilter 旁路前缀,零过滤器改动):
- GET page(routes/targets/logs) + PUT routes/{id} / targets/{id}
- 值域白名单(visibility/authMode/cacheScope)非法 400 不落库;写后热重载注册表
- OpenApiLogRepository.pageLatest 两可空过滤+倒序分页
- 10 用例(page/列表/合法编辑/三态 400/limit 截断),全量 75/75 绿
This commit is contained in:
chenxing
2026-09-23 00:12:17 +08:00
parent 50f9948b49
commit 06b0f4ed23
4 changed files with 442 additions and 0 deletions
@@ -0,0 +1,196 @@
package com.yunhai.platform.gateway.admin;
import com.yunhai.platform.gateway.entity.OpenApiLog;
import com.yunhai.platform.gateway.entity.OpenApiRoute;
import com.yunhai.platform.gateway.entity.OpenApiTarget;
import com.yunhai.platform.gateway.repository.OpenApiLogRepository;
import com.yunhai.platform.gateway.repository.OpenApiRouteRepository;
import com.yunhai.platform.gateway.repository.OpenApiTargetRepository;
import com.yunhai.platform.gateway.route.RouteRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.ArrayList;
import java.util.Arrays;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* 治理小页后端(N02 TDD):page/routes/targets/logs 查询形态 + 路由/目标行内编辑的
* 值域闸(非法值 400 且不落库)+ 写后热重载调用。MockMvc standalone + mock 仓储/注册表。
*/
class InternalAdminControllerTest {
private OpenApiRouteRepository routeRepo;
private OpenApiTargetRepository targetRepo;
private OpenApiLogRepository logRepo;
private RouteRegistry registry;
private MockMvc mvc;
@BeforeEach
void setUp() {
routeRepo = Mockito.mock(OpenApiRouteRepository.class);
targetRepo = Mockito.mock(OpenApiTargetRepository.class);
logRepo = Mockito.mock(OpenApiLogRepository.class);
registry = Mockito.mock(RouteRegistry.class);
InternalAdminController controller = new InternalAdminController();
controller.setRepos(routeRepo, targetRepo, logRepo, registry);
mvc = MockMvcBuilders.standaloneSetup(controller).build();
when(routeRepo.findAll()).thenReturn(new ArrayList<>(Arrays.asList(route(1L))));
when(targetRepo.findAll()).thenReturn(new ArrayList<>(Arrays.asList(target(5L))));
when(registry.reload()).thenReturn(22);
when(registry.reloadTargets()).thenReturn(13);
when(logRepo.pageLatest(any(), any(), any(Pageable.class)))
.thenReturn(new PageImpl<>(Arrays.asList(logRow())));
}
@Test
void page_servesHtml() throws Exception {
mvc.perform(get("/api/internal/admin/page"))
.andExpect(status().isOk())
.andExpect(result -> {
String body = result.getResponse().getContentAsString();
org.assertj.core.api.Assertions.assertThat(body).contains("<html");
});
}
@Test
void routes_listSorted() throws Exception {
mvc.perform(get("/api/internal/admin/routes"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].targetCode").value("userbase"))
.andExpect(jsonPath("$[0].pathPattern").value("/oapi/user/**"));
}
@Test
void routeUpdate_legalValue_savesAndReloads() throws Exception {
when(routeRepo.findById(1L)).thenReturn(java.util.Optional.of(route(1L)));
mvc.perform(put("/api/internal/admin/routes/1")
.contentType("application/json")
.content("{\"authMode\":\"ANONYMOUS\",\"cacheTtlSeconds\":30,\"enabled\":true}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.loaded").value(22));
verify(routeRepo).save(any(OpenApiRoute.class));
verify(registry).reload();
}
@Test
void routeUpdate_illegalAuthMode_400AndNoSave() throws Exception {
when(routeRepo.findById(1L)).thenReturn(java.util.Optional.of(route(1L)));
mvc.perform(put("/api/internal/admin/routes/1")
.contentType("application/json")
.content("{\"authMode\":\"OPEN\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.msg").value(org.hamcrest.Matchers.containsString("authMode")));
verify(routeRepo, never()).save(any());
verify(registry, never()).reload();
}
@Test
void routeUpdate_negativeTtl_400() throws Exception {
when(routeRepo.findById(1L)).thenReturn(java.util.Optional.of(route(1L)));
mvc.perform(put("/api/internal/admin/routes/1")
.contentType("application/json")
.content("{\"cacheTtlSeconds\":-1}"))
.andExpect(status().isBadRequest());
verify(routeRepo, never()).save(any());
}
@Test
void routeUpdate_missingRow_400() throws Exception {
when(routeRepo.findById(99L)).thenReturn(java.util.Optional.empty());
mvc.perform(put("/api/internal/admin/routes/99")
.contentType("application/json")
.content("{\"enabled\":false}"))
.andExpect(status().isBadRequest());
}
@Test
void targetUpdate_savesAndReloadsTargets() throws Exception {
when(targetRepo.findById(5L)).thenReturn(java.util.Optional.of(target(5L)));
mvc.perform(put("/api/internal/admin/targets/5")
.contentType("application/json")
.content("{\"baseUrl\":\"http://notice-service:10002\",\"enabled\":true}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.targets").value(13));
verify(targetRepo).save(any(OpenApiTarget.class));
verify(registry).reloadTargets();
}
@Test
void targetUpdate_blankBaseUrl_400NoSave() throws Exception {
when(targetRepo.findById(5L)).thenReturn(java.util.Optional.of(target(5L)));
mvc.perform(put("/api/internal/admin/targets/5")
.contentType("application/json")
.content("{\"baseUrl\":\" \"}"))
.andExpect(status().isBadRequest());
verify(targetRepo, never()).save(any());
}
@Test
void logs_defaultLimitAndFilters() throws Exception {
mvc.perform(get("/api/internal/admin/logs?targetCode=notice&callerAppKey=100001&limit=10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.total").value(1))
.andExpect(jsonPath("$.rows[0].targetCode").value("notice"));
verify(logRepo).pageLatest(eq("notice"), eq("100001"), any(Pageable.class));
}
@Test
void logs_overLimitCappedTo500() throws Exception {
mvc.perform(get("/api/internal/admin/logs?limit=9999"))
.andExpect(status().isOk());
verify(logRepo).pageLatest(any(), any(), Mockito.argThat(p -> p.getPageSize() == 500));
}
private static OpenApiRoute route(Long id) {
OpenApiRoute r = new OpenApiRoute();
r.setId(id);
r.setTargetCode("userbase");
r.setPathPattern("/oapi/user/**");
r.setHttpMethod("*");
r.setVisibility("INTERNAL");
r.setAuthMode("SIGNED");
r.setCacheTtlSeconds(0);
r.setCacheScope("CALLER");
r.setRateLimitQps(0);
r.setEnabled(true);
return r;
}
private static OpenApiTarget target(Long id) {
OpenApiTarget t = new OpenApiTarget();
t.setId(id);
t.setTargetCode("notice");
t.setBaseUrl("http://notice-service:10002");
t.setEnabled(true);
return t;
}
private static OpenApiLog logRow() {
OpenApiLog l = new OpenApiLog();
l.setId(100L);
l.setTargetCode("notice");
l.setMethod("GET");
l.setPath("/oapi/notice/getDict");
l.setStatus(200);
l.setCallerAppKey("100001");
l.setCostMs(5);
return l;
}
}