Skip to content

fix: 清除用户/Key 到期时间后保存不生效#533

Merged
ding113 merged 1 commit intodevfrom
fix/expire-clear-save
Jan 4, 2026
Merged

fix: 清除用户/Key 到期时间后保存不生效#533
ding113 merged 1 commit intodevfrom
fix/expire-clear-save

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 4, 2026

Summary

Fixes a bug where clearing the expiration date (expiresAt) for Users/Keys via the dashboard UI would not persist to the database.

修复用户/Key 到期时间清除后保存不生效的问题。

Problem

When using the "Clear Date" button in the dashboard to remove the expiration date for a User or Key, the change was not being saved to the database. This occurred because:

  1. Schema preprocessing issue: The UpdateUserSchema was converting null/empty string to undefined, losing the explicit "clear" semantic
  2. Partial update issue: When editing a Key with partial data (e.g., only updating quota), the missing expiresAt field would inadvertently clear the expiration date

在仪表板中使用"清除日期"按钮移除用户或密钥的到期时间时,更改未能保存到数据库:

  1. Schema 预处理将 null/空字符串转换为 undefined,丢失了"清除"语义
  2. 局部更新密钥时(如仅修改限额),缺失的 expiresAt 字段会意外清空到期时间

Related to:

Solution

Implemented proper update semantics for the expiresAt field:

  1. UpdateUserSchema (src/lib/validation/schemas.ts):

    • Preserve null/empty string as explicit clear signal (changed from converting to undefined)
    • Made schema accept .nullable() to allow explicit null values
    • undefined = don't update field, null/"" = clear expiration (set to null)
  2. editKey (src/actions/keys.ts):

    • Only update expires_at when expiresAt field is explicitly present in request data
    • Use Object.hasOwn(data, "expiresAt") to detect field presence
    • Prevents accidental clearing during partial updates (e.g., quota-only changes)
    • Added validation for invalid date strings (returns INVALID_FORMAT error)

修复方案:

  1. UpdateUserSchema 保留 null/空字符串为显式清除语义
  2. editKey 仅在显式携带 expiresAt 字段时才更新/清除,避免局部更新误清空

Changes

Core Changes

  • src/lib/validation/schemas.ts (+6/-2): Updated UpdateUserSchema.expiresAt preprocessing logic
  • src/actions/keys.ts (+22/-4): Added field presence detection and conditional update for expires_at

Test Coverage

Added 7 new test files covering all scenarios:

Backend Logic Tests:

  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts (+146): Tests editKey behavior with/without expiresAt field
  • tests/unit/actions/users-edit-user-expires-at-clear.test.ts (+49): Tests editUser with null expiresAt
  • tests/unit/validation/user-schemas-expires-at-clear.test.ts (+94): Tests UpdateUserSchema preprocessing logic

UI Tests:

  • tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx (+137): Tests "Clear Date" button submits expiresAt field
  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx (+121): Tests user form clear date behavior

Testing

Automated Tests

  • Unit tests added for schema validation (8 test cases)
  • Unit tests added for backend logic (4 test cases)
  • UI tests added for form behavior (2 test cases)

Manual Testing Scenarios

  1. Clear expiration: Click "Clear Date" on user/key with existing expiration → saves successfully
  2. Partial update: Edit key quota without touching expiration field → expiration preserved
  3. Invalid date: Submit invalid date string → error displayed
  4. Set expiration: Set new expiration date → saves successfully

Local Verification

bun run lint && bun run typecheck && bun run test && bun run build

All checks passed ✓

Files Changed

  • src/actions/keys.ts (+22/-4)
  • src/lib/validation/schemas.ts (+6/-2)
  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts (+146/-0)
  • tests/unit/actions/users-edit-user-expires-at-clear.test.ts (+49/-0)
  • tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx (+137/-0)
  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx (+121/-0)
  • tests/unit/validation/user-schemas-expires-at-clear.test.ts (+94/-0)

Description enhanced by Claude AI

@coderabbitai
Copy link

coderabbitai bot commented Jan 4, 2026

📝 Walkthrough

功能概述

本PR增强了expiresAt字段的处理机制,区分显式清除过期时间(设为null)与不进行更新(字段未提供)的场景。在验证层、操作层和UI层添加了相应的逻辑判断和测试覆盖。

改动详情

