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:
@@ -0,0 +1,226 @@
|
|||||||
|
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.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关治理小页后端(/api/internal/admin/**,D5 最小治理面):路由/目标地址的查询与行内编辑、
|
||||||
|
* 台账倒序查询;写操作落库后立即热重载注册表(routes→reload / targets→reloadTargets),
|
||||||
|
* 无须重启网关。与 {@code RouteRefreshController} 同前缀,被 RoutingFilter 旁路(不进 oapi 链)。
|
||||||
|
* <p>
|
||||||
|
* <b>收网前提示</b>:本前缀当前宿主端口可达(10013 未绑 127.0.0.1),页面用于 observe 期
|
||||||
|
* 治理与排障;T0.2 端口收网随下次重新部署自然收紧(届时仅容器网内可达)。
|
||||||
|
* 值域白名单与 {@code docs/sql/01_schema.sql} 注释对齐:visibility∈{INTERNAL,EXTERNAL}、
|
||||||
|
* authMode∈{SIGNED,ANONYMOUS}、cacheScope∈{CALLER,GLOBAL};非法值 400 且不落库。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/internal/admin")
|
||||||
|
public class InternalAdminController {
|
||||||
|
|
||||||
|
private static final Set<String> VISIBILITY = new HashSet<>(Arrays.asList("INTERNAL", "EXTERNAL"));
|
||||||
|
private static final Set<String> AUTH_MODE = new HashSet<>(Arrays.asList("SIGNED", "ANONYMOUS"));
|
||||||
|
private static final Set<String> CACHE_SCOPE = new HashSet<>(Arrays.asList("CALLER", "GLOBAL"));
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OpenApiRouteRepository routeRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OpenApiTargetRepository targetRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OpenApiLogRepository logRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RouteRegistry routeRegistry;
|
||||||
|
|
||||||
|
/** 测试/显式装配入口 */
|
||||||
|
void setRepos(OpenApiRouteRepository routeRepo, OpenApiTargetRepository targetRepo,
|
||||||
|
OpenApiLogRepository logRepo, RouteRegistry routeRegistry) {
|
||||||
|
this.routeRepo = routeRepo;
|
||||||
|
this.targetRepo = targetRepo;
|
||||||
|
this.logRepo = logRepo;
|
||||||
|
this.routeRegistry = routeRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 治理小页本体(classpath:/static/admin.html,N03 产出) */
|
||||||
|
@GetMapping(value = "/page", produces = MediaType.TEXT_HTML_VALUE)
|
||||||
|
public String page() throws IOException {
|
||||||
|
return StreamUtils.copyToString(
|
||||||
|
new ClassPathResource("static/admin.html").getInputStream(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 路由表(治理对象全量,含禁用行;排序稳定便于表格展示) */
|
||||||
|
@GetMapping("/routes")
|
||||||
|
public List<OpenApiRoute> routes() {
|
||||||
|
List<OpenApiRoute> all = routeRepo.findAll();
|
||||||
|
all.sort((a, b) -> {
|
||||||
|
int c = a.getTargetCode().compareTo(b.getTargetCode());
|
||||||
|
return c != 0 ? c : a.getPathPattern().compareTo(b.getPathPattern());
|
||||||
|
});
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行内编辑路由治理旋钮。只接受治理字段(不可改 target/pattern/method——那是路由身份,
|
||||||
|
* 改动应走 SQL 迁移评审);enabled 可整体停用一行。成功后热重载并返回加载数。
|
||||||
|
*/
|
||||||
|
@PutMapping("/routes/{id}")
|
||||||
|
public Map<String, Object> updateRoute(@PathVariable("id") Long id,
|
||||||
|
@RequestBody Map<String, Object> body) {
|
||||||
|
OpenApiRoute route = routeRepo.findById(id).orElse(null);
|
||||||
|
if (route == null) {
|
||||||
|
throw new IllegalArgumentException("路由不存在:" + id);
|
||||||
|
}
|
||||||
|
if (body.containsKey("visibility")) {
|
||||||
|
String v = text(body.get("visibility"));
|
||||||
|
if (!VISIBILITY.contains(v)) {
|
||||||
|
throw new IllegalArgumentException("visibility 非法:" + v + "(合法 " + VISIBILITY + ")");
|
||||||
|
}
|
||||||
|
route.setVisibility(v);
|
||||||
|
}
|
||||||
|
if (body.containsKey("authMode")) {
|
||||||
|
String v = text(body.get("authMode"));
|
||||||
|
if (!AUTH_MODE.contains(v)) {
|
||||||
|
throw new IllegalArgumentException("authMode 非法:" + v + "(合法 " + AUTH_MODE + ")");
|
||||||
|
}
|
||||||
|
route.setAuthMode(v);
|
||||||
|
}
|
||||||
|
if (body.containsKey("cacheTtlSeconds")) {
|
||||||
|
int v = intVal(body.get("cacheTtlSeconds"), "cacheTtlSeconds");
|
||||||
|
if (v < 0) {
|
||||||
|
throw new IllegalArgumentException("cacheTtlSeconds 不可为负:" + v);
|
||||||
|
}
|
||||||
|
route.setCacheTtlSeconds(v);
|
||||||
|
}
|
||||||
|
if (body.containsKey("cacheScope")) {
|
||||||
|
String v = text(body.get("cacheScope"));
|
||||||
|
if (!CACHE_SCOPE.contains(v)) {
|
||||||
|
throw new IllegalArgumentException("cacheScope 非法:" + v + "(合法 " + CACHE_SCOPE + ")");
|
||||||
|
}
|
||||||
|
route.setCacheScope(v);
|
||||||
|
}
|
||||||
|
if (body.containsKey("rateLimitQps")) {
|
||||||
|
int v = intVal(body.get("rateLimitQps"), "rateLimitQps");
|
||||||
|
if (v < 0) {
|
||||||
|
throw new IllegalArgumentException("rateLimitQps 不可为负:" + v);
|
||||||
|
}
|
||||||
|
route.setRateLimitQps(v);
|
||||||
|
}
|
||||||
|
if (body.containsKey("enabled")) {
|
||||||
|
route.setEnabled(Boolean.TRUE.equals(body.get("enabled")));
|
||||||
|
}
|
||||||
|
if (body.containsKey("remark")) {
|
||||||
|
route.setRemark(text(body.get("remark")));
|
||||||
|
}
|
||||||
|
route.setGmtModified(LocalDateTime.now());
|
||||||
|
routeRepo.save(route);
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("loaded", routeRegistry.reload());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 目标地址表全量 */
|
||||||
|
@GetMapping("/targets")
|
||||||
|
public List<OpenApiTarget> targets() {
|
||||||
|
List<OpenApiTarget> all = targetRepo.findAll();
|
||||||
|
all.sort((a, b) -> a.getTargetCode().compareTo(b.getTargetCode()));
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 行内编辑目标地址/启停;成功后热重载 target 快照(禁用/空白行按无行回落 yml 由 TargetRouter 承担) */
|
||||||
|
@PutMapping("/targets/{id}")
|
||||||
|
public Map<String, Object> updateTarget(@PathVariable("id") Long id,
|
||||||
|
@RequestBody Map<String, Object> body) {
|
||||||
|
OpenApiTarget target = targetRepo.findById(id).orElse(null);
|
||||||
|
if (target == null) {
|
||||||
|
throw new IllegalArgumentException("目标不存在:" + id);
|
||||||
|
}
|
||||||
|
if (body.containsKey("baseUrl")) {
|
||||||
|
String v = text(body.get("baseUrl"));
|
||||||
|
if (v == null || v.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("baseUrl 不可为空");
|
||||||
|
}
|
||||||
|
target.setBaseUrl(v.trim());
|
||||||
|
}
|
||||||
|
if (body.containsKey("enabled")) {
|
||||||
|
target.setEnabled(Boolean.TRUE.equals(body.get("enabled")));
|
||||||
|
}
|
||||||
|
target.setGmtModified(LocalDateTime.now());
|
||||||
|
targetRepo.save(target);
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("targets", routeRegistry.reloadTargets());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 台账倒序查询(limit 默认 50 上限 500;targetCode/callerAppKey 可空) */
|
||||||
|
@GetMapping("/logs")
|
||||||
|
public Map<String, Object> logs(@RequestParam(value = "limit", required = false) Integer limit,
|
||||||
|
@RequestParam(value = "targetCode", required = false) String targetCode,
|
||||||
|
@RequestParam(value = "callerAppKey", required = false) String callerAppKey) {
|
||||||
|
int size = limit == null || limit < 1 ? 50 : Math.min(limit, 500);
|
||||||
|
Page<OpenApiLog> page = logRepo.pageLatest(emptyToNull(targetCode), emptyToNull(callerAppKey),
|
||||||
|
PageRequest.of(0, size));
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("total", page.getTotalElements());
|
||||||
|
out.put("rows", new ArrayList<>(page.getContent()));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 值域/参数错误 → 400 + JSON 错误体(页面 alert 展示)。本类抛出的 IllegalArgumentException
|
||||||
|
* 全是"调用方传值非法",缺省会被 Spring 映射成 500,误导排障方向——故在类内显式收敛。
|
||||||
|
*/
|
||||||
|
@org.springframework.web.bind.annotation.ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
@org.springframework.web.bind.annotation.ResponseStatus(
|
||||||
|
org.springframework.http.HttpStatus.BAD_REQUEST)
|
||||||
|
public Map<String, Object> badRequest(IllegalArgumentException e) {
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("code", 1);
|
||||||
|
out.put("msg", e.getMessage());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String text(Object v) {
|
||||||
|
return v == null ? null : String.valueOf(v).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int intVal(Object v, String field) {
|
||||||
|
if (!(v instanceof Number)) {
|
||||||
|
throw new IllegalArgumentException(field + " 须为数字:" + v);
|
||||||
|
}
|
||||||
|
return ((Number) v).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String emptyToNull(String s) {
|
||||||
|
return s == null || s.trim().isEmpty() ? null : s.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,22 @@
|
|||||||
package com.yunhai.platform.gateway.repository;
|
package com.yunhai.platform.gateway.repository;
|
||||||
|
|
||||||
import com.yunhai.platform.gateway.entity.OpenApiLog;
|
import com.yunhai.platform.gateway.entity.OpenApiLog;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
public interface OpenApiLogRepository extends JpaRepository<OpenApiLog, Long> {
|
public interface OpenApiLogRepository extends JpaRepository<OpenApiLog, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 台账倒序分页(治理小页查询;targetCode/callerAppKey 可空=不过滤)。
|
||||||
|
* 可空参数走 (:x is null or ...) 形态与既有分页查询一致。
|
||||||
|
*/
|
||||||
|
@Query("select l from OpenApiLog l where (:targetCode is null or l.targetCode = :targetCode) "
|
||||||
|
+ "and (:callerAppKey is null or l.callerAppKey = :callerAppKey) "
|
||||||
|
+ "order by l.id desc")
|
||||||
|
Page<OpenApiLog> pageLatest(@Param("targetCode") String targetCode,
|
||||||
|
@Param("callerAppKey") String callerAppKey,
|
||||||
|
Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head><meta charset="UTF-8"><title>网关治理</title></head>
|
||||||
|
<body><!-- N03 将替换为完整治理页 --></body>
|
||||||
|
</html>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user