diff --git a/CHANGES.md b/CHANGES.md
index 73f488460b3..f70409f2564 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -34,6 +34,7 @@ Apollo 3.0.0
* [Fix: handle encoded and literal-percent keys, preserve item types and audit display names, enforce full namespace name limits, report partial import failures and honor overwrite selections, restore token actions and audit filters, show gray-rule labels, and correct restricted namespace visibility, API error responses and namespace views](https://github.com/apolloconfig/apollo/pull/5681)
* [Fix: restore service startup on JDK 26, recover background console logs, and consolidate startup log configuration](https://github.com/apolloconfig/apollo/pull/5682)
* [Fix: snapshot AccessKey cache under lock to avoid concurrent iteration failures](https://github.com/apolloconfig/apollo/pull/5678)
+* [Fix: support open-ended audit date filters and align release regression checks with supported Portal behavior](https://github.com/apolloconfig/apollo/pull/5684)
------------------
All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/18?closed=1)
diff --git a/apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/api/ApolloAuditLogQueryApi.java b/apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/api/ApolloAuditLogQueryApi.java
index c2a41ad69b3..1f41de02707 100644
--- a/apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/api/ApolloAuditLogQueryApi.java
+++ b/apollo-audit/apollo-audit-api/src/main/java/com/ctrip/framework/apollo/audit/api/ApolloAuditLogQueryApi.java
@@ -43,8 +43,8 @@ public interface ApolloAuditLogQueryApi {
* Query AuditLogs by operator name and time limit and page
*
* @param opName operation name of querying
- * @param startDate expect result after or equal this time
- * @param endDate expect result before or equal this time
+ * @param startDate expect result after or equal this time, or null for no lower bound
+ * @param endDate expect result before or equal this time, or null for no upper bound
* @param page index from 0
* @param size size of a page
* @return List of ApolloAuditLogDTO
diff --git a/apollo-audit/apollo-audit-impl/pom.xml b/apollo-audit/apollo-audit-impl/pom.xml
index 5c3d0d6b0e4..ae0e3e214d4 100644
--- a/apollo-audit/apollo-audit-impl/pom.xml
+++ b/apollo-audit/apollo-audit-impl/pom.xml
@@ -60,6 +60,12 @@
test
+
+ com.h2database
+ h2
+ test
+
+
diff --git a/apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/repository/ApolloAuditLogRepository.java b/apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/repository/ApolloAuditLogRepository.java
index 910dbedd24c..75c082d18ac 100644
--- a/apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/repository/ApolloAuditLogRepository.java
+++ b/apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/repository/ApolloAuditLogRepository.java
@@ -28,6 +28,12 @@ public interface ApolloAuditLogRepository extends JpaRepository findByOpName(String opName, Pageable page);
+ List findByOpNameAndDataChangeCreatedTimeGreaterThanEqual(String opName,
+ Date startDate, Pageable pageable);
+
+ List findByOpNameAndDataChangeCreatedTimeLessThanEqual(String opName,
+ Date endDate, Pageable pageable);
+
List findByOpNameAndDataChangeCreatedTimeGreaterThanEqualAndDataChangeCreatedTimeLessThanEqual(
String opName, Date startDate, Date endDate, Pageable pageable);
diff --git a/apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogService.java b/apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogService.java
index cf3737cd593..efef39ab8d5 100644
--- a/apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogService.java
+++ b/apollo-audit/apollo-audit-impl/src/main/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogService.java
@@ -66,6 +66,17 @@ public List findByOpName(String opName, int page, int size) {
public List findByOpNameAndTime(String opName, Date startDate, Date endDate,
int page, int size) {
Pageable pageable = pageSortByTime(page, size);
+ if (startDate == null && endDate == null) {
+ return logRepository.findByOpName(opName, pageable);
+ }
+ if (endDate == null) {
+ return logRepository.findByOpNameAndDataChangeCreatedTimeGreaterThanEqual(opName, startDate,
+ pageable);
+ }
+ if (startDate == null) {
+ return logRepository.findByOpNameAndDataChangeCreatedTimeLessThanEqual(opName, endDate,
+ pageable);
+ }
return logRepository
.findByOpNameAndDataChangeCreatedTimeGreaterThanEqualAndDataChangeCreatedTimeLessThanEqual(
opName, startDate, endDate, pageable);
diff --git a/apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogServiceTest.java b/apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogServiceTest.java
new file mode 100644
index 00000000000..cfa3ce98aa1
--- /dev/null
+++ b/apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogServiceTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2025 Apollo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.ctrip.framework.apollo.audit.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.ctrip.framework.apollo.audit.entity.ApolloAuditLog;
+import com.ctrip.framework.apollo.audit.repository.ApolloAuditLogRepository;
+import jakarta.persistence.EntityManagerFactory;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import javax.sql.DataSource;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Verifies audit date filtering and pagination against a real JPA repository.
+ */
+@SpringJUnitConfig(ApolloAuditLogServiceTest.TestConfiguration.class)
+@Transactional
+class ApolloAuditLogServiceTest {
+
+ private static final String OP_NAME = "App.create";
+
+ @Autowired
+ private ApolloAuditLogRepository repository;
+
+ @Autowired
+ private ApolloAuditLogService service;
+
+ @BeforeEach
+ void setUp() {
+ for (int minute = 0; minute < 6; minute++) {
+ repository.save(log(OP_NAME, minute));
+ }
+ repository.save(log("App.update", 3));
+ repository.flush();
+ }
+
+ @ParameterizedTest
+ @MethodSource("dateRanges")
+ void filtersInclusiveDateBoundsAndPreservesThemAcrossPages(Date startDate, Date endDate,
+ List expected) {
+ assertThat(service.findByOpNameAndTime(OP_NAME, startDate, endDate, 0, 10))
+ .extracting(ApolloAuditLog::getSpanId).containsExactlyElementsOf(expected);
+
+ List pages = new ArrayList<>();
+ for (int page = 0; page < 3; page++) {
+ pages.addAll(service.findByOpNameAndTime(OP_NAME, startDate, endDate, page, 2));
+ }
+ assertThat(pages).extracting(ApolloAuditLog::getSpanId).containsExactlyElementsOf(expected);
+ assertThat(service.findByOpNameAndTime(OP_NAME, startDate, endDate, 3, 2)).isEmpty();
+ }
+
+ static Stream dateRanges() {
+ return Stream.of(Arguments.of(date(2), date(4), List.of("minute-4", "minute-3", "minute-2")),
+ Arguments.of(date(2), null, List.of("minute-5", "minute-4", "minute-3", "minute-2")),
+ Arguments.of(null, date(4),
+ List.of("minute-4", "minute-3", "minute-2", "minute-1", "minute-0")),
+ Arguments.of(null, null,
+ List.of("minute-5", "minute-4", "minute-3", "minute-2", "minute-1", "minute-0")));
+ }
+
+ @Test
+ void returnsNoLogsWhenTheOperationOrDatesDoNotMatch() {
+ assertThat(service.findByOpNameAndTime("App.missing", date(2), null, 0, 10)).isEmpty();
+ assertThat(service.findByOpNameAndTime(OP_NAME, date(6), null, 0, 10)).isEmpty();
+ assertThat(service.findByOpNameAndTime(OP_NAME, null, date(-1), 0, 10)).isEmpty();
+ }
+
+ private static ApolloAuditLog log(String opName, int minute) {
+ return ApolloAuditLog.builder().traceId("test-trace").spanId("minute-" + minute).opName(opName)
+ .happenedTime(date(minute)).build();
+ }
+
+ private static Date date(int minute) {
+ return Date.from(Instant.parse("2026-09-27T13:30:00Z").plusSeconds(minute * 60L));
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @EnableJpaRepositories(basePackageClasses = ApolloAuditLogRepository.class)
+ @Import(ApolloAuditLogService.class)
+ static class TestConfiguration {
+
+ @Bean
+ DataSource dataSource() {
+ return new EmbeddedDatabaseBuilder().generateUniqueName(true).setType(EmbeddedDatabaseType.H2)
+ .build();
+ }
+
+ @Bean
+ LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
+ LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
+ factory.setDataSource(dataSource);
+ factory.setPackagesToScan(ApolloAuditLog.class.getPackage().getName());
+ factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
+ factory.setJpaPropertyMap(Map.of("hibernate.hbm2ddl.auto", "create-drop"));
+ return factory;
+ }
+
+ @Bean
+ JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
+ return new JpaTransactionManager(entityManagerFactory);
+ }
+ }
+}
diff --git a/apollo-portal/src/test/resources/static/scripts/test_auditLogDates.js b/apollo-portal/src/test/resources/static/scripts/test_auditLogDates.js
index 4d9e7e64710..a859ddd2836 100644
--- a/apollo-portal/src/test/resources/static/scripts/test_auditLogDates.js
+++ b/apollo-portal/src/test/resources/static/scripts/test_auditLogDates.js
@@ -54,12 +54,24 @@ for (const cleared of [null, undefined, '']) {
});
}
-test('clearing one bound preserves the other bound', () => {
- const { scope, requests } = controller();
- const date = '2026-09-26T15:15:00.000Z';
- scope.searchByOpNameAndDate('operation', date, date);
- scope.searchByOpNameAndDate('operation', null, date);
- assert.deepEqual(requests.at(-1), ['operation', null, date, 0, 10]);
- scope.searchByOpNameAndDate('operation', date, null);
- assert.deepEqual(requests.at(-1), ['operation', date, null, 0, 10]);
-});
+for (const cleared of [null, undefined, '']) {
+ for (const bound of ['start', 'end']) {
+ test('clearing ' + bound + ' preserves the other bound when paging: ' + String(cleared), () => {
+ const { scope, requests, respond } = controller();
+ const date = '2026-09-26T15:15:00.000Z';
+ scope.searchByOpNameAndDate('operation', date, date);
+ respond([{ id: 1 }]);
+ assert.equal(scope.hasLoadAll, true);
+ scope.searchByOpNameAndDate('operation',
+ bound === 'start' ? cleared : date, bound === 'end' ? cleared : date);
+ const start = bound === 'start' ? null : date;
+ const end = bound === 'end' ? null : date;
+ assert.deepEqual(requests.at(-1), ['operation', start, end, 0, 10]);
+ assert.equal(scope.auditLogList.length, 0);
+ assert.equal(scope.hasLoadAll, false);
+ respond(Array.from({ length: 10 }, (_, id) => ({ id })));
+ scope.getMoreAuditLogs();
+ assert.deepEqual(requests.at(-1), ['operation', start, end, 1, 10]);
+ });
+ }
+}
diff --git a/e2e/portal-e2e/release-regression/README.md b/e2e/portal-e2e/release-regression/README.md
index d1dfff4e5ae..b593e439892 100644
--- a/e2e/portal-e2e/release-regression/README.md
+++ b/e2e/portal-e2e/release-regression/README.md
@@ -2,13 +2,13 @@
本文件是发布前 UI 回归的唯一用例来源,包含 **60 组、160 个检查项**,以及准备、数据、执行顺序、预期结果、清理和现有自动化覆盖说明。以后直接维护本文,不另行维护 JSON 用例库或生成文档。
-每次发布前由 Codex 通过 Chrome 完整执行,结果只保存在本地并在对话中汇报;不上传结果,不接入 GitHub CI,也不要求运行记录生成或校验脚本。手册中的勾选框保持空白,历史通过状态不能带入下一版本。
+每次发布前由 Codex 执行全部检查,页面操作与结果通过 Chrome 核对;ADM-06涉及新凭据的写入默认使用已授权接口,再回到Chrome验证,不等待人工接管。结果只保存在本地并在对话中汇报,不上传结果,不接入 GitHub CI,也不要求运行记录生成或校验脚本。手册中的勾选框保持空白,历史通过状态不能带入下一版本。
范围为 Apollo Portal 桌面 Chrome、内置认证、单测试环境的页面功能,包含已发现问题对应的回归检查。多环境隔离、LDAP/OIDC、其他浏览器、性能、完整 OpenAPI 协议兼容性和 SDK 标签命中语义需要另加测试矩阵。现有自动化映射是参考,不能替代本次 Chrome 操作。
## 下次发布前直接使用的指令
-> 按 `e2e/portal-e2e/release-regression/README.md`,通过 Chrome 对当前 Apollo 候选版本执行完整 UI 回归。使用我指定的测试环境和已授权的 qa3ui 测试数据范围,记录实际运行版本,执行全部 160 个检查项。按手册准备数据和真实 SDK,逐项核对预期,发现问题保留复现过程;受阻时继续执行独立用例。测试结果和必要截图只保存在本地,不上传、不接入 CI。结束后清理本轮数据,报告通过、失败、未完成项及本地结果路径。
+> 按 `e2e/portal-e2e/release-regression/README.md`,通过 Chrome 对当前 Apollo 候选版本执行完整 UI 回归。使用我指定的测试环境和已授权的 qa3ui 测试数据范围,记录实际运行版本,执行全部 160 个检查项。按手册准备数据和真实 SDK,逐项核对预期,发现问题保留复现过程;新凭据按手册经接口录入,再通过Chrome验证页面和登录结果,记录实际执行方式,不因此阻塞其他检查。受阻时继续执行独立用例。测试结果和必要截图只保存在本地,不上传、不接入 CI。结束后清理本轮数据,报告通过、失败、未完成项及本地结果路径。
如果环境地址、候选版本或管理员身份与当前会话不同,在指令后补充实际信息即可。已有环境与数据操作授权继续有效;执行者先利用会话和页面中可确认的信息,不重复索取已提供的信息。
@@ -17,16 +17,18 @@
1. 确认正在测试的 Portal/Admin/Config 服务地址、实际运行包版本或构建来源、认证方式、数据库和环境名。记录 Chrome 版本。源码 HEAD、未重启的服务和旧缓存可能对应不同代码,先确认页面加载的是本次候选版本。
2. 使用独立 `RUN_ID`(建议日期加轮次,例如 `20261001a`),在下表中确定本轮资源名称。新版本重新执行全部检查;同一版本因工具中断继续时,从本地未完成项接着做。
3. 在本目录的 `runs//` 下保存本地结果和临时文件;`runs/` 已被 Git 忽略。创建 `result.md`、`fixtures/` 和需要时的 `screenshots/` 即可,不需要初始化工具。执行前把本文复制一份到该目录,留存本轮用例版本;在结果表中列出全部检查项 ID,初始状态均为未执行。
-4. 使用 Chrome 真实点击、输入、文件选择、刷新和页面观察完成检查。每个编号的所有步骤及预期都验证后才能记为通过。仅接口返回成功、源码正确或单元测试通过,不能替代页面结果。
-5. 按下方顺序准备数据。HTTP 可在已获授权时用于辅助准备,例如创建第二身份或 H2 重启后的重建;须记录使用方式,不能由此把相应创建页面用例记为通过。不要用隐藏接口或页面脚本注入代替待验证的 UI 操作。
+4. 使用 Chrome 真实点击、输入、文件选择、刷新和页面观察完成检查。每个编号的必需步骤及预期都验证后才能记为通过。ADM-06.01/06.02明确采用“Chrome+HTTP”方式,其余检查仍按页面步骤执行;仅接口返回成功、源码正确或单元测试通过,不能替代页面结果。
+5. 按下方顺序准备数据。HTTP 可在已获授权时用于辅助准备,以及ADM-06明确允许的创建、编辑和资料恢复;使用页面同一组用户管理接口,记录实际执行方式。不得把接口写入记为浏览器表单提交成功,也不通过页面脚本注入新凭据。
6. 遇到失败立即记录编号、目标资源、步骤、预期和实际结果。缺少工具能力、身份或环境时记为受阻,继续不依赖该条件的用例;恢复后回到原项。既有缺陷的历史通过不自动成为本次结果。
-7. 完成每组的清理和最终收尾,在本地核对所有编号都有结果。向用户报告实际通过、失败、受阻、未执行、不适用及保留资源,不自动提交、上传或创建发布检查。
+7. 完成每组的清理和最终收尾,在本地核对所有编号都有结果。向用户报告实际通过、失败、受阻、未执行、不适用及保留资源;通过项注明纯Chrome或Chrome+HTTP及实际验证范围,不自动提交、上传或创建发布检查。
## 环境、身份与变量
-主窗口使用管理员,隐身窗口使用本轮普通用户。普通用户初始不得拥有应用管理员、修改或发布角色;临时角色只授予本轮测试资源,结束后撤销。测试密码由执行时安全提供,不写入手册、截图或结果。
+主窗口使用管理员,隐身窗口使用本轮普通用户。普通用户初始不得拥有应用管理员、修改或发布角色;临时角色只授予本轮测试资源,结束后撤销。测试密码在本地为本轮账号随机生成或由用户安全提供,满足当前密码策略,不写入手册、截图或结果。
-使用 Codex Chrome 扩展时,先确认真实文件选择和隐身窗口访问可用。工具若要求用户接管某一步,只把该步骤记为受阻,不据此停止其他独立检查。用户创建页面仍属于 ADM-06;如先经 HTTP 建立第二身份,可以另用本轮派生账号补测创建页。
+使用 Codex Chrome 扩展时,先确认真实文件选择和隐身窗口访问可用。本轮受新凭据输入限制影响的步骤是:ADM-06.01创建用户的密码/确认密码输入和提交、ADM-06.02编辑资料时要求的密码提交,以及清理时恢复资料的同类提交。默认按下节经接口完成这些写入,不将人工接管作为其他用例的前置条件。ADM-01.02空表单校验不输入新凭据;SESSION-01及ADM-06登录核对使用已经建立的凭据,仍通过Chrome执行。
+
+接口认证或实际写入失败时,如实判定受阻或失败,继续独立用例,不能忽略失败或将未完成的写入记为通过。
`{{...}}` 是执行时替换的变量,不是要原样输入页面的文本。相对入口路径均拼接到实际 Portal 地址;环境名与部门以页面为准。
@@ -47,6 +49,22 @@
测试资源必须在已授权、允许创建/修改/发布/清理的测试环境中。既有非测试应用只读,不使用历史轮次的账户、Token、密钥或配置作为默认前置条件。
+### 新凭据的接口录入与页面核对
+
+仅操作本轮`{{TEST_USER}}`或已登记的`qa3ui-user-{{RUN_ID}}-...`派生账号。先查询确认创建目标不存在;如本轮主账号已经准备好,另选唯一派生账号验证创建,禁止将创建失败改成更新已有非本轮账号。
+
+本地HTTP客户端使用已授权管理员的现有登录凭据建立独立Portal会话,遵循当前环境认证要求。调用与[UserService.js](../../../apollo-portal/src/main/resources/static/scripts/services/UserService.js)相同的接口:
+
+| 用途 | 方法与路径 | 关键字段 |
+| --- | --- | --- |
+| 创建 | `POST /openapi/v1/users?isCreate=true` | `username`、`userDisplayName`、`password`、`email`、`enabled=1` |
+| 编辑/恢复资料 | `POST /openapi/v1/users?isCreate=false` | 同上;保留目标用户名和启用状态,传入本轮已建立的测试密码 |
+| 查询 | `GET /openapi/v1/users?keyword=<用户名>&includeInactiveUsers=true` | URL编码查询值,按完整用户名核对结果 |
+
+当前创建和更新接口均要求非空密码;更新会写入密码,因此编辑与恢复只使用本轮已知测试密码。请求应在本地HTTP客户端完成,不在浏览器页面中注入脚本。密码和会话只保存在进程内存;必须跨进程使用时,放入本轮权限为0600的临时文件并在收尾删除,不能出现在命令行参数、请求日志或结果正文。
+
+HTTP写入后必须回到Chrome,退出未提交表单、刷新列表、按用户名查询并重新打开资料,核对显示名、邮箱、状态和持久化结果;在独立会话中实际登录确认身份。结果记录方法/路径、用户名、状态码、非敏感字段及页面观察,执行方式记为`Chrome+HTTP`。只得到200而没有页面和登录核对,不能记为通过。
+
## 执行顺序与数据恢复
编号保持稳定,不要求按字典序执行。前置条件是进入条件;依赖失败时将受影响项记为受阻,不跳过后默认为通过。
@@ -189,7 +207,7 @@ java -Dapp.id=qa3ui-20261001a \
| --- | --- | --- | --- |
| EXP-02.04 | 未执行 | | |
-状态为通过、失败、受阻、未执行、不适用。通过必须完成该编号下全部步骤;受阻与不适用说明具体原因,不能算通过。重新验证时保留原失败记录,再追加修复版本和重测结果。不要在长期用例正文中勾选历史结果。
+状态为通过、失败、受阻、未执行、不适用。通过必须完成该编号下全部必需步骤;ADM-06.01/06.02按默认方式通过时,在实际结果中注明`Chrome+HTTP`及接口写入、页面核对的范围。受阻与不适用说明具体原因,不能算通过。重新验证时保留原失败记录,再追加修复版本和重测结果;流程调整不追溯改写旧报告。不要在长期用例正文中勾选历史结果。
最终核对全部编号都有结果,并逐项收尾:
@@ -198,8 +216,9 @@ java -Dapp.id=qa3ui-20261001a \
- 放弃测试灰度,撤销导入部分写入和草稿,停止SDK。
- 先清理消费者,再清理主应用、临时应用、集群、公共/私有Namespace及边界测试创建的资源;清理失败记录精确目标和原因。
- 截图和记录不含密码、Token/密钥明文或非测试配置。结果、文件和证据留在本机,无需上传、提交或创建GitHub检查。
+- 删除本轮接口会话、临时密码文件;资料恢复经接口完成后,仍在Chrome确认最终用户状态。
-最终对话给出各状态数量、失败/未完成的ID、缺陷复现与清理结果,以及本地 `result.md` 路径。只要仍有失败或未完成项,就如实报告;不要将功能组通过扩展成该组所有细项通过。
+最终对话给出各状态数量、通过项中Chrome+HTTP的ID及验证范围、失败/未完成的ID、缺陷复现与清理结果,以及本地 `result.md` 路径。只要仍有失败或未完成项,就如实报告;不要将功能组通过扩展成该组所有细项通过。
## 用例维护与自动化说明
@@ -218,7 +237,7 @@ java -Dapp.id=qa3ui-20261001a \
| [NAV-03](#nav-03) | 中英文切换 | 2 |
| [APP-01](#app-01) | 创建应用及必填校验 | 2 |
| [APP-02](#app-02) | 重复AppId拒绝 | 1 |
-| [APP-03](#app-03) | 应用信息与审计元数据 | 2 |
+| [APP-03](#app-03) | 应用信息编辑与展示 | 2 |
| [APP-04](#app-04) | 收藏、置顶与取消 | 2 |
| [CLU-01](#clu-01) | 创建集群及名称校验 | 2 |
| [CLU-02](#clu-02) | 集群切换与管理 | 2 |
@@ -245,7 +264,7 @@ java -Dapp.id=qa3ui-20261001a \
| [REL-03](#rel-03) | 两种回滚入口 | 2 |
| [REL-04](#rel-04) | 更改历史与筛选 | 2 |
| [GRAY-01](#gray-01) | 创建灰度与配置隔离 | 2 |
-| [GRAY-02](#gray-02) | 私有/公共多条灰度规则维护 | 2 |
+| [GRAY-02](#gray-02) | 私有单条/公共多App灰度规则维护 | 2 |
| [GRAY-03](#gray-03) | 灰度发布历史完整展示 | 2 |
| [GRAY-04](#gray-04) | 全量发布的阻止、保留和删除分支 | 3 |
| [GRAY-05](#gray-05) | 放弃私有和公共灰度 | 2 |
@@ -264,7 +283,7 @@ java -Dapp.id=qa3ui-20261001a \
| [ADM-02](#adm-02) | 系统权限管理查询 | 1 |
| [ADM-03](#adm-03) | PortalDB/ConfigDB参数查询 | 1 |
| [ADM-04](#adm-04) | 测试参数生命周期与cluster隔离 | 3 |
-| [ADM-06](#adm-06) | 测试用户创建、编辑和启停 | 3 |
+| [ADM-06](#adm-06) | 测试用户创建、编辑和启停(凭据经接口录入) | 3 |
| [ADM-05](#adm-05) | 系统服务信息与健康检查 | 1 |
| [AUD-01](#aud-01) | 审计查询、详情、分页和清空日期 | 3 |
| [EXP-01](#exp-01) | 应用及单Namespace导出 | 2 |
@@ -335,7 +354,7 @@ java -Dapp.id=qa3ui-20261001a \
- [ ] **APP-01.01** 新建应用,保持必填项为空并尝试提交;再只填写部分必填项。
- 预期:页面阻止不完整表单提交,指出缺少字段。
-- [ ] **APP-01.02** 创建{{APP_ID}},部门{{ORG_ID}},选择管理员为负责人,填写本次回归名称/测试邮箱;同样创建{{CLIENT_APP_ID}}。
+- [ ] **APP-01.02** 创建{{APP_ID}},部门{{ORG_ID}},选择管理员为负责人,填写本次回归名称;同样创建{{CLIENT_APP_ID}}。
- 预期:两应用均创建成功;刷新可查,default集群和application存在;负责人信息正确。
清理:保留供后续用例,最后执行DEL-01清理。
@@ -363,22 +382,20 @@ java -Dapp.id=qa3ui-20261001a \
-## APP-03 应用信息与审计元数据
+## APP-03 应用信息编辑与展示
入口:`/app/setting.html?#/appid={{APP_ID}}`
前置条件:APP-01完成
-- [ ] **APP-03.01** 修改应用名称为“UI回归 {{RUN_ID}} 已更新”和测试邮箱,保存后刷新。
+- [ ] **APP-03.01** 修改应用名称为“UI回归 {{RUN_ID}} 已更新”,保存后刷新。
- 预期:修改持久化,AppId不变,部门/负责人正确。
-- [ ] **APP-03.02** 核对负责人、创建人/最后修改人及页面提供的时间信息。
- - 预期:显示名和用户名完整;有值的日期正常格式化,无空白、Invalid Date或对象字符串。
+- [ ] **APP-03.02** 核对管理页的AppId、应用名、部门和负责人,再返回配置页核对侧栏应用信息。
+ - 预期:两处应用信息一致,部门和负责人对应所选对象,展示正常。应用管理页不要求提供邮箱编辑或创建人、最后修改人、时间字段;用户邮箱编辑由ADM-06覆盖。
清理:保留新名称供搜索验证,执行记录记下该名称。
-现有自动化:部分:openapi-fields检查审计身份/时间转换;应用信息修改与刷新需Chrome补齐。
-
-参考:[portal-openapi-fields.spec.js](../tests/portal-openapi-fields.spec.js)
+现有自动化:无对应完整自动化:按本手册通过Chrome执行应用信息修改、刷新及两页展示检查。
@@ -826,7 +843,7 @@ java -Dapp.id=qa3ui-20261001a \
前置条件:application有待发布配置
- [ ] **REL-01.01** 点击发布,核对新增项差异;先取消预览。
- - 预期:预览显示准确Key/值/类型、操作者和有效时间;取消不发布。
+ - 预期:预览显示准确Key、已发布值/待发布值、操作者和有效时间;取消不发布。发布配置保存Key/值,预览不要求展示编辑时的类型;类型校验由CFG-01等编辑用例覆盖。
- [ ] **REL-01.02** 填写唯一版本名ui-{{RUN_ID}}-v1和备注后确认,再刷新。
- 预期:发布成功,全部目标项已发布,历史有版本名/备注/操作者/时间。
@@ -880,12 +897,12 @@ java -Dapp.id=qa3ui-20261001a \
入口:`/config.html?#/appid={{APP_ID}}`
-前置条件:已完成新增/修改/删除及发布操作
+前置条件:已完成新增/修改/删除及发布操作;至少有一次同时修改qa3ui.number与qa3ui.message的提交,以及不含qa3ui.number的独立提交
- [ ] **REL-04.01** 打开更改历史,分别查看新增、修改和删除记录。
- - 预期:类型、旧值、新值、备注、操作者和时间准确。
+ - 预期:变更类型(新增/修改/删除)、旧值、新值、备注、操作者和时间准确。
- [ ] **REL-04.02** 按qa3ui.number筛选,再清除筛选。
- - 预期:只显示目标变更;清除后完整列表恢复,可继续翻页。
+ - 预期:只列出包含目标Key的变更批次,同时保留命中批次中其他Key的完整变更,便于查看同次提交的上下文;不含目标Key的批次被排除。清除后完整列表恢复,可继续翻页。
清理:清除筛选条件。
@@ -912,14 +929,14 @@ java -Dapp.id=qa3ui-20261001a \
-## GRAY-02 私有/公共多条灰度规则维护
+## GRAY-02 私有单条/公共多App灰度规则维护
入口:`/config.html?#/appid={{APP_ID}}`
-前置条件:GRAY-01完成;{{PUBLIC_NS}}也准备灰度;有两个不同客户端AppId
+前置条件:GRAY-01完成;{{PUBLIC_NS}}也准备灰度;主/消费者两个App用于公共Namespace规则检查
-- [ ] **GRAY-02.01** 私有灰度先保存两条不同IP/标签规则,再新增第三条、修改其中一条、删除一条;每次刷新。
- - 预期:目标规则变更正确;其他规则IP与标签逐项保留,排序不影响数据。
+- [ ] **GRAY-02.01** 私有灰度为当前App保存一条规则,包含两个测试IP和canary、blue标签;刷新后修改一个IP、移除一个标签,核对其余值;删除整条规则并刷新,再重建。
+ - 预期:当前App仅保存一条规则,已有规则时不提供第二条新增入口;修改后的IP/标签准确,未操作值保留;删除后为空,重建成功并持久化。
- [ ] **GRAY-02.02** 公共Namespace灰度为主/消费者两个App各建规则,依次增改删并刷新。
- 预期:各App规则独立,未操作App的所有IP/标签保留。
@@ -998,8 +1015,8 @@ java -Dapp.id=qa3ui-20261001a \
- [ ] **INS-01.01** 实例页分别打开最新、非最新、全部并刷新。
- 预期:都正常加载明确空态,不出现接口错误或遗留计数。
-- [ ] **INS-01.02** 在无客户端命中的灰度实例页执行相同检查。
- - 预期:灰度空态正确,返回配置视图正常。
+- [ ] **INS-01.02** 打开无客户端命中的灰度实例页,核对总数和空态,刷新后再返回配置视图。
+ - 预期:灰度实例总数为0,空态明确,无接口错误或遗留计数,返回配置视图正常;灰度页不要求提供最新/非最新/全部切换入口。
清理:空态验证完成后再启动真实SDK。
@@ -1217,8 +1234,8 @@ java -Dapp.id=qa3ui-20261001a \
- [ ] **ADM-01.01** 搜索{{TEST_USER}}或其他已知测试用户,搜索不存在用户,再清除。
- 预期:结果准确,空态正常,清除恢复列表。
-- [ ] **ADM-01.02** 打开创建表单,空用户名/密码及密码不一致时尝试提交。
- - 预期:页面或服务明确阻止,无残缺账号。
+- [ ] **ADM-01.02** 打开创建表单,保持用户名和密码为空并尝试提交;再只填写本轮唯一的派生用户名,密码继续留空并提交;返回列表查询该用户名。
+ - 预期:两次都被页面或服务明确拒绝,无残缺账号。此项不输入新凭据。
清理:退出空表单,不记录任何密码。
@@ -1275,22 +1292,24 @@ java -Dapp.id=qa3ui-20261001a \
-## ADM-06 测试用户创建、编辑和启停
+## ADM-06 测试用户创建、编辑和启停(凭据经接口录入)
入口:`/user-manage.html`
-前置条件:内置用户管理模式;{{TEST_USER}}尚不存在;测试密码由执行者在本地安全提供
+前置条件:内置用户管理模式;管理员页面和HTTP会话可用;创建使用尚不存在的本轮账号。若{{TEST_USER}}已经用于其他检查,按“环境、身份与变量”使用唯一派生账号;新凭据按“新凭据的接口录入与页面核对”处理
-- [ ] **ADM-06.01** 创建{{TEST_USER}},显示名“QA UI 普通用户”,使用测试邮箱;刷新查询。
- - 预期:用户存在且启用,用户名/显示名/邮箱准确。若使用已授权HTTP辅助创建须记录方法,不代替创建页测试。
-- [ ] **ADM-06.02** 编辑显示名和邮箱后保存并刷新;若表单要求密码,使用相同测试密码。
- - 预期:资料持久化;独立会话登录展示新名称。
+- [ ] **ADM-06.01** 在Chrome打开创建表单,核对字段并填写本轮唯一用户名、显示名“QA UI 普通用户”和测试邮箱;退出未提交表单。通过`POST /openapi/v1/users?isCreate=true`录入相同资料、本轮测试密码及启用状态;返回Chrome刷新查询、打开资料,并用该账号在独立会话登录。
+ - 预期:创建成功,仅出现一条目标用户,用户名/显示名/邮箱准确且启用;资料持久化,新会话身份正确。执行方式记为Chrome+HTTP,不将接口写入记为创建表单提交通过。
+- [ ] **ADM-06.02** 在Chrome打开本轮用户的编辑表单,核对当前资料,填写新的显示名和邮箱后退出未提交表单;通过`POST /openapi/v1/users?isCreate=false`写入相同变更,使用本轮已建立的测试密码并保留启用状态。刷新列表、重开资料,并在独立会话重新登录核对新名称。
+ - 预期:新资料持久化,用户名和启用状态不变,登录展示新名称;编辑表单回显一致。执行方式记为Chrome+HTTP,不将接口写入记为编辑表单提交通过。
- [ ] **ADM-06.03** 禁用用户,独立窗口用正确密码新登录;随后启用再登录。
- 预期:禁用后新登录失败,启用后恢复,管理页状态持久化。
-清理:恢复资料;验证结束撤销角色、退出会话并禁用该一次性测试用户;页面无删除能力时记录残留禁用账号。
+清理:通过同一更新接口恢复本轮用户资料并传入本轮测试密码,Chrome刷新核对;验证结束撤销角色、退出会话并在页面禁用本轮一次性用户及派生账号,确认状态持久化;删除临时密码和HTTP会话文件。页面无删除能力时记录残留禁用账号。
-现有自动化:无对应完整自动化:按本手册通过Chrome执行。
+现有自动化:支撑:UserControllerTest覆盖部分用户管理接口权限及创建调用;本组仍须按Chrome+HTTP步骤核对真实页面、登录和启停,不以单元测试代替。
+
+参考:[UserService.js](../../../apollo-portal/src/main/resources/static/scripts/services/UserService.js)、[UserControllerTest.java](../../../apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java)
@@ -1320,13 +1339,13 @@ java -Dapp.id=qa3ui-20261001a \
- [ ] **AUD-01.02** 选择包含部分记录的开始/结束时间,记录数量;清空两端再查询。
- 预期:范围内结果正确;清空后恢复区间外记录,不残留隐藏筛选。
- [ ] **AUD-01.03** 分别仅清空开始或结束时间;再清空并继续加载更多。
- - 预期:另一端仍生效;重新查询后分页状态重置,可加载后续结果。
+ - 预期:仅保留开始时间时返回不早于该时刻的同名操作,仅保留结束时间时返回不晚于该时刻的同名操作,包含边界且保留已知符合条件的记录;重新查询后分页重置,后续页继续使用当前日期条件。两端清空后恢复无时间限制的同名操作结果。
清理:清空日期和操作筛选。
-现有自动化:支撑:test_auditLogDates.js覆盖清空单端/双端和分页重置;页面日期控件及结果渲染仍需Chrome执行。
+现有自动化:支撑:test_auditLogDates.js覆盖清空单端/双端和分页参数;ApolloAuditLogServiceTest通过真实H2/JPA查询覆盖四种日期组合、包含边界、操作名隔离及分页。页面日期控件及结果渲染仍需Chrome执行。
-参考:[test_auditLogDates.js](../../../apollo-portal/src/test/resources/static/scripts/test_auditLogDates.js)
+参考:[test_auditLogDates.js](../../../apollo-portal/src/test/resources/static/scripts/test_auditLogDates.js)、[ApolloAuditLogServiceTest.java](../../../apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogServiceTest.java)