From 15d123a3c8ecfe1e058432b3df56561d7c838a9d Mon Sep 17 00:00:00 2001 From: Jason Song Date: Wed, 30 Sep 2026 21:53:46 +0800 Subject: [PATCH 1/2] fix(audit): support open-ended date filters Preserve optional audit date bounds and pagination with real JPA regression coverage. Align the 160-check release handbook with supported UI behavior and authorized HTTP credential setup. --- .../audit/api/ApolloAuditLogQueryApi.java | 4 +- apollo-audit/apollo-audit-impl/pom.xml | 6 + .../repository/ApolloAuditLogRepository.java | 6 + .../audit/service/ApolloAuditLogService.java | 11 ++ .../service/ApolloAuditLogServiceTest.java | 139 ++++++++++++++++++ .../static/scripts/test_auditLogDates.js | 30 ++-- e2e/portal-e2e/release-regression/README.md | 105 +++++++------ 7 files changed, 247 insertions(+), 54 deletions(-) create mode 100644 apollo-audit/apollo-audit-impl/src/test/java/com/ctrip/framework/apollo/audit/service/ApolloAuditLogServiceTest.java 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) From 940aaa972e173a36fe0e9f306fe59e906a157b13 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Wed, 30 Sep 2026 22:00:16 +0800 Subject: [PATCH 2/2] docs: add changelog for audit date filter fix --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) 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)