阶层 / 文件 改动摘要
核心逻辑改动
src/actions/keys.ts, src/lib/validation/schemas.ts
在editKey函数中引入hasExpiresAtField标志以检测expiresAt字段是否由调用方显式提供;仅当提供时才进行expires_at的条件更新。UpdateUserSchema调整了expiresAt的预处理逻辑:undefined保持undefined(不更新),null或空字符串映射为null(显式清除)。新增日期格式无效时返回INVALID_FORMAT错误的验证。移除了dailyResetMode的默认值"fixed"。
操作层单元测试
tests/unit/actions/keys-edit-key-expires-at-clear.test.ts, tests/unit/actions/users-edit-user-expires-at-clear.test.ts
为editKey和editUser操作添加单元测试,验证不同expiresAt输入场景:字段未提供(不更新)、显式undefined(清除为null)、有效日期字符串(转为Date对象)、无效日期格式(返回错误)。
UI层单元测试
tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx, tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx
为EditKeyForm和UserForm添加UI单元测试,验证清除过期日期的交互流程。测试涵盖日期选择、点击"Clear Date"按钮、表单提交等操作,并断言editKey/editUser被调用且payload包含expiresAt字段。
验证层单元测试
tests/unit/validation/user-schemas-expires-at-clear.test.ts
为UpdateUserSchema和CreateUserSchema添加expiresAt处理的验证测试,覆盖null/空字符串清除、undefined不更新、有效ISO日期解析、无效格式拒绝等场景。

代码审查工作量估计

🎯 3 (中等) | ⏱️ ~20 分钟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 标题准确、简洁地概括了本次变更的核心问题——修复用户/Key 到期时间清除后保存不生效的缺陷。
Description check ✅ Passed PR 描述清晰地说明了问题、具体修改内容(UpdateUserSchema、editKey 逻辑调整)以及测试覆盖,与代码变更高度相关。
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/expire-clear-save

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求修复了用户和密钥过期时间清除后保存不生效的错误。该修复通过优化 UpdateUserSchema 和 editKey 函数中的数据处理逻辑,确保正确处理过期时间的显式清除操作,并防止在进行局部更新时意外修改过期时间。同时,还增加了全面的测试以保证这些更改的健壮性。

Highlights

  • 过期时间清除逻辑: UpdateUserSchema 现在明确将 expiresAt 字段的 null 或空字符串视为清除操作,避免被预处理为 undefined。
  • Key编辑行为: editKey 函数已修改,仅当输入数据中明确包含 expiresAt 字段时才更新或清除 expires_at,以防止在局部更新时意外清空。
  • 测试覆盖: 增加了新的单元测试和 UI 测试,覆盖了 expiresAt 字段的清除、不更新和非法输入等场景。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added bug Something isn't working area:core size/L Large PR (< 1000 lines) labels Jan 4, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 4, 2026

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the handling of the expiresAt field in key and user update operations to introduce clearer semantics. Specifically, it distinguishes between explicitly clearing an expiration date (by providing null or an empty string) and not updating it at all (by omitting the field). The editKey action now checks for the explicit presence of the expiresAt field to prevent accidental clearing during partial updates and includes new validation for invalid date formats. The UpdateUserSchema was updated to reflect these new expiresAt preprocessing rules, allowing null as a valid value. The changes are thoroughly covered by new unit tests for both the keys and users actions, as well as the validation schemas. Additionally, new UI tests were added for EditKeyForm and UserForm to ensure that the 'Clear Date' functionality correctly triggers the backend's updated logic. A review comment suggests extracting duplicated test helper functions (loadMessages, render, clickButtonByText) into a shared utility file to improve code reusability and maintainability across UI tests.

Comment on lines +37 to +76
function loadMessages() {
const base = path.join(process.cwd(), "messages/en");
const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8"));

return {
common: read("common.json"),
errors: read("errors.json"),
quota: read("quota.json"),
ui: read("ui.json"),
dashboard: read("dashboard.json"),
forms: read("forms.json"),
};
}

function render(node: ReactNode) {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);

act(() => {
root.render(node);
});

return {
container,
unmount: () => {
act(() => root.unmount());
container.remove();
},
};
}

