feat(admin): 管理面信封化+分页化,OpenAPI 文档端点旁路(B1-B6)
build / build-push (push) Successful in 1m59s

- pom: 引 yh-platform-dependencies(import) / yh-api-std / springdoc-openapi-ui 1.7.0 + nexus-public 仓库
- RoutingFilter.isGatewayInternal: /v3/api-docs、/swagger-ui/**、/swagger-ui.html 纳入自身路径旁路,避免被 TargetRouter 当 target 裸 404
- InternalAdminController: page/routes/targets/logs 三接口分页化返回 BasePageResult;补 @Operation(operationId=GatewayAdmin.*);错误契约去 @ResponseStatus(400) 改 200+code=2;logs 兼容旧 limit 入参
- RouteRefreshController: 补 @Tag/@Operation(operationId=GatewayAdmin.refresh)
- static/admin.html: 适配 {code,msg,data} 信封与 content/total 分页
- InternalAdminControllerTest: 重写 12 用例,77 项测试全绿
This commit is contained in:
chenxing
2026-09-23 12:17:02 +08:00
parent 6e64acc0c7
commit 2389d2fa9a
6 changed files with 198 additions and 67 deletions
+36
View File
@@ -23,6 +23,19 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<!-- 基础平台版本管理(yh-* 架包与 spring-boot-dependencies 对齐);形态同 UserBase/Notice -->
<dependencies>
<dependency>
<groupId>com.yunhai.platform</groupId>
<artifactId>yh-platform-dependencies</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 白名单(开放接口中心 T0.8):本清单外依赖不得引入 -->
<dependency>
@@ -77,6 +90,19 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 接口规范:管理面(/api/internal/**)成功响应统一包装为 {code,msg,data},与 notice/userbase
前端解析形态一致;同时传递引入 yh-utils-core(ErrorCode / BasePageQuery / BasePageResult)。
oapi 转发链不经 Controller(Filter 直写 response),不受本 starter 的 ResponseBodyAdvice 影响。 -->
<dependency>
<groupId>com.yunhai.platform</groupId>
<artifactId>yh-api-std</artifactId>
</dependency>
<!-- Swagger / OpenAPI 文档(springdoc,兼容 Boot 2.7;版本对齐 UserBase/Notice) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
</dependencies>
<build>
@@ -95,4 +121,14 @@
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>nexus-public</id>
<name>bsch/nexus public</name>
<url>http://nexus.yunhaixingtong.com/repository/maven-public/</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
</project>
@@ -1,5 +1,6 @@
package com.yunhai.platform.gateway.admin;
import com.yunhai.platform.apistd.ApiResponse;
import com.yunhai.platform.gateway.entity.OpenApiLog;
import com.yunhai.platform.gateway.entity.OpenApiRoute;
import com.yunhai.platform.gateway.entity.OpenApiTarget;
@@ -7,12 +8,18 @@ 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 com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BasePageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
@@ -24,24 +31,27 @@ 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),
* 台账分页查询;写操作落库后立即热重载注册表(routes→reload / targets→reloadTargets),
* 无须重启网关。与 {@code RouteRefreshController} 同前缀,被 RoutingFilter 旁路(不进 oapi 链)。
* <p>
* <b>响应契约</b>:成功响应由 {@code yh-api-std} 的 {@code ApiResponseAdvice} 统一包成
* {@code {code:0, msg:"", data:...}}(与 notice/userbase 一致);非法入参走 {@link #badRequest}
* 返回 HTTP 200 + {@code {code:2, msg:...}}(平台规范:业务错误不改 HTTP 状态码)。
* <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 且不落库。
* authMode∈{SIGNED,ANONYMOUS}、cacheScope∈{CALLER,GLOBAL};非法值返回 code=2 且不落库。
*/
@Tag(name = "GatewayAdmin", description = "网关治理:路由旋钮 / 目标地址 / 访问台账")
@RestController
@RequestMapping("/api/internal/admin")
public class InternalAdminController {
@@ -71,28 +81,29 @@ public class InternalAdminController {
this.routeRegistry = routeRegistry;
}
/** 治理小页本体(classpath:/static/admin.html,N03 产出) */
/** 治理小页本体(classpath:/static/admin.html,N03 产出);返回 String 故不被信封包装 */
@Operation(operationId = "GatewayAdmin.page", summary = "治理小页(内置 HTML)")
@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);
}
/** 路由表(治理对象全量,含禁用行;排序稳定便于表格展示) */
/** 路由表分页(治理对象全量含禁用行;targetCode+pathPattern 稳定排序) */
@Operation(operationId = "GatewayAdmin.routes", summary = "路由注册表分页")
@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;
public BasePageResult<OpenApiRoute> routes(
@RequestParam(value = "pageIndex", required = false, defaultValue = "1") Integer pageIndex,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize) {
Sort sort = Sort.by("targetCode").and(Sort.by("pathPattern"));
return toResult(routeRepo.findAll(pageRequest(pageIndex, pageSize, sort)));
}
/**
* 行内编辑路由治理旋钮。只接受治理字段(不可改 target/pattern/method——那是路由身份,
* 改动应走 SQL 迁移评审);enabled 可整体停用一行。成功后热重载并返回加载数。
*/
@Operation(operationId = "GatewayAdmin.updateRoute", summary = "编辑路由治理旋钮")
@PutMapping("/routes/{id}")
public Map<String, Object> updateRoute(@PathVariable("id") Long id,
@RequestBody Map<String, Object> body) {
@@ -148,15 +159,17 @@ public class InternalAdminController {
return out;
}
/** 目标地址表全量 */
/** 目标地址表分页 */
@Operation(operationId = "GatewayAdmin.targets", summary = "目标地址分页")
@GetMapping("/targets")
public List<OpenApiTarget> targets() {
List<OpenApiTarget> all = targetRepo.findAll();
all.sort((a, b) -> a.getTargetCode().compareTo(b.getTargetCode()));
return all;
public BasePageResult<OpenApiTarget> targets(
@RequestParam(value = "pageIndex", required = false, defaultValue = "1") Integer pageIndex,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize) {
return toResult(targetRepo.findAll(pageRequest(pageIndex, pageSize, Sort.by("targetCode"))));
}
/** 行内编辑目标地址/启停;成功后热重载 target 快照(禁用/空白行按无行回落 yml 由 TargetRouter 承担) */
@Operation(operationId = "GatewayAdmin.updateTarget", summary = "编辑目标地址/启停")
@PutMapping("/targets/{id}")
public Map<String, Object> updateTarget(@PathVariable("id") Long id,
@RequestBody Map<String, Object> body) {
@@ -181,32 +194,61 @@ public class InternalAdminController {
return out;
}
/** 台账倒序查询(limit 默认 50 上限 500;targetCode/callerAppKey 可空) */
/**
* 台账倒序分页查询。{@code pageSize} 优先、上限 500;兼容旧 {@code limit} 参数
* (未传 pageSize 时以 limit 作 pageSize)。targetCode/callerAppKey 可空=不过滤。
*/
@Operation(operationId = "GatewayAdmin.logs", summary = "访问台账分页")
@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;
public BasePageResult<OpenApiLog> logs(
@RequestParam(value = "pageIndex", required = false, defaultValue = "1") Integer pageIndex,
@RequestParam(value = "pageSize", required = false) Integer pageSize,
@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "targetCode", required = false) String targetCode,
@RequestParam(value = "callerAppKey", required = false) String callerAppKey) {
int size = resolvePageSize(pageSize, limit);
int page = normalizePageIndex(pageIndex);
Page<OpenApiLog> result = logRepo.pageLatest(emptyToNull(targetCode), emptyToNull(callerAppKey),
PageRequest.of(page - 1, size));
return toResult(result);
}
/**
* 值域/参数错误 → 400 + JSON 错误体(页面 alert 展示)。本类抛出的 IllegalArgumentException
* 值域/参数错误 → HTTP 200 + {@code {code:2, msg}}(平台规范:业务错误不改 HTTP 状态码,
* 与 notice/userbase 的 GlobalExceptionHandler 行为一致)。本类抛出的 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;
@ExceptionHandler(IllegalArgumentException.class)
public ApiResponse<Object> badRequest(IllegalArgumentException e) {
return ApiResponse.fail(ErrorCode.PARAM_ERROR, e.getMessage());
}
private static <T> BasePageResult<T> toResult(Page<T> page) {
return new BasePageResult<T>(
page.getContent(),
(int) page.getTotalElements(),
page.getNumber() + 1,
page.getSize(),
page.getTotalPages(),
page.hasNext());
}
private static PageRequest pageRequest(Integer pageIndex, Integer pageSize, Sort sort) {
return PageRequest.of(normalizePageIndex(pageIndex) - 1, normalizePageSize(pageSize), sort);
}
private static int normalizePageIndex(Integer pageIndex) {
return pageIndex == null || pageIndex < 1 ? 1 : pageIndex;
}
private static int normalizePageSize(Integer pageSize) {
return pageSize == null || pageSize < 1 ? 10 : pageSize;
}
/** pageSize 优先;兼容旧 limit 参数,上限 500 */
private static int resolvePageSize(Integer pageSize, Integer limit) {
Integer v = pageSize != null ? pageSize : limit;
return v == null || v < 1 ? 50 : Math.min(v, 500);
}
private static String text(Object v) {
@@ -37,6 +37,16 @@ public class RoutingFilter implements Filter {
*/
public static final String ACTUATOR_PREFIX = "/actuator";
/**
* OpenAPI 文档端点(B2):springdoc 的 {@code /v3/api-docs}、{@code /swagger-ui/**} 属网关自身路径,
* 须与 {@link #INTERNAL_PREFIX} 同级旁路——否则首段({@code v3} / {@code swagger-ui.html})会被
* {@link TargetRouter} 当作 target 解析 → 未知 target 裸 404,文档端点不可达。
*/
public static final String API_DOCS_PREFIX = "/v3/api-docs";
public static final String SWAGGER_UI_PREFIX = "/swagger-ui";
/** springdoc UI 传统入口;{@link #isSelfPath} 的 equals+前缀双条件均不覆盖此路径,须单独 equals 判定 */
public static final String SWAGGER_UI_HTML = "/swagger-ui.html";
/** 路由产物 attribute({@link TargetRouter.Routing}) */
public static final String ATTR_ROUTING = "yh.gateway.routing";
/** 注册表策略 attribute({@link RouteSpec}) */
@@ -89,7 +99,11 @@ public class RoutingFilter implements Filter {
}
static boolean isGatewayInternal(String uri) {
return isSelfPath(uri, INTERNAL_PREFIX) || isSelfPath(uri, ACTUATOR_PREFIX);
return isSelfPath(uri, INTERNAL_PREFIX)
|| isSelfPath(uri, ACTUATOR_PREFIX)
|| isSelfPath(uri, API_DOCS_PREFIX)
|| isSelfPath(uri, SWAGGER_UI_PREFIX)
|| (uri != null && uri.equals(SWAGGER_UI_HTML));
}
private static boolean isSelfPath(String uri, String prefix) {
@@ -1,5 +1,7 @@
package com.yunhai.platform.gateway.route;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -12,6 +14,7 @@ import java.util.Map;
* 网关内部管理端点(T0.10):/api/internal/** 属网关自身路径,收网后仅容器网内可达,
* 不注册进 open_api_route(注册表只治理 /{target}/oapi/**)。
*/
@Tag(name = "GatewayAdmin", description = "网关治理:路由旋钮 / 目标地址 / 访问台账")
@RestController
@RequestMapping("/api/internal")
public class RouteRefreshController {
@@ -20,6 +23,7 @@ public class RouteRefreshController {
private RouteRegistry routeRegistry;
/** 清缓存重 load,返回加载数(routes = 路由条目,targets = DB 地址行,E11 同端点重载) */
@Operation(operationId = "GatewayAdmin.refresh", summary = "热重载路由注册表与目标地址")
@PostMapping("/routes/refresh")
public Map<String, Object> refresh() {
Map<String, Object> body = new LinkedHashMap<>();
+19 -13
View File
@@ -106,6 +106,7 @@ function toast(msg, isErr) {
setTimeout(function(){ t.className = ''; }, isErr ? 4200 : 2200);
}
/* 管理面响应 = 平台信封 {code,msg,data}(yh-api-std 统一包装);成功时把 data 交给调用方 */
function api(method, url, body) {
var opt = { method: method, headers: {} };
if (body !== undefined) {
@@ -113,9 +114,10 @@ function api(method, url, body) {
opt.body = JSON.stringify(body);
}
return fetch(url, opt).then(function(r) {
return r.json().catch(function(){ return {}; }).then(function(j) {
if (!r.ok) { throw new Error(j.msg || ('HTTP ' + r.status)); }
return j;
if (!r.ok) { throw new Error('HTTP ' + r.status); }
return r.json().then(function(j) {
if (j.code !== 0) { throw new Error(j.msg || ('业务错误 code=' + j.code)); }
return j.data;
});
});
}
@@ -128,7 +130,8 @@ function esc(s) {
/* ---------- 路由表 ---------- */
function loadRoutes() {
return api('GET', 'routes').then(function(rows) {
return api('GET', 'routes?pageIndex=1&pageSize=200').then(function(pg) {
var rows = (pg && pg.content) || [];
var tb = $('routeT').tBodies[0];
tb.innerHTML = rows.map(function(r) { return routeRow(r); }).join('');
});
@@ -167,13 +170,14 @@ function saveRoute(btn) {
else { body[k] = el.value; }
});
api('PUT', 'routes/' + tr.dataset.id, body)
.then(function(j) { toast('已保存,注册表加载 ' + j.loaded + ' 条路由'); })
.then(function(d) { toast('已保存,注册表加载 ' + d.loaded + ' 条路由'); })
.catch(function(e) { toast('保存失败:' + e.message, true); });
}
/* ---------- 目标表 ---------- */
function loadTargets() {
return api('GET', 'targets').then(function(rows) {
return api('GET', 'targets?pageIndex=1&pageSize=200').then(function(pg) {
var rows = (pg && pg.content) || [];
var tb = $('tgtT').tBodies[0];
tb.innerHTML = rows.map(function(t) {
return '<tr data-id="' + t.id + '">'
@@ -195,23 +199,25 @@ function saveTarget(btn) {
body[k] = el.type === 'checkbox' ? el.checked : el.value;
});
api('PUT', 'targets/' + tr.dataset.id, body)
.then(function(j) { toast('已保存,目标表加载 ' + j.targets + ' 条'); })
.then(function(d) { toast('已保存,目标表加载 ' + d.targets + ' 条'); })
.catch(function(e) { toast('保存失败:' + e.message, true); });
}
/* ---------- 台账 ---------- */
function loadLogs() {
var q = 'logs?limit=' + (parseInt($('qLimit').value || '50', 10) || 50);
var size = parseInt($('qLimit').value || '50', 10) || 50;
var q = 'logs?pageIndex=1&pageSize=' + size;
if ($('qTarget').value.trim()) q += '&targetCode=' + encodeURIComponent($('qTarget').value.trim());
if ($('qCaller').value.trim()) q += '&callerAppKey=' + encodeURIComponent($('qCaller').value.trim());
return api('GET', q).then(function(j) {
$('logTotal').textContent = '共 ' + j.total + ' 条';
return api('GET', q).then(function(pg) {
$('logTotal').textContent = '共 ' + pg.total + ' 条';
var rows = (pg && pg.content) || [];
var tb = $('logT').tBodies[0];
if (!j.rows || !j.rows.length) {
if (!rows.length) {
tb.innerHTML = '<tr><td colspan="9" class="idle">无匹配记录</td></tr>';
return;
}
tb.innerHTML = j.rows.map(function(l) {
tb.innerHTML = rows.map(function(l) {
var st = l.status;
var cls = st >= 500 ? 'st5xx' : (st >= 400 ? 'st4xx' : 'st2');
return '<tr>'
@@ -230,7 +236,7 @@ function loadLogs() {
/* ---------- 顶部 ---------- */
function reloadRegistry() {
api('POST', '../routes/refresh')
.then(function(j) { toast('注册表已重载:路由 ' + j.loaded + ' 条 / 目标 ' + j.targets + ' 条'); })
.then(function(d) { toast('注册表已重载:路由 ' + d.loaded + ' 条 / 目标 ' + d.targets + ' 条'); })
.catch(function(e) { toast('重载失败:' + e.message, true); });
}
@@ -15,7 +15,6 @@ 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;
@@ -30,7 +29,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
/**
* 治理小页后端(N02 TDD):page/routes/targets/logs 查询形态 + 路由/目标行内编辑的
* 值域闸(非法值 400 且不落库)+ 写后热重载调用。MockMvc standalone + mock 仓储/注册表。
* 值域闸(非法值不落库)+ 写后热重载调用。MockMvc standalone + mock 仓储/注册表。
* <p>
* <b>契约说明(B3/B4 后)</b>:查询接口返回 {@code BasePageResult}({@code {total, content, ...}}),
* 非法入参返回 <b>HTTP 200</b> + {@code {code:2, msg}}(平台规范)。standalone 模式不装配
* {@code yh-api-std} 的 {@code ApiResponseAdvice},故成功响应在本测试中保持裸结构;
* 线上由 advice 统一再包一层 {@code {code,msg,data}}(分页体落在 {@code data})。
*/
class InternalAdminControllerTest {
@@ -51,8 +55,10 @@ class InternalAdminControllerTest {
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(routeRepo.findAll(any(Pageable.class)))
.thenReturn(new PageImpl<>(Arrays.asList(route(1L))));
when(targetRepo.findAll(any(Pageable.class)))
.thenReturn(new PageImpl<>(Arrays.asList(target(5L))));
when(registry.reload()).thenReturn(22);
when(registry.reloadTargets()).thenReturn(13);
when(logRepo.pageLatest(any(), any(), any(Pageable.class)))
@@ -73,8 +79,19 @@ class InternalAdminControllerTest {
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/**"));
.andExpect(jsonPath("$.total").value(1))
.andExpect(jsonPath("$.pageIndex").value(1))
.andExpect(jsonPath("$.content[0].targetCode").value("userbase"))
.andExpect(jsonPath("$.content[0].pathPattern").value("/oapi/user/**"));
}
@Test
void targets_paged() throws Exception {
mvc.perform(get("/api/internal/admin/targets?pageIndex=1&pageSize=20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.total").value(1))
.andExpect(jsonPath("$.content[0].targetCode").value("notice"));
verify(targetRepo).findAll(Mockito.<Pageable>argThat(p -> p.getPageSize() == 20 && p.getPageNumber() == 0));
}
@Test
@@ -90,34 +107,37 @@ class InternalAdminControllerTest {
}
@Test
void routeUpdate_illegalAuthMode_400AndNoSave() throws Exception {
void routeUpdate_illegalAuthMode_bizErrorAndNoSave() 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(status().isOk())
.andExpect(jsonPath("$.code").value(2))
.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 {
void routeUpdate_negativeTtl_bizError() 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());
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(2));
verify(routeRepo, never()).save(any());
}
@Test
void routeUpdate_missingRow_400() throws Exception {
void routeUpdate_missingRow_bizError() 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());
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(2));
}
@Test
@@ -133,12 +153,13 @@ class InternalAdminControllerTest {
}
@Test
void targetUpdate_blankBaseUrl_400NoSave() throws Exception {
void targetUpdate_blankBaseUrl_bizErrorNoSave() 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());
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(2));
verify(targetRepo, never()).save(any());
}
@@ -147,10 +168,18 @@ class InternalAdminControllerTest {
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"));
.andExpect(jsonPath("$.content[0].targetCode").value("notice"));
verify(logRepo).pageLatest(eq("notice"), eq("100001"), any(Pageable.class));
}
@Test
void logs_pageIndexAndSize() throws Exception {
mvc.perform(get("/api/internal/admin/logs?pageIndex=2&pageSize=20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.total").value(1));
verify(logRepo).pageLatest(any(), any(), Mockito.argThat(p -> p.getPageSize() == 20 && p.getPageNumber() == 1));
}
@Test
void logs_overLimitCappedTo500() throws Exception {
mvc.perform(get("/api/internal/admin/logs?limit=9999"))