function clickButtonByText(text: string) {
const buttons = Array.from(document.body.querySelectorAll("button"));
const btn = buttons.find((b) => (b.textContent || "").includes(text));
if (!btn) {
throw new Error(`未找到按钮: ${text}`);
}
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这些测试辅助函数(loadMessages, render, clickButtonByText)在多个UI测试文件中(例如 user-form-expiry-clear-ui.test.tsx)被重复定义。为了提高代码的复用性和可维护性,建议将这些通用的辅助函数提取到一个共享的测试工具文件中,例如 tests/lib/test-utils.tsx。这样,所有UI测试都可以导入并使用这些函数,避免了代码重复。

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx (1)

79-82: 建议使用 afterEach 进行清理以避免测试污染

当前 unmount() 在测试末尾调用,但如果断言失败会导致清理代码无法执行,可能影响后续测试。建议使用 afterEach 确保清理始终执行。

🔎 建议的修改
 describe("UserForm: 清除 expiresAt 后应提交 null", () => {
+  let cleanup: (() => void) | null = null;
+
   beforeEach(() => {
     vi.clearAllMocks();
   });
+
+  afterEach(() => {
+    cleanup?.();
+    cleanup = null;
+  });

然后在测试中:

-    const { unmount } = render(
+    const { unmount } = cleanup = render(

或者直接保存引用:

     const { unmount } = render(...);
+    cleanup = unmount;
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between b7d5bcd and ff56250.

📒 Files selected for processing (7)
  • src/actions/keys.ts
  • src/lib/validation/schemas.ts
  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts
  • tests/unit/actions/users-edit-user-expires-at-clear.test.ts
  • tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx
  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx
  • tests/unit/validation/user-schemas-expires-at-clear.test.ts
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use 2-space indentation in all code files

Files:

  • tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx
  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts
  • tests/unit/validation/user-schemas-expires-at-clear.test.ts
  • src/actions/keys.ts
  • tests/unit/actions/users-edit-user-expires-at-clear.test.ts
  • src/lib/validation/schemas.ts
  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use double quotes for strings instead of single quotes
Use trailing commas in multi-line structures
Enforce maximum line length of 100 characters
Use path alias @/* to reference files from ./src/* directory

**/*.{ts,tsx,js,jsx}: Use Biome for linting and formatting with 2-space indent, double quotes, trailing commas, and 100 character max line length
Use path alias @/* to reference files in ./src/* directory

Files:

  • tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx
  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts
  • tests/unit/validation/user-schemas-expires-at-clear.test.ts
  • src/actions/keys.ts
  • tests/unit/actions/users-edit-user-expires-at-clear.test.ts
  • src/lib/validation/schemas.ts
  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript strict mode for type safety
Use readonly or const assertions for immutable data structures

Files:

  • tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx
  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts
  • tests/unit/validation/user-schemas-expires-at-clear.test.ts
  • src/actions/keys.ts
  • tests/unit/actions/users-edit-user-expires-at-clear.test.ts
  • src/lib/validation/schemas.ts
  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vitest for unit testing with Node environment, coverage thresholds: 50% lines/functions, 40% branches

Files:

  • tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx
  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts
  • tests/unit/validation/user-schemas-expires-at-clear.test.ts
  • tests/unit/actions/users-edit-user-expires-at-clear.test.ts
  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx
**/*.{tsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use next-intl for internationalization with 5 locales: en, ja, ru, zh-CN, zh-TW

Files:

  • tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx
  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx
**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

Ensure test database names contain 'test' keyword for safety validation

Files:

  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts
  • tests/unit/validation/user-schemas-expires-at-clear.test.ts
  • tests/unit/actions/users-edit-user-expires-at-clear.test.ts
src/actions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/actions/**/*.ts: Validate all user inputs with Zod schemas before processing
Use Server Actions in next-safe-action with OpenAPI generation for admin API endpoints
Use Next.js API Routes and Server Actions for admin operations and REST endpoints

Files:

  • src/actions/keys.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Hash API keys using SHA-256 before storing in database, never store plaintext keys
Mask API keys and sensitive data in application logs
Validate required environment variables at startup with clear error messages

Files:

  • src/actions/keys.ts
  • src/lib/validation/schemas.ts
src/{repository,actions}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Avoid N+1 queries by using eager loading and batch queries for statistics

Files:

  • src/actions/keys.ts
src/lib/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use connection pooling for database and Redis connections

Files:

  • src/lib/validation/schemas.ts
🧠 Learnings (4)
📚 Learning: 2026-01-03T09:08:49.019Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-03T09:08:49.019Z
Learning: Applies to src/**/*{guard,auth}*.ts : Use constant-time comparison for API key validation to prevent timing attacks

Applied to files:

  • tests/unit/actions/keys-edit-key-expires-at-clear.test.ts
  • src/actions/keys.ts
📚 Learning: 2026-01-03T09:08:49.019Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-03T09:08:49.019Z
Learning: Applies to src/actions/**/*.ts : Validate all user inputs with Zod schemas before processing

Applied to files:

  • tests/unit/validation/user-schemas-expires-at-clear.test.ts
  • src/lib/validation/schemas.ts
📚 Learning: 2026-01-03T09:08:49.019Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-03T09:08:49.019Z
Learning: Applies to **/*.test.ts : Ensure test database names contain 'test' keyword for safety validation

Applied to files:

  • tests/unit/validation/user-schemas-expires-at-clear.test.ts
📚 Learning: 2026-01-03T09:08:49.019Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-03T09:08:49.019Z
Learning: Applies to **/*.test.{ts,tsx} : Use Vitest for unit testing with Node environment, coverage thresholds: 50% lines/functions, 40% branches

Applied to files:

  • tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx
🧬 Code graph analysis (6)
tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx (1)
src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx (1)
  • EditKeyForm (48-358)
tests/unit/actions/keys-edit-key-expires-at-clear.test.ts (1)
src/actions/keys.ts (1)
  • editKey (281-477)
tests/unit/validation/user-schemas-expires-at-clear.test.ts (1)
src/lib/validation/schemas.ts (2)
  • UpdateUserSchema (147-265)
  • CreateUserSchema (16-142)
src/actions/keys.ts (1)
src/lib/utils/error-messages.ts (1)
  • ERROR_CODES (110-117)
tests/unit/actions/users-edit-user-expires-at-clear.test.ts (1)
src/actions/users.ts (1)
  • editUser (1066-1202)
tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx (1)
src/app/[locale]/dashboard/_components/user/forms/user-form.tsx (1)
  • UserForm (60-447)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Greptile Review
  • GitHub Check: check-codex-status
  • GitHub Check: pr-review
  • GitHub Check: pr-label
  • GitHub Check: pr-description
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (13)
src/lib/validation/schemas.ts (2)

200-248: LGTM! expiresAt 清除语义实现正确

预处理逻辑清晰地区分了三种语义:

  • undefined:不更新该字段
  • null"":显式清除过期时间
  • 有效日期:设置新的过期时间

添加 .nullable() (line 228) 与 Zod 4 的 nullable 日期验证对齐,允许显式传递 null 值。注释说明清晰,逻辑正确。


250-250: 移除 dailyResetMode 默认值的更改是正确的

这是符合更新语义的正确实现。UpdateUserSchema 用于 PATCH 操作(部分更新),此时 undefined 表示"不修改该字段"。验证结果显示:

  • CreateUserSchema(第 124 行)保留了 .default("fixed") 供新用户创建使用 ✓
  • UpdateUserSchema(第 250 行)正确地移除了默认值,避免无意中覆盖现有值 ✓
  • 数据库更新逻辑(src/repository/user.ts 第 348 行)正确实现了选择性更新模式:仅当 userData.dailyResetMode !== undefined 时才更新该字段 ✓
  • editUser 和其他调用方正确处理可选的 dailyResetMode 参数 ✓
tests/unit/actions/users-edit-user-expires-at-clear.test.ts (1)

1-49: LGTM! 测试覆盖清除 expiresAt 的核心场景

测试结构清晰,正确验证了:

  1. 传入 expiresAt: nulleditUser 返回成功
  2. updateUser 被调用一次且 payload 包含 expiresAt: null

Mock 设置完整,符合单元测试最佳实践。

tests/unit/validation/user-schemas-expires-at-clear.test.ts (2)

4-48: LGTM! UpdateUserSchema 测试覆盖全面

测试用例完整验证了 expiresAt 的三种语义:

  • 清除null"" 均解析为 null
  • 不更新:缺省字段保持 undefined
  • 设置值:ISO 字符串正确解析为 Date

边界条件和错误场景覆盖充分(非法字符串、非法 Date、非法类型、超过 10 年限制)。


50-94: LGTM! CreateUserSchema 测试确保向后兼容性

测试验证了 CreateUserSchema 的不同语义:

  • null 视为未设置(解析为 undefined
  • 支持 Date 对象和 ISO 字符串(必须为未来时间)
  • 正确拒绝过去时间和非法输入

与 UpdateUserSchema 的语义区分清晰,符合创建/更新场景的不同需求。

tests/unit/actions/keys-edit-key-expires-at-clear.test.ts (1)

40-146: LGTM! editKey 测试覆盖全面且准确

4 个测试用例完整验证了 expiresAt 的条件更新逻辑:

  1. 不携带字段:验证 expires_at 不在 payload 中(避免误清空)
  2. 显式清除expiresAt: undefinedexpires_at: null
  3. 设置新值:有效日期字符串 → Date 对象
  4. 错误处理:非法字符串 → INVALID_FORMAT

Mock 数据完整,断言精确(使用 Object.hasOwntoBeInstanceOferrorCode 检查),符合 PR 目标。

tests/unit/dashboard/edit-key-form-expiry-clear-ui.test.tsx (1)

78-137: LGTM! UI 测试验证端到端清除流程

测试完整模拟了用户清除过期时间的操作流程:

  1. 渲染带有 expiresAt 值的 EditKeyForm
  2. 点击日期选择器按钮
  3. 点击 "Clear Date" 清除日期
  4. 提交表单

关键断言(line 133)验证了 PR 修复的核心:提交时必须显式携带 expiresAt 字段,后端才能识别为"清除"操作。

辅助函数(loadMessagesrenderclickButtonByText)设计合理,正确使用 act() 处理 React 更新。

tests/unit/dashboard/user-form-expiry-clear-ui.test.tsx (2)

99-105: 测试依赖硬编码的日期字符串和按钮文本

测试中使用硬编码的 "2026-01-04""Clear Date" 来定位按钮。如果日期格式或国际化文本发生变化,测试可能会失败。不过对于当前的单元测试场景,这是可接受的做法。


110-117: 测试逻辑正确,验证了 expiresAt 清除功能

测试正确验证了点击清除日期后,editUser 被调用且 expiresAt 参数为 null。这符合 PR 的修复目标。

src/actions/keys.ts (4)

346-348: 使用 Object.hasOwn 检测字段存在性的做法正确

这种方式能够区分"未提供字段"和"显式提供字段(即使值为 null/undefined)",解决了局部更新误清空的问题。


434-440: 无效日期验证逻辑正确

该检查能够正确捕获 new Date("invalid-string") 产生的无效日期。但请注意,如果上述 null 转换问题存在,epoch 日期(1970-01-01)会被认为是有效日期而通过验证。


447-463: 条件展开实现了正确的局部更新语义

使用 ...(hasExpiresAtField ? { expires_at: expiresAt } : {}) 确保仅在显式提供 expiresAt 时才更新该字段,避免了局部更新时误清空到期时间的问题。


424-432: Schema设计已防止此问题发生——无需修改

KeyFormSchemaexpiresAt 字段的 transform 逻辑保证了该字段只会输出 undefined 或字符串值,永不输出 null

expiresAt: z
  .string()
  .optional()
  .default("")
  .transform((val) => (val === "" ? undefined : val))

因此当前代码检查 validatedData.expiresAt === undefined 是正确且充分的,不存在 null 值导致 epoch 日期的问题。

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

No significant issues identified in this PR. Changes appear focused on making expiresAt clear/update semantics explicit for both Key edits and User updates, with unit/UI tests added to lock the behavior.

PR Size: L

  • Lines changed: 581
  • Files changed: 7
  • Split suggestions: Consider splitting into (1) UpdateUserSchema semantics + related tests and (2) editKey expiresAt semantics + related tests.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate
  • Code clarity - Good

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Identified PR #533 (fix/expire-clear-savedev), computed size L (581 LOC changed across 7 files), and applied label size/L.
  • Reviewed the full diff across src/actions/keys.ts and src/lib/validation/schemas.ts plus the new unit/UI tests; no issues met the ≥80 confidence reporting threshold, so no inline diff comments were posted.
  • Submitted the summary review to PR #533 via gh pr review --comment (includes required split suggestion for a size L PR).

@ding113 ding113 merged commit b66b636 into dev Jan 4, 2026
20 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 4, 2026
This was referenced Jan 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core bug Something isn't working size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant