fix/status-review-sync-20260409 #1
@@ -67,7 +67,13 @@
|
||||
"Bash(while read:*)",
|
||||
"Bash(do basename:*)",
|
||||
"Bash(dir \"D:\\\\project\\\\frontend\")",
|
||||
"Bash(grep -E '\\\\.txt$')"
|
||||
"Bash(grep -E '\\\\.txt$')",
|
||||
"Bash(go tool:*)",
|
||||
"Bash(sort -t: -k2 -rn)",
|
||||
"Bash(sort -t: -k3 -rn)",
|
||||
"Bash(gosec ./...)",
|
||||
"Bash(gosec -no-fail ./internal/...)",
|
||||
"Bash(gosec -no-fail -quiet ./internal/...)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,25 +39,45 @@
|
||||
- GAP-07(SDK):❌ 推迟 v2.0
|
||||
- 密码历史记录:✅ ChangePassword + doResetPassword 均已接线
|
||||
|
||||
## 代码审查状态(最新:2026-04-03 Sprint 16 完成)
|
||||
- 代码审查评分:**10/10**(Sprint 16 彻底解决所有遗留问题)
|
||||
- 🔴 阻塞级问题:0 个
|
||||
- 🟡 建议级问题:0 个
|
||||
- 🟢 未修复安全问题:0 个(SEC-04/06/08 已全部修复)
|
||||
- E2E 测试通过率:100% (17/17)
|
||||
## 代码审查状态(最新:2026-04-08 生产级评估 v3.0)
|
||||
|
||||
- **综合评分**:⚠️ 5.9/10 **不合格**
|
||||
- 🔴 P0 阻塞问题:7 个(必须立即修复)
|
||||
- 🟠 P1 严重问题:5 个(本周修复)
|
||||
- 🟡 P2 高优先级:4 个(本月修复)
|
||||
|
||||
### 关键差距(v2.0 → v3.0 真实评估)
|
||||
|
||||
| 维度 | v2.0 | v3.0 | 差距原因 |
|
||||
|------|------|------|----------|
|
||||
| 代码质量 | 9.7 | **7.5** | 后端覆盖率仅32.1% |
|
||||
| 安全强度 | 9.7 | **6.0** | 无gosec、占位JWT密钥 |
|
||||
| 部署简单性 | 8.0 | **5.0** | Docker无健康检查、无资源限制 |
|
||||
| 运维可靠性 | 7.0 | **4.0** | 无备份自动化、无灾备方案 |
|
||||
| 文档规范性 | 7.0 | **5.0** | Runbook缺失、无OpenAPI |
|
||||
|
||||
### Sprint 19(2026-04-08):生产级差距分析
|
||||
|
||||
- 制定生产级审查标准:`docs/code-review/CODE_REVIEW_STANDARD_V3.md`
|
||||
- 5维评估体系(代码质量25%+安全30%+部署15%+运维20%+文档10%)
|
||||
- P0-P4分级体系
|
||||
- 生产合并门禁清单
|
||||
- 差距分析报告:`docs/code-review/PRODUCTION_GAP_ANALYSIS_2026-04-08.md`
|
||||
- 7个P0问题清单
|
||||
- 三阶段修复路线图
|
||||
|
||||
### 历史修复验证
|
||||
|
||||
- Sprint 15 修复清单:
|
||||
- BUG-01: Goroutine 中使用已回收的 gin context(auth_handler.go、sms_handler.go)
|
||||
- BUG-02: 密码历史 goroutine 使用裸 context.Background()(user_service.go、password_reset.go)
|
||||
- BUG-03: 登录日志 goroutine 使用裸 context.Background()(auth.go)
|
||||
- BUG-04: handleError 所有错误一律返回 500(auth_handler.go)
|
||||
- BUG-05: Logout 不使 Token 失效(auth_handler.go)
|
||||
- BUG-06: GetCSRFToken 返回 not_implemented(auth_handler.go)
|
||||
- 报告:`docs/sprints/SPRINT_15_CODE_REVIEW_REPORT.md`
|
||||
- BUG-01: Goroutine 中使用已回收的 gin context ✅ 已验证
|
||||
- BUG-02: 密码历史 goroutine 使用裸 context.Background() ✅ 已验证
|
||||
- BUG-03: 登录日志 goroutine 使用裸 context.Background() ✅ 已验证
|
||||
- BUG-04: handleError 所有错误一律返回 500 ✅ 已验证
|
||||
- BUG-05: Logout 不使 Token 失效 ✅ 已验证
|
||||
- BUG-06: GetCSRFToken 返回 not_implemented ✅ 已验证
|
||||
- Sprint 16 修复清单:
|
||||
- P1: E2E 测试中 exportHandler 未初始化,导致 2 个测试失败
|
||||
- SEC-04: JTI 时间戳防枚举(格式:timestamp + random)
|
||||
- SEC-08: Refresh Token 滚动轮换防无限流(Token Rotation)
|
||||
- 报告:`docs/sprints/SPRINT_16_FINAL_ISSUE_RESOLUTION.md`
|
||||
- SEC-04: JTI 时间戳防枚举(格式:timestamp + random)✅ 已验证
|
||||
- SEC-08: Refresh Token 滚动轮换防无限流 ✅ 已验证
|
||||
|
||||
## 关键 API 路由
|
||||
- 登录: `POST /api/v1/auth/login`(参数: account/username/email/phone, password, device_id, device_name, device_browser, device_os)
|
||||
@@ -103,6 +123,25 @@
|
||||
- 前端执行方案(唯一有效):`docs/plans/ADMIN_FRONTEND_EXECUTION_PLAN.md`
|
||||
- 前后端联调实施指南:`docs/processes/FRONTEND_BACKEND_REVIEW_IMPLEMENTATION_GUIDE.md`
|
||||
|
||||
## 安全实践亮点(已验证)
|
||||
- ✅ Argon2id 密码哈希(64MB内存、5次迭代、4并行)
|
||||
- ✅ crypto/rand 生成 Token 和盐(无 math/rand)
|
||||
- ✅ JTI 格式:timestamp(8字节hex) + random(16字节hex)
|
||||
- ✅ Token 滚动轮换防无限流
|
||||
- ✅ 内存存储 access_token(非 localStorage)
|
||||
- ✅ HttpOnly Cookie 存储 refresh_token
|
||||
- ✅ 30秒请求超时控制
|
||||
- ✅ CSRF 保护机制
|
||||
- ✅ 登录异常检测(AnomalyDetector)
|
||||
- ✅ 常数时间密码比较(防时序攻击)
|
||||
|
||||
## 代码审查标准(v2.0)
|
||||
- 标准文档:`docs/code-review/CODE_REVIEW_STANDARD_V2.md`
|
||||
- 流程文档:`docs/code-review/CODE_REVIEW_PROCESS.md`
|
||||
- 报告目录:`docs/code-review/`
|
||||
- 合并门禁:go vet ✅ / go build ✅ / go test ✅ / lint ✅
|
||||
- 时效要求:常规PR首次审查 4h,紧急 1h
|
||||
|
||||
## 技术经验积累
|
||||
- replace_in_file 操作要确保不会重复插入内容
|
||||
- Ant Design Menu 受控/非受控模式切换:受控模式(openKeys)与CSS冲突,改用 defaultOpenKeys
|
||||
|
||||
119
README.md
119
README.md
@@ -1,2 +1,119 @@
|
||||
# user-system
|
||||
# User Management System (UMS)
|
||||
|
||||
企业级用户管理系统,支持 RBAC 角色权限管理、多因素认证、设备信任和安全审计。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置依赖
|
||||
|
||||
- Go 1.21+
|
||||
- Node.js 18+
|
||||
- SQLite(默认,无需安装)
|
||||
|
||||
### 启动后端
|
||||
|
||||
```bash
|
||||
# 复制环境配置
|
||||
cp .env.example .env
|
||||
# 编辑 .env 填入必要配置(JWT_SECRET, DEFAULT_ADMIN_PASSWORD 等)
|
||||
|
||||
# 启动服务
|
||||
go run ./cmd/server
|
||||
```
|
||||
|
||||
服务启动后访问 `http://localhost:8080/api/v1/auth/bootstrap` 初始化管理员账号。
|
||||
|
||||
### 启动前端
|
||||
|
||||
```bash
|
||||
cd frontend/admin
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
.
|
||||
├── cmd/server/ # 后端入口
|
||||
├── internal/ # 后端代码
|
||||
│ ├── api/handler/ # HTTP 处理器
|
||||
│ ├── api/middleware/ # 中间件(认证、权限、限流)
|
||||
│ ├── auth/ # 认证服务(JWT/SSO)
|
||||
│ ├── repository/ # 数据访问层
|
||||
│ ├── service/ # 业务逻辑层
|
||||
│ └── domain/ # 领域模型
|
||||
├── frontend/admin/ # 管理后台前端
|
||||
├── configs/ # 配置文件
|
||||
├── docs/ # 详细文档
|
||||
└── data/ # SQLite 数据库目录
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 用户管理 | 注册、登录、CRUD、批量操作 |
|
||||
| RBAC | 角色继承、权限细粒度控制 |
|
||||
| TOTP | Google Authenticator 二次验证 |
|
||||
| 设备信任 | 信任设备免二次验证 |
|
||||
| 登录日志 | 完整操作审计 |
|
||||
| Webhook | 事件通知(user.created/deleted 等)|
|
||||
| SSO | CAS 协议支持 |
|
||||
|
||||
## 环境变量
|
||||
|
||||
关键配置项(详见 `.env.example`):
|
||||
|
||||
| 变量 | 说明 | 必填 |
|
||||
|------|------|------|
|
||||
| `JWT_SECRET` | JWT 签名密钥 | 是 |
|
||||
| `DEFAULT_ADMIN_EMAIL` | 初始管理员邮箱 | 是 |
|
||||
| `DEFAULT_ADMIN_PASSWORD` | 初始管理员密码 | 是 |
|
||||
| `SMTP_*` | 邮件服务配置 | 是(邮件功能)|
|
||||
| `SMS_*` | 短信服务配置 | 否 |
|
||||
|
||||
## API 文档
|
||||
|
||||
完整 API 规范:`docs/API.md`
|
||||
|
||||
认证流程:
|
||||
```
|
||||
1. POST /api/v1/auth/register # 注册用户
|
||||
2. POST /api/v1/auth/login # 登录获取 Token
|
||||
3. POST /api/v1/auth/refresh # 刷新 Token
|
||||
```
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
go build ./cmd/server
|
||||
|
||||
# 测试
|
||||
go test ./internal/... -cover
|
||||
|
||||
# 前端构建
|
||||
cd frontend/admin && npm run build
|
||||
|
||||
# Docker 构建
|
||||
docker build -t ums .
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
- 开发部署:`docs/DEPLOYMENT.md`
|
||||
- 生产部署:`DEPLOY_GUIDE.md`
|
||||
- 运行手册:`docs/guides/` 目录下的 7 个 Runbook
|
||||
|
||||
## 测试覆盖率
|
||||
|
||||
```
|
||||
api/handler 15.6%
|
||||
api/middleware 21.5%
|
||||
auth 28.1%
|
||||
repository 47.2%
|
||||
internal/middleware 65.4%
|
||||
```
|
||||
|
||||
目标:80%+
|
||||
|
||||
678
docs/code-review/CODE_REVIEW_STANDARD_V3.md
Normal file
678
docs/code-review/CODE_REVIEW_STANDARD_V3.md
Normal file
@@ -0,0 +1,678 @@
|
||||
# 代码审查标准与质量评级规范 v3.0
|
||||
|
||||
**文档版本**: v3.0
|
||||
**生成日期**: 2026-04-08
|
||||
**适用范围**: User Management System (UMS) 项目
|
||||
**审查专家**: 代码审查专家
|
||||
**目标**: 生产级软件质量标准
|
||||
|
||||
---
|
||||
|
||||
## 修订说明
|
||||
|
||||
v3.0 版本针对"生产上线"要求进行全面升级:
|
||||
|
||||
| 维度 | v2.0 | v3.0 | 差距 |
|
||||
|------|------|------|------|
|
||||
| 代码质量 | 9.7/10 | **7.5/10** | 测试覆盖率仅32.1%,严重不足 |
|
||||
| 安全强度 | 9.7/10 | **6.0/10** | 无gosec扫描、占位JWT密钥、缺渗透测试 |
|
||||
| 部署简单性 | 8.0/10 | **5.0/10** | docker-compose简陋、无K8s、无健康检查 |
|
||||
| 运维可靠性 | 7.0/10 | **4.0/10** | 无备份自动化、无灾备方案、监控薄弱 |
|
||||
| 文档规范性 | 7.0/10 | **5.0/10** | 缺OpenAPI、缺Runbook、缺应急响应 |
|
||||
|
||||
**综合评分(v3.0真实评估)**: **5.9/10 ⚠️ 不合格**
|
||||
|
||||
---
|
||||
|
||||
## 一、生产级质量标准(v3.0)
|
||||
|
||||
### 1.1 五维评估体系
|
||||
|
||||
| 维度 | 权重 | 生产标准 | 当前差距 |
|
||||
|------|------|----------|----------|
|
||||
| **代码质量** | 25% | 覆盖率≥80%,无技术债 | 覆盖率32.1%,差距48% |
|
||||
| **安全强度** | 30% | 渗透测试、gosec合格、合规 | 无扫描工具、占位密钥 |
|
||||
| **部署简单性** | 15% | 一键部署、配置分离、不可变 | docker-compose简陋 |
|
||||
| **运维可靠性** | 20% | 监控完善、告警到位、备份自动化 | 监控基础、告警未验证 |
|
||||
| **文档规范性** | 10% | OpenAPI、Runbook、应急响应 | 文档残缺 |
|
||||
|
||||
### 1.2 生产合并门禁(必须全部通过)
|
||||
|
||||
```yaml
|
||||
# 生产级 PR 合并门禁
|
||||
pre_merge_checks:
|
||||
# 代码质量
|
||||
- name: 后端覆盖率
|
||||
command: go test -coverprofile coverage.out ./...
|
||||
threshold: 80%
|
||||
critical_paths: 90%
|
||||
- name: 前端覆盖率
|
||||
command: npm test -- --coverage
|
||||
threshold: 80%
|
||||
|
||||
# 安全
|
||||
- name: Go安全扫描
|
||||
command: gosec ./...
|
||||
critical: high/critical must be 0
|
||||
- name: 前端安全扫描
|
||||
command: npm audit
|
||||
critical: 0 vulnerabilities
|
||||
- name: 依赖漏洞扫描
|
||||
command: govulncheck ./...
|
||||
critical: 0 findings
|
||||
|
||||
# 构建
|
||||
- name: 后端编译
|
||||
command: go build ./cmd/server
|
||||
- name: 前端构建
|
||||
command: npm run build
|
||||
|
||||
# 测试
|
||||
- name: 后端测试
|
||||
command: go test ./... -count=1 -race
|
||||
- name: 前端测试
|
||||
command: npm test -- --coverage
|
||||
- name: E2E测试
|
||||
command: npm run e2e:full:win
|
||||
```
|
||||
|
||||
### 1.3 问题分级(生产级)
|
||||
|
||||
| 级别 | 标识 | 定义 | 合并影响 |
|
||||
|------|------|------|----------|
|
||||
| **P0 阻塞** | 🔴 | 安全漏洞、数据丢失风险、生产不可用 | **必须修复** |
|
||||
| **P1 严重** | 🟠 | 功能错误、性能严重劣化、合规风险 | **必须修复** |
|
||||
| **P2 高** | 🟡 | 测试覆盖率不足、技术债积累、文档缺失 | **72小时内修复** |
|
||||
| **P3 中** | 🔵 | 代码风格、轻微优化、文档完善 | **本周修复** |
|
||||
| **P4 低** | 💭 | 挑剔级改进、愿望清单 | 可延迟 |
|
||||
|
||||
---
|
||||
|
||||
## 二、代码质量审查清单
|
||||
|
||||
### 2.1 测试覆盖率要求
|
||||
|
||||
```yaml
|
||||
coverage_requirements:
|
||||
backend:
|
||||
overall: 80%
|
||||
critical_paths:
|
||||
auth_handler: 90%
|
||||
jwt: 95%
|
||||
password: 95%
|
||||
repository: 70%
|
||||
excluded:
|
||||
- cmd/server/main.go # 可豁免
|
||||
- docs/ # 文档包
|
||||
- testdb/ # 测试数据库
|
||||
- testutil/ # 测试工具
|
||||
|
||||
frontend:
|
||||
overall: 80%
|
||||
critical_paths:
|
||||
auth: 90%
|
||||
http_client: 90%
|
||||
router: 100%
|
||||
guards: 100%
|
||||
```
|
||||
|
||||
### 2.2 单元测试审查
|
||||
|
||||
```
|
||||
□ 每个公开函数有单元测试
|
||||
□ 边界条件被测试(空值、极大值、特殊字符)
|
||||
□ 错误路径被测试
|
||||
□ 并发安全被测试(go test -race)
|
||||
□ 无 mock 滥用(真实依赖优先)
|
||||
□ 测试命名规范:Test[函数名][场景]
|
||||
□ 单元测试不依赖外部状态
|
||||
```
|
||||
|
||||
### 2.3 集成测试审查
|
||||
|
||||
```
|
||||
□ 数据库操作有集成测试
|
||||
□ API 端点有集成测试
|
||||
□ 认证流程有集成测试
|
||||
□ 测试使用隔离数据库(不污染开发数据)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、安全强度审查清单
|
||||
|
||||
### 3.1 自动化安全扫描(必须集成CI)
|
||||
|
||||
```yaml
|
||||
security_automation:
|
||||
gosec:
|
||||
schedule: daily
|
||||
fail_on: high/critical
|
||||
exclusions: documented
|
||||
|
||||
npm_audit:
|
||||
schedule: daily
|
||||
fail_on: moderate/high
|
||||
audit_level: moderate
|
||||
|
||||
govulncheck:
|
||||
schedule: weekly
|
||||
fail_on: any
|
||||
|
||||
owasp_dependency_check:
|
||||
schedule: weekly
|
||||
report: required
|
||||
```
|
||||
|
||||
### 3.2 安全审查清单
|
||||
|
||||
#### 认证安全
|
||||
```
|
||||
□ 密码使用 Argon2id(已验证 ✅)
|
||||
□ Token 使用 crypto/rand(已验证 ✅)
|
||||
□ JTI 防枚举(已验证 ✅)
|
||||
□ Token 滚动轮换(已验证 ✅)
|
||||
□ 登录速率限制(已验证 ✅)
|
||||
□ 异常登录检测(已验证 ✅)
|
||||
□ 退出登录清理状态(已验证 ✅)
|
||||
```
|
||||
|
||||
#### 数据安全
|
||||
```
|
||||
□ 敏感数据不写入日志
|
||||
□ 敏感数据不返回 API
|
||||
□ 数据库使用参数化查询
|
||||
□ 密码不硬编码
|
||||
□ 密钥从环境变量/密钥管理器读取
|
||||
```
|
||||
|
||||
#### 传输安全
|
||||
```
|
||||
□ HTTPS 强制
|
||||
□ HSTS 配置
|
||||
□ CSRF 保护
|
||||
□ CORS 非 wildcard
|
||||
```
|
||||
|
||||
#### 依赖安全
|
||||
```
|
||||
□ 无已知 CVE 漏洞
|
||||
□ 无弃用依赖
|
||||
□ 最小权限依赖原则
|
||||
```
|
||||
|
||||
### 3.3 渗透测试要求
|
||||
|
||||
```yaml
|
||||
penetration_testing:
|
||||
schedule: quarterly
|
||||
scope:
|
||||
- SQL注入测试
|
||||
- XSS测试
|
||||
- CSRF测试
|
||||
- 认证绕过测试
|
||||
- 会话管理测试
|
||||
- 敏感数据泄露测试
|
||||
report: required
|
||||
responsible: external_security_team
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、部署简单性审查清单
|
||||
|
||||
### 4.1 Docker 部署标准
|
||||
|
||||
```yaml
|
||||
docker_requirements:
|
||||
# 镜像构建
|
||||
multi_stage: true # 使用多阶段构建减小镜像
|
||||
non_root_user: true # 非 root 用户运行
|
||||
scratch_base: preferred # 最小基础镜像
|
||||
|
||||
# 健康检查
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# 资源限制
|
||||
resources:
|
||||
memory: 512Mi
|
||||
cpu: "500m"
|
||||
|
||||
# 重启策略
|
||||
restart: unless-stopped
|
||||
restart_max_attempts: 5
|
||||
```
|
||||
|
||||
### 4.2 Kubernetes 部署要求
|
||||
|
||||
```
|
||||
□ 有 Helm Chart 或 Kustomize 配置
|
||||
□ 有 Deployment/StatefulSet
|
||||
□ 有 Service 配置
|
||||
□ 有 Ingress 配置
|
||||
□ 有 ConfigMap/Secret 管理配置
|
||||
□ 有 HPA(水平自动扩缩容)
|
||||
□ 有 PodDisruptionBudget
|
||||
□ 有资源请求/限制
|
||||
□ 有健康检查(liveness/readiness)
|
||||
□ 有安全上下文
|
||||
□ 有网络策略
|
||||
```
|
||||
|
||||
### 4.3 部署配置管理
|
||||
|
||||
```
|
||||
□ 环境变量与配置分离
|
||||
□ 敏感配置使用 Secret
|
||||
□ 配置有版本控制
|
||||
□ 支持多环境部署(dev/staging/prod)
|
||||
□ 部署脚本幂等
|
||||
□ 回滚方案可用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、运维可靠性审查清单
|
||||
|
||||
### 5.1 监控要求
|
||||
|
||||
```yaml
|
||||
monitoring_requirements:
|
||||
# 基础设施监控
|
||||
infrastructure:
|
||||
- CPU使用率
|
||||
- 内存使用率
|
||||
- 磁盘使用率
|
||||
- 网络IO
|
||||
|
||||
# 应用监控
|
||||
application:
|
||||
- 请求延迟(P50/P95/P99)
|
||||
- 错误率
|
||||
- QPS
|
||||
- 活跃连接数
|
||||
|
||||
# 业务监控
|
||||
business:
|
||||
- 登录成功率
|
||||
- 注册成功率
|
||||
- API 调用成功率
|
||||
- Token 刷新成功率
|
||||
|
||||
# 数据库监控
|
||||
database:
|
||||
- 连接池使用率
|
||||
- 查询延迟
|
||||
- 慢查询数量
|
||||
- 复制延迟(如果有)
|
||||
```
|
||||
|
||||
### 5.2 告警要求
|
||||
|
||||
```yaml
|
||||
alerting_requirements:
|
||||
# 关键告警
|
||||
critical:
|
||||
- 服务不可用
|
||||
- 错误率 > 5%
|
||||
- P99延迟 > 1s
|
||||
- 数据库连接池耗尽
|
||||
|
||||
# 警告告警
|
||||
warning:
|
||||
- 错误率 > 1%
|
||||
- P95延迟 > 500ms
|
||||
- 磁盘使用率 > 80%
|
||||
- 内存使用率 > 85%
|
||||
|
||||
# 通知渠道
|
||||
channels:
|
||||
- email: on_call_team
|
||||
- slack: ops-alerts
|
||||
- sms: critical_only
|
||||
|
||||
# 告警升级
|
||||
escalation:
|
||||
- 5分钟未响应 → 升级
|
||||
- 15分钟未响应 → 升级到 manager
|
||||
```
|
||||
|
||||
### 5.3 日志要求
|
||||
|
||||
```
|
||||
□ 结构化日志(JSON)
|
||||
□ 日志级别配置
|
||||
□ 日志轮转配置
|
||||
□ 敏感信息脱敏
|
||||
□ 日志保留期配置(默认30天)
|
||||
□ 集中式日志收集
|
||||
□ 日志查询支持
|
||||
```
|
||||
|
||||
### 5.4 备份与恢复
|
||||
|
||||
```yaml
|
||||
backup_requirements:
|
||||
# 数据库备份
|
||||
database:
|
||||
frequency: daily
|
||||
retention: 30days
|
||||
verification: weekly
|
||||
encrypted: true
|
||||
offsite: true
|
||||
|
||||
# 配置备份
|
||||
config:
|
||||
frequency: on_change
|
||||
storage: git
|
||||
|
||||
# 文件备份
|
||||
files:
|
||||
frequency: weekly
|
||||
scope: uploads/
|
||||
|
||||
# 恢复测试
|
||||
recovery_test:
|
||||
frequency: quarterly
|
||||
documented: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、文档规范性审查清单
|
||||
|
||||
### 6.1 API 文档
|
||||
|
||||
```yaml
|
||||
api_documentation:
|
||||
# OpenAPI 规范
|
||||
openapi:
|
||||
version: "3.0.0"
|
||||
required: true
|
||||
generation: automated
|
||||
|
||||
# 文档内容
|
||||
content:
|
||||
- 所有端点有描述
|
||||
- 所有参数有说明
|
||||
- 所有响应有示例
|
||||
- 错误码有说明
|
||||
- 认证方式有说明
|
||||
|
||||
# 文档位置
|
||||
location:
|
||||
- swagger_ui: /swagger/index.html
|
||||
- openapi_json: /swagger/doc.json
|
||||
- redoc: /docs
|
||||
```
|
||||
|
||||
### 6.2 部署文档
|
||||
|
||||
```
|
||||
□ 部署前置条件清单
|
||||
□ 部署步骤(分环境)
|
||||
□ 环境变量说明
|
||||
□ 依赖服务说明
|
||||
□ 验证步骤
|
||||
□ 回滚步骤
|
||||
□ 常见问题与解决方案
|
||||
```
|
||||
|
||||
### 6.3 运维文档
|
||||
|
||||
```
|
||||
□ 系统架构图
|
||||
□ 组件说明
|
||||
□ 监控指标说明
|
||||
□ 告警处理手册
|
||||
□ 日志说明
|
||||
□ 备份恢复手册
|
||||
□ 扩容指南
|
||||
□ 故障排查手册
|
||||
□ 应急响应流程
|
||||
```
|
||||
|
||||
### 6.4 Runbook 要求
|
||||
|
||||
```yaml
|
||||
runbook_requirements:
|
||||
# 必需 Runbook
|
||||
required:
|
||||
- service_startup: 服务启动
|
||||
- service_shutdown: 服务停止
|
||||
- config_update: 配置更新
|
||||
- log_analysis: 日志分析
|
||||
- backup_restore: 备份恢复
|
||||
- incident_response: 事件响应
|
||||
- security_incident: 安全事件响应
|
||||
- scaling: 扩缩容
|
||||
- database_migration: 数据库迁移
|
||||
|
||||
# Runbook 格式
|
||||
format:
|
||||
- 触发条件明确
|
||||
- 步骤清晰可执行
|
||||
- 验证步骤明确
|
||||
- 回滚步骤存在
|
||||
- 联系人明确
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、差距分析与行动计划
|
||||
|
||||
### 7.1 当前差距评估
|
||||
|
||||
| 维度 | 当前状态 | 目标状态 | 差距 | 优先级 |
|
||||
|------|----------|----------|------|--------|
|
||||
| 后端测试覆盖率 | 32.1% | 80% | -47.9% | 🔴 P0 |
|
||||
| 前端测试覆盖率 | ~70% | 80% | -10% | 🟠 P1 |
|
||||
| gosec 集成 | 未安装 | 集成CI | N/A | 🔴 P0 |
|
||||
| JWT密钥占位符 | config.yaml | 环境变量 | N/A | 🔴 P0 |
|
||||
| Docker健康检查 | 无 | 必须 | N/A | 🟠 P1 |
|
||||
| K8s配置 | 无 | 必需 | N/A | 🟡 P2 |
|
||||
| 备份自动化 | 手动 | 自动 | N/A | 🟠 P1 |
|
||||
| 监控完善 | 基础 | 完整 | N/A | 🟡 P2 |
|
||||
| Runbook | 缺失 | 必需 | N/A | 🟡 P2 |
|
||||
| 渗透测试 | 无 | 季度 | N/A | 🟠 P1 |
|
||||
|
||||
### 7.2 修复行动计划
|
||||
|
||||
#### 🔴 P0 - 必须立即修复(本周)
|
||||
|
||||
| # | 任务 | 影响 | 工作量 |
|
||||
|---|------|------|--------|
|
||||
| 1 | 安装 gosec 并集成 CI | 安全扫描 | 2h |
|
||||
| 2 | 移除 config.yaml 占位密钥,改为环境变量 | 生产安全 | 1h |
|
||||
| 3 | 提升后端测试覆盖率至 60% | 代码质量 | 8h |
|
||||
| 4 | Docker 添加 healthcheck | 部署可靠性 | 1h |
|
||||
|
||||
#### 🟠 P1 - 本周内完成
|
||||
|
||||
| # | 任务 | 影响 | 工作量 |
|
||||
|---|------|------|--------|
|
||||
| 5 | 后端覆盖率提升至 80% | 代码质量 | 16h |
|
||||
| 6 | 前端覆盖率提升至 80% | 代码质量 | 8h |
|
||||
| 7 | Docker 添加资源限制 | 运维可靠性 | 1h |
|
||||
| 8 | 备份脚本自动化 | 运维可靠性 | 4h |
|
||||
| 9 | 季度渗透测试计划 | 安全合规 | 2h |
|
||||
|
||||
#### 🟡 P2 - 本月完成
|
||||
|
||||
| # | 任务 | 影响 | 工作量 |
|
||||
|---|------|------|--------|
|
||||
| 10 | K8s Helm Chart | 部署简单性 | 16h |
|
||||
| 11 | 完善监控指标 | 运维可靠性 | 8h |
|
||||
| 12 | 告警配置验证 | 运维可靠性 | 4h |
|
||||
| 13 | 核心 Runbook | 运维可靠性 | 8h |
|
||||
| 14 | OpenAPI 规范完善 | 文档规范性 | 4h |
|
||||
|
||||
---
|
||||
|
||||
## 八、审查流程(v3.0)
|
||||
|
||||
### 8.1 PR 审查流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ PR 创建 │
|
||||
└─────────────────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 1. CI 门禁(自动) │
|
||||
│ □ go vet / npm run lint │
|
||||
│ □ go build / npm run build │
|
||||
│ □ go test / npm test │
|
||||
│ □ 覆盖率检查(≥80%) │
|
||||
│ □ gosec / npm audit(安全扫描) │
|
||||
│ ⚠️ 任一失败 → PR Blocked │
|
||||
└─────────────────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 2. 人工审查(审查者) │
|
||||
│ □ 业务逻辑审查 │
|
||||
│ □ 安全审查 │
|
||||
│ □ 性能审查 │
|
||||
│ □ 可维护性审查 │
|
||||
│ □ 文档审查 │
|
||||
└─────────────────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 3. 问题修复(作者) │
|
||||
│ 🔴 P0 → 必须修复后重新审查 │
|
||||
│ 🟠 P1 → 必须修复后重新审查 │
|
||||
│ 🟡 P2 → 72小时内修复 │
|
||||
│ 🔵 P3 → 本周修复 │
|
||||
└─────────────────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 4. 审查确认(审查者) │
|
||||
│ □ 所有 🔴🟠 已修复 │
|
||||
│ □ 覆盖率达标 │
|
||||
│ □ 安全扫描通过 │
|
||||
│ □ Approve │
|
||||
└─────────────────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 5. 生产合并前检查 │
|
||||
│ □ E2E 测试通过 │
|
||||
│ □ 部署文档更新 │
|
||||
│ □ 变更日志记录 │
|
||||
│ □ 监控指标验证 │
|
||||
└─────────────────────────────────┴───────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 8.2 审查时效
|
||||
|
||||
| PR 类型 | 首次审查 | 问题修复复核 | 总时限 |
|
||||
|---------|----------|------------|--------|
|
||||
| 紧急修复 | 1小时 | 30分钟 | 4小时 |
|
||||
| 常规功能 | 4小时 | 2小时 | 24小时 |
|
||||
| 重构/优化 | 8小时 | 4小时 | 48小时 |
|
||||
| P0修复 | 30分钟 | 15分钟 | 2小时 |
|
||||
|
||||
---
|
||||
|
||||
## 九、合规要求
|
||||
|
||||
### 9.1 安全合规
|
||||
|
||||
```yaml
|
||||
security_compliance:
|
||||
# 数据保护
|
||||
data_protection:
|
||||
- GDPR合规(如果适用)
|
||||
- 数据加密存储
|
||||
- 数据传输加密
|
||||
- 数据访问审计
|
||||
|
||||
# 访问控制
|
||||
access_control:
|
||||
- 最小权限原则
|
||||
- 密钥轮换
|
||||
- 多因素认证
|
||||
- 访问审计日志
|
||||
|
||||
# 漏洞管理
|
||||
vulnerability_management:
|
||||
- 依赖扫描(每日)
|
||||
- 渗透测试(季度)
|
||||
- 漏洞修复 SLA(严重24h,高危7天)
|
||||
```
|
||||
|
||||
### 9.2 运营合规
|
||||
|
||||
```yaml
|
||||
operational_compliance:
|
||||
# 可用性
|
||||
availability:
|
||||
- SLO定义(99.9%)
|
||||
- SLA承诺(99.5%)
|
||||
- 错误预算监控
|
||||
|
||||
# 备份
|
||||
backup:
|
||||
- 备份策略文档
|
||||
- 恢复测试记录
|
||||
- 备份保留策略
|
||||
|
||||
# 事件管理
|
||||
incident_management:
|
||||
- 事件分级标准
|
||||
- 升级流程
|
||||
- 事后复盘要求
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、附录
|
||||
|
||||
### 10.1 快速检查命令
|
||||
|
||||
```bash
|
||||
# 完整门禁检查(生产合并前必须)
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== 代码质量检查 ==="
|
||||
go test ./... -count=1 -race
|
||||
go tool cover -func coverage.out | grep total | awk '{print "Coverage:", $3}'
|
||||
|
||||
echo "=== 安全扫描 ==="
|
||||
gosec ./...
|
||||
npm audit
|
||||
govulncheck ./...
|
||||
|
||||
echo "=== 构建检查 ==="
|
||||
go build ./cmd/server
|
||||
npm run build
|
||||
|
||||
echo "=== E2E 测试 ==="
|
||||
npm run e2e:full:win
|
||||
|
||||
echo "=== 所有检查通过 ==="
|
||||
```
|
||||
|
||||
### 10.2 评分计算器
|
||||
|
||||
```
|
||||
综合评分 = 代码质量×0.25 + 安全强度×0.30 + 部署简单性×0.15 + 运维可靠性×0.20 + 文档规范性×0.10
|
||||
|
||||
生产标准:
|
||||
- ≥9.0:卓越,可随时发布
|
||||
- 8.0-8.9:优秀,可发布
|
||||
- 7.0-7.9:良好,修复P2后发布
|
||||
- 6.0-6.9:需要改进,修复P1后发布
|
||||
- <6.0:不合格,修复P0后重新评估
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*本文档由代码审查专家 Agent 生成*
|
||||
*版本: v3.0*
|
||||
*最后更新: 2026-04-08*
|
||||
*下次审查: 2026-04-15*
|
||||
488
docs/code-review/PRODUCTION_GAP_ANALYSIS_2026-04-08.md
Normal file
488
docs/code-review/PRODUCTION_GAP_ANALYSIS_2026-04-08.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# 生产级质量差距分析报告
|
||||
|
||||
**审查日期**: 2026-04-08
|
||||
**审查范围**: 用户管理系统(UMS)全栈代码
|
||||
**评估标准**: CODE_REVIEW_STANDARD_V3.md
|
||||
**审查专家**: 代码审查专家
|
||||
|
||||
---
|
||||
|
||||
## 执行摘要
|
||||
|
||||
### 整体评估
|
||||
|
||||
| 维度 | v2.0评分 | v3.0评分 | 真实差距 |
|
||||
|------|----------|----------|----------|
|
||||
| **代码质量** | 9.7/10 | **7.5/10** | -2.2 |
|
||||
| **安全强度** | 9.7/10 | **6.0/10** | -3.7 |
|
||||
| **部署简单性** | 8.0/10 | **5.0/10** | -3.0 |
|
||||
| **运维可靠性** | 7.0/10 | **4.0/10** | -3.0 |
|
||||
| **文档规范性** | 7.0/10 | **5.0/10** | -2.0 |
|
||||
|
||||
**综合评分**: **5.9/10 ⚠️ 不合格**
|
||||
|
||||
### 关键发现
|
||||
|
||||
> 🔴 **生产上线存在重大差距,代码审查标准v2.0评估过于乐观**
|
||||
|
||||
1. **测试覆盖率严重不足**:后端覆盖率仅32.1%,远低于生产标准80%
|
||||
2. **安全扫描缺失**:无gosec集成、无渗透测试计划
|
||||
3. **配置安全性问题**:JWT密钥使用占位符
|
||||
4. **部署配置简陋**:Docker无健康检查、无资源限制
|
||||
5. **运维保障薄弱**:无备份自动化、无灾备方案
|
||||
|
||||
---
|
||||
|
||||
## 一、代码质量差距分析
|
||||
|
||||
### 1.1 测试覆盖率真相
|
||||
|
||||
#### 后端覆盖率(实际测量)
|
||||
|
||||
```
|
||||
github.com/user-management-system/internal/api/handler
|
||||
├── auth_handler.go: 10.0% ⚠️
|
||||
├── user_handler.go: 0.0% 🔴
|
||||
└── ...
|
||||
|
||||
github.com/user-management-system/internal/auth
|
||||
├── jwt.go: 23.8% ⚠️
|
||||
├── password.go: 80.6% ✅
|
||||
└── ...
|
||||
|
||||
github.com/user-management-system/internal/repository
|
||||
├── user.go: 15.3% 🔴
|
||||
├── device.go: 0.0% 🔴
|
||||
└── ...
|
||||
|
||||
github.com/user-management-system/cmd/server
|
||||
└── main.go: 0.0% 🔴
|
||||
|
||||
总计覆盖率: 32.1% 🔴
|
||||
```
|
||||
|
||||
| 模块 | 当前覆盖 | 目标覆盖 | 差距 |
|
||||
|------|----------|----------|------|
|
||||
| api/handler | 10% | 90% | -80% |
|
||||
| repository | 15% | 70% | -55% |
|
||||
| service | 30% | 70% | -40% |
|
||||
| auth | 24% | 90% | -66% |
|
||||
| **总计** | **32.1%** | **80%** | **-47.9%** |
|
||||
|
||||
#### 前端覆盖率(近期测量)
|
||||
|
||||
```
|
||||
statements: ~70%
|
||||
branches: ~80%
|
||||
functions: ~90%
|
||||
lines: ~70%
|
||||
```
|
||||
|
||||
### 1.2 关键代码问题
|
||||
|
||||
#### 🔴 P0: cmd/server/main.go 零覆盖
|
||||
|
||||
```go
|
||||
// main.go - 核心入口,无测试覆盖
|
||||
func main() {
|
||||
// 服务启动逻辑完全无测试
|
||||
// 健康检查、优雅关闭全部裸奔
|
||||
}
|
||||
```
|
||||
|
||||
**风险**:无法验证服务启动、配置加载、依赖初始化的正确性
|
||||
|
||||
#### 🔴 P0: auth_handler.go 覆盖率仅10%
|
||||
|
||||
```go
|
||||
// auth_handler.go - 核心认证处理器
|
||||
func (h *AuthHandler) Login(c *gin.Context) // 81.8% - 部分覆盖
|
||||
func (h *AuthHandler) Logout(c *gin.Context) // 0.0% - 未覆盖
|
||||
func (h *AuthHandler) RefreshToken(...) // 0.0% - 未覆盖
|
||||
func (h *AuthHandler) GetUserInfo(...) // 0.0% - 未覆盖
|
||||
func (h *AuthHandler) GetCSRFToken(...) // 0.0% - 未覆盖
|
||||
```
|
||||
|
||||
**风险**:登录登出流程未充分测试,生产可能存在未发现的bug
|
||||
|
||||
#### 🟠 P1: repository 层覆盖率极低
|
||||
|
||||
```go
|
||||
// repository/user.go - 15.3%
|
||||
// repository/device.go - 0.0%
|
||||
// repository/role.go - 15.0%
|
||||
```
|
||||
|
||||
**风险**:数据库操作未充分测试,边界条件和错误处理可能存在缺陷
|
||||
|
||||
---
|
||||
|
||||
## 二、安全强度差距分析
|
||||
|
||||
### 2.1 安全工具缺失
|
||||
|
||||
#### 🔴 P0: gosec 未安装
|
||||
|
||||
```bash
|
||||
$ gosec ./...
|
||||
gosec : 无法将"gosec"项识别为 cmdlet...
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 无法进行自动化安全扫描
|
||||
- 无法在CI中集成安全检查
|
||||
- 可能遗漏常见安全漏洞
|
||||
|
||||
**影响**:
|
||||
- OWASP Top 10 漏洞可能未检测
|
||||
- 高危漏洞可能在生产发现
|
||||
|
||||
### 2.2 配置安全问题
|
||||
|
||||
#### 🔴 P0: JWT密钥使用占位符
|
||||
|
||||
```yaml
|
||||
# configs/config.yaml
|
||||
jwt:
|
||||
secret: "change-me-in-production-use-at-least-32-bytes-secret" # ⚠️
|
||||
```
|
||||
|
||||
**风险**:
|
||||
- 如果部署时忘记修改,生产JWT密钥将完全可预测
|
||||
- 攻击者可伪造任意token
|
||||
|
||||
**修复方案**:
|
||||
```yaml
|
||||
jwt:
|
||||
secret: "" # 必须从环境变量读取
|
||||
```
|
||||
|
||||
### 2.3 安全措施验证
|
||||
|
||||
| 安全措施 | 实现状态 | 生产标准 | 差距 |
|
||||
|----------|----------|----------|------|
|
||||
| 密码哈希 | ✅ Argon2id | 必须 | 已满足 |
|
||||
| Token生成 | ✅ crypto/rand | 必须 | 已满足 |
|
||||
| SQL注入防护 | ✅ GORM参数化 | 必须 | 已满足 |
|
||||
| XSS防护 | ✅ 输出编码 | 必须 | 已满足 |
|
||||
| CSRF保护 | ✅ CSRF Token | 必须 | 已满足 |
|
||||
| 速率限制 | ✅ 已实现 | 必须 | 已满足 |
|
||||
| 安全扫描 | ❌ 无gosec | 必须 | 🔴 |
|
||||
| 渗透测试 | ❌ 无 | 季度 | 🔴 |
|
||||
|
||||
---
|
||||
|
||||
## 三、部署简单性差距分析
|
||||
|
||||
### 3.1 Docker配置问题
|
||||
|
||||
#### 🔴 P0: 缺少健康检查
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml - 当前配置
|
||||
user-management:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
# ❌ 缺少 healthcheck
|
||||
```
|
||||
|
||||
**风险**:
|
||||
- K8s/负载均衡无法判断服务健康状态
|
||||
- 故障实例可能继续接收流量
|
||||
- 滚动更新无法正确判断就绪
|
||||
|
||||
**修复**:
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
```
|
||||
|
||||
#### 🔴 P0: 缺少资源限制
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml - 当前配置
|
||||
user-management:
|
||||
build: .
|
||||
# ❌ 缺少 resources
|
||||
```
|
||||
|
||||
**风险**:
|
||||
- 无内存限制,可能OOM
|
||||
- 无CPU限制,可能过度占用
|
||||
- 容器可能影响宿主机稳定性
|
||||
|
||||
**修复**:
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: '0.5'
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
```
|
||||
|
||||
### 3.2 部署能力评估
|
||||
|
||||
| 部署能力 | 当前状态 | 目标状态 | 差距 |
|
||||
|----------|----------|----------|------|
|
||||
| Docker构建 | ✅ 可构建 | 必须 | 已满足 |
|
||||
| 多阶段构建 | ❌ 无 | 推荐 | 🟡 |
|
||||
| 非root运行 | ❌ 未知 | 推荐 | 🟡 |
|
||||
| 健康检查 | ❌ 无 | 必须 | 🔴 |
|
||||
| 资源限制 | ❌ 无 | 必须 | 🔴 |
|
||||
| 重启策略 | ❌ 无 | 必须 | 🔴 |
|
||||
| K8s部署 | ❌ 无 | 推荐 | 🟡 |
|
||||
| Helm Chart | ❌ 无 | 推荐 | 🟡 |
|
||||
|
||||
---
|
||||
|
||||
## 四、运维可靠性差距分析
|
||||
|
||||
### 4.1 监控现状
|
||||
|
||||
#### 🟡 P2: 监控指标不足
|
||||
|
||||
```go
|
||||
// internal/monitoring/collector.go - 当前采集指标
|
||||
- 内存使用 (runtime.MemStats.Alloc)
|
||||
- Goroutine数量
|
||||
- 数据库连接池使用
|
||||
```
|
||||
|
||||
**缺失的监控**:
|
||||
- 请求延迟分布(P50/P95/P99)
|
||||
- QPS/错误率
|
||||
- 业务指标(登录成功率等)
|
||||
- 自定义业务指标
|
||||
|
||||
### 4.2 告警现状
|
||||
|
||||
| 告警能力 | 当前状态 | 目标状态 | 差距 |
|
||||
|----------|----------|----------|------|
|
||||
| 告警配置 | ⚠️ 存在但不完整 | 必须 | 🟡 |
|
||||
| 告警测试 | ❌ 未验证 | 必须 | 🔴 |
|
||||
| 升级流程 | ❌ 无 | 必须 | 🔴 |
|
||||
| 通知渠道 | ❌ 配置但不验证 | 必须 | 🔴 |
|
||||
|
||||
### 4.3 备份恢复现状
|
||||
|
||||
#### 🔴 P0: 备份恢复未自动化
|
||||
|
||||
**当前状态**:
|
||||
- 手动执行备份脚本
|
||||
- 恢复过程未文档化
|
||||
- 无定期恢复演练
|
||||
|
||||
**风险**:
|
||||
- 灾难发生时可能无法快速恢复
|
||||
- 人工操作可能出错
|
||||
- 无法保证RTO/RPO
|
||||
|
||||
**目标**:
|
||||
```yaml
|
||||
backup:
|
||||
frequency: daily
|
||||
automated: true
|
||||
retention: 30days
|
||||
encrypted: true
|
||||
offsite: true
|
||||
recovery_test_frequency: quarterly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、文档规范性差距分析
|
||||
|
||||
### 5.1 文档现状评估
|
||||
|
||||
| 文档类型 | 存在 | 完整 | 可用 | 生产标准 |
|
||||
|----------|------|------|------|----------|
|
||||
| API文档 | ✅ | ⚠️ 部分 | ⚠️ 需Swagger | 🔴 |
|
||||
| 部署文档 | ✅ | ⚠️ 基础 | ✅ | 🟡 |
|
||||
| 架构文档 | ✅ | ⚠️ 基础 | ✅ | 🟡 |
|
||||
| Runbook | ❌ | ❌ | ❌ | 🔴 |
|
||||
| 应急响应 | ❌ | ❌ | ❌ | 🔴 |
|
||||
| 安全策略 | ⚠️ | ❌ | ❌ | 🔴 |
|
||||
|
||||
### 5.2 API文档问题
|
||||
|
||||
#### 🟡 P2: 缺少Swagger注解
|
||||
|
||||
```go
|
||||
// 当前:手写API.md文档
|
||||
// 问题:需要手动维护,容易过时
|
||||
|
||||
// 目标:使用Swagger注解自动生成
|
||||
// @Summary 用户登录
|
||||
// @Description 用户使用账号密码登录系统
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LoginRequest true "登录请求"
|
||||
// @Success 200 {object} LoginResponse
|
||||
// @Router /api/v1/auth/login [post]
|
||||
```
|
||||
|
||||
### 5.3 Runbook缺失
|
||||
|
||||
**必需的Runbook(当前全部缺失)**:
|
||||
|
||||
| Runbook | 用途 | 优先级 |
|
||||
|---------|------|--------|
|
||||
| 服务启动 | 新服务器部署 | 🔴 |
|
||||
| 服务停止 | 维护操作 | 🔴 |
|
||||
| 配置更新 | 修改配置 | 🔴 |
|
||||
| 日志分析 | 问题排查 | 🔴 |
|
||||
| 备份恢复 | 数据恢复 | 🔴 |
|
||||
| 安全事件 | 安全问题处理 | 🔴 |
|
||||
| 扩容操作 | 应对流量高峰 | 🟠 |
|
||||
|
||||
---
|
||||
|
||||
## 六、问题汇总
|
||||
|
||||
### 6.1 P0 阻塞问题(必须立即修复)
|
||||
|
||||
| # | 问题 | 维度 | 影响 | 修复工作量 |
|
||||
|---|------|------|------|------------|
|
||||
| 1 | 后端覆盖率仅32.1% | 代码质量 | 生产bug风险 | 16h |
|
||||
| 2 | gosec未安装/集成 | 安全 | 漏洞未检测 | 2h |
|
||||
| 3 | JWT密钥占位符 | 安全 | 生产安全风险 | 1h |
|
||||
| 4 | Docker无健康检查 | 部署 | 故障发现延迟 | 1h |
|
||||
| 5 | Docker无资源限制 | 运维 | 资源耗尽风险 | 1h |
|
||||
| 6 | 无备份自动化 | 运维 | 恢复能力缺失 | 4h |
|
||||
| 7 | Runbook全部缺失 | 文档 | 运维能力缺失 | 8h |
|
||||
|
||||
### 6.2 P1 严重问题(本周修复)
|
||||
|
||||
| # | 问题 | 维度 | 影响 | 修复工作量 |
|
||||
|---|------|------|----------|------------|
|
||||
| 8 | 后端覆盖率<60% | 代码质量 | 测试不足 | 8h |
|
||||
| 9 | auth_handler覆盖<50% | 代码质量 | 认证风险 | 4h |
|
||||
| 10 | 季度渗透测试缺失 | 安全 | 合规风险 | 2h |
|
||||
| 11 | 告警配置未验证 | 运维 | 告警失效 | 4h |
|
||||
| 12 | 无灾难恢复方案 | 运维 | 灾难风险 | 4h |
|
||||
|
||||
### 6.3 P2 高优先级问题(本月修复)
|
||||
|
||||
| # | 问题 | 维度 | 修复工作量 |
|
||||
|---|------|------|------------|
|
||||
| 13 | 后端覆盖率<80% | 代码质量 | 8h |
|
||||
| 14 | K8s部署配置 | 部署 | 16h |
|
||||
| 15 | 监控指标完善 | 运维 | 8h |
|
||||
| 16 | OpenAPI Swagger | 文档 | 4h |
|
||||
|
||||
---
|
||||
|
||||
## 七、修复路线图
|
||||
|
||||
### 第一阶段:止血(本周)
|
||||
|
||||
```
|
||||
目标:修复所有P0问题
|
||||
时间:5天
|
||||
工作量:~33h
|
||||
|
||||
Day 1:
|
||||
[ ] 安装gosec并验证
|
||||
[ ] 移除JWT占位符,改用环境变量
|
||||
[ ] Docker添加healthcheck
|
||||
|
||||
Day 2-3:
|
||||
[ ] 后端覆盖率提升至50%
|
||||
[ ] 重点:auth_handler, main.go
|
||||
|
||||
Day 4:
|
||||
[ ] Docker添加资源限制
|
||||
[ ] 备份脚本自动化
|
||||
|
||||
Day 5:
|
||||
[ ] 编写核心Runbook(5个)
|
||||
[ ] 验证告警配置
|
||||
```
|
||||
|
||||
### 第二阶段:达标(本月)
|
||||
|
||||
```
|
||||
目标:修复P1问题,核心指标达标
|
||||
时间:4周
|
||||
工作量:~42h
|
||||
|
||||
Week 2:
|
||||
[ ] 后端覆盖率80%
|
||||
[ ] 季度渗透测试计划
|
||||
|
||||
Week 3:
|
||||
[ ] K8s Helm Chart
|
||||
[ ] 监控完善
|
||||
|
||||
Week 4:
|
||||
[ ] 所有Runbook
|
||||
[ ] OpenAPI完善
|
||||
[ ] 灾难恢复方案
|
||||
```
|
||||
|
||||
### 第三阶段:卓越(下季度)
|
||||
|
||||
```
|
||||
目标:达到生产卓越标准
|
||||
时间:季度
|
||||
工作量:待定
|
||||
|
||||
Q2:
|
||||
[ ] 自动化安全扫描集成CI
|
||||
[ ] 合规审计
|
||||
[ ] 性能基准测试
|
||||
[ ] 灾备演练
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、结论与建议
|
||||
|
||||
### 8.1 诚实评估
|
||||
|
||||
**当前状态**:⚠️ **5.9/10 不合格**
|
||||
|
||||
**核心问题**:
|
||||
1. 测试覆盖率严重不足(32.1% vs 80%)
|
||||
2. 安全扫描工具缺失
|
||||
3. 部署配置简陋
|
||||
4. 运维保障薄弱
|
||||
|
||||
**v2.0评估过于乐观**:之前的9.7分未充分考虑生产级标准
|
||||
|
||||
### 8.2 行动建议
|
||||
|
||||
| 优先级 | 行动 | 期限 |
|
||||
|--------|------|------|
|
||||
| 🔴 P0 | 提升后端覆盖率至50% | 本周 |
|
||||
| 🔴 P0 | 移除JWT占位符 | 今天 |
|
||||
| 🔴 P0 | 安装gosec | 今天 |
|
||||
| 🔴 P0 | Docker健康检查 | 今天 |
|
||||
| 🟠 P1 | 覆盖率至80% | 本月 |
|
||||
| 🟠 P1 | 备份自动化 | 本周 |
|
||||
| 🟠 P1 | Runbook基础版 | 本周 |
|
||||
|
||||
### 8.3 合并门禁建议
|
||||
|
||||
**在以下条件满足前,禁止合并到main分支用于生产**:
|
||||
|
||||
1. ✅ go test覆盖率 ≥ 60%
|
||||
2. ✅ gosec扫描无高危漏洞
|
||||
3. ✅ Docker包含healthcheck
|
||||
4. ✅ JWT密钥从环境变量读取
|
||||
5. ✅ 备份脚本可执行
|
||||
|
||||
---
|
||||
|
||||
*本报告由代码审查专家 Agent 生成*
|
||||
*审查日期: 2026-04-08*
|
||||
*标准版本: CODE_REVIEW_STANDARD_V3.md*
|
||||
1454
gosec-report.json
Normal file
1454
gosec-report.json
Normal file
File diff suppressed because it is too large
Load Diff
75
internal/service/auth_runtime_test.go
Normal file
75
internal/service/auth_runtime_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Auth Runtime Helper Functions Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestIsUserNotFoundError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "gorm record not found",
|
||||
err: gorm.ErrRecordNotFound,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wrapped gorm record not found",
|
||||
err: errors.Join(gorm.ErrRecordNotFound, errors.New("additional context")),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "other error",
|
||||
err: errors.New("some other error"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "generic error",
|
||||
err: errors.New("something went wrong"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "error containing user not found",
|
||||
err: errors.New("user not found"),
|
||||
expected: true, // contains "user not found" in lowercase
|
||||
},
|
||||
{
|
||||
name: "error containing record not found",
|
||||
err: errors.New("record not found"),
|
||||
expected: true, // contains "record not found"
|
||||
},
|
||||
{
|
||||
name: "error containing not found",
|
||||
err: errors.New("entity not found"),
|
||||
expected: true, // contains "not found"
|
||||
},
|
||||
{
|
||||
name: "error containing 用户不存在",
|
||||
err: errors.New("用户不存在"),
|
||||
expected: true, // contains Chinese "用户不存在"
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isUserNotFoundError(tt.err)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isUserNotFoundError(%v) = %v, want %v", tt.err, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
99
internal/service/classified_error_test.go
Normal file
99
internal/service/classified_error_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Classified Error Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestClassifiedError(t *testing.T) {
|
||||
// Test error with message
|
||||
e1 := &classifiedError{message: "custom message", cause: errors.New("cause")}
|
||||
if e1.Error() != "custom message" {
|
||||
t.Errorf("Error() = %q, want %q", e1.Error(), "custom message")
|
||||
}
|
||||
|
||||
// Test error with cause but no message
|
||||
e2 := &classifiedError{cause: errors.New("underlying error")}
|
||||
if e2.Error() != "underlying error" {
|
||||
t.Errorf("Error() = %q, want %q", e2.Error(), "underlying error")
|
||||
}
|
||||
|
||||
// Test error with neither message nor cause
|
||||
e3 := &classifiedError{}
|
||||
if e3.Error() != "" {
|
||||
t.Errorf("Error() = %q, want empty string", e3.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifiedErrorUnwrap(t *testing.T) {
|
||||
innerErr := errors.New("inner error")
|
||||
e := &classifiedError{message: "outer", cause: innerErr}
|
||||
|
||||
unwrapped := e.Unwrap()
|
||||
if unwrapped != innerErr {
|
||||
t.Errorf("Unwrap() = %v, want %v", unwrapped, innerErr)
|
||||
}
|
||||
|
||||
// Test errors.Is
|
||||
if !errors.Is(e, innerErr) {
|
||||
t.Error("errors.Is(e, innerErr) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRateLimitError(t *testing.T) {
|
||||
err := newRateLimitError("too many requests")
|
||||
|
||||
// Should be a classifiedError
|
||||
var ce *classifiedError
|
||||
if !errors.As(err, &ce) {
|
||||
t.Errorf("errors.As(err, &classifiedError{}) = false")
|
||||
}
|
||||
|
||||
// Should wrap ErrRateLimitExceeded
|
||||
if !errors.Is(err, ErrRateLimitExceeded) {
|
||||
t.Error("errors.Is(err, ErrRateLimitExceeded) = false")
|
||||
}
|
||||
|
||||
// Error message should be "too many requests"
|
||||
if err.Error() != "too many requests" {
|
||||
t.Errorf("err.Error() = %q, want %q", err.Error(), "too many requests")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewValidationError(t *testing.T) {
|
||||
err := newValidationError("invalid input")
|
||||
|
||||
// Should be a classifiedError
|
||||
var ce *classifiedError
|
||||
if !errors.As(err, &ce) {
|
||||
t.Errorf("errors.As(err, &classifiedError{}) = false")
|
||||
}
|
||||
|
||||
// Should wrap ErrValidationFailed
|
||||
if !errors.Is(err, ErrValidationFailed) {
|
||||
t.Error("errors.Is(err, ErrValidationFailed) = false")
|
||||
}
|
||||
|
||||
// Error message should be "invalid input"
|
||||
if err.Error() != "invalid input" {
|
||||
t.Errorf("err.Error() = %q, want %q", err.Error(), "invalid input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrRateLimitExceeded(t *testing.T) {
|
||||
// ErrRateLimitExceeded is a sentinel error
|
||||
if ErrRateLimitExceeded.Error() != "rate limit exceeded" {
|
||||
t.Errorf("ErrRateLimitExceeded.Error() = %q, want %q", ErrRateLimitExceeded.Error(), "rate limit exceeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrValidationFailed(t *testing.T) {
|
||||
// ErrValidationFailed is a sentinel error
|
||||
if ErrValidationFailed.Error() != "validation failed" {
|
||||
t.Errorf("ErrValidationFailed.Error() = %q, want %q", ErrValidationFailed.Error(), "validation failed")
|
||||
}
|
||||
}
|
||||
63
internal/service/config_defaults_test.go
Normal file
63
internal/service/config_defaults_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Password Reset Configuration Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestDefaultPasswordResetConfig(t *testing.T) {
|
||||
cfg := DefaultPasswordResetConfig()
|
||||
|
||||
if cfg.TokenTTL != 15*time.Minute {
|
||||
t.Errorf("TokenTTL = %v, want %v", cfg.TokenTTL, 15*time.Minute)
|
||||
}
|
||||
if cfg.SMTPHost != "" {
|
||||
t.Errorf("SMTPHost = %q, want empty", cfg.SMTPHost)
|
||||
}
|
||||
if cfg.SMTPPort != 587 {
|
||||
t.Errorf("SMTPPort = %d, want 587", cfg.SMTPPort)
|
||||
}
|
||||
if cfg.SMTPUser != "" {
|
||||
t.Errorf("SMTPUser = %q, want empty", cfg.SMTPUser)
|
||||
}
|
||||
if cfg.SMTPPass != "" {
|
||||
t.Errorf("SMTPPass = %q, want empty", cfg.SMTPPass)
|
||||
}
|
||||
if cfg.FromEmail != "noreply@example.com" {
|
||||
t.Errorf("FromEmail = %q, want %q", cfg.FromEmail, "noreply@example.com")
|
||||
}
|
||||
if cfg.SiteURL != "http://localhost:8080" {
|
||||
t.Errorf("SiteURL = %q, want %q", cfg.SiteURL, "http://localhost:8080")
|
||||
}
|
||||
if cfg.PasswordMinLen != 8 {
|
||||
t.Errorf("PasswordMinLen = %d, want 8", cfg.PasswordMinLen)
|
||||
}
|
||||
if cfg.PasswordRequireSpecial != false {
|
||||
t.Error("PasswordRequireSpecial = true, want false")
|
||||
}
|
||||
if cfg.PasswordRequireNumber != false {
|
||||
t.Error("PasswordRequireNumber = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SMS Configuration Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestDefaultSMSCodeConfig(t *testing.T) {
|
||||
cfg := DefaultSMSCodeConfig()
|
||||
|
||||
if cfg.CodeTTL != 5*time.Minute {
|
||||
t.Errorf("CodeTTL = %v, want %v", cfg.CodeTTL, 5*time.Minute)
|
||||
}
|
||||
if cfg.ResendCooldown != time.Minute {
|
||||
t.Errorf("ResendCooldown = %v, want %v", cfg.ResendCooldown, time.Minute)
|
||||
}
|
||||
if cfg.MaxDailyLimit != 10 {
|
||||
t.Errorf("MaxDailyLimit = %d, want 10", cfg.MaxDailyLimit)
|
||||
}
|
||||
}
|
||||
30
internal/service/email_config_test.go
Normal file
30
internal/service/email_config_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Email Configuration Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestDefaultEmailCodeConfig(t *testing.T) {
|
||||
cfg := DefaultEmailCodeConfig()
|
||||
|
||||
if cfg.CodeTTL != 5*time.Minute {
|
||||
t.Errorf("CodeTTL = %v, want %v", cfg.CodeTTL, 5*time.Minute)
|
||||
}
|
||||
if cfg.ResendCooldown != time.Minute {
|
||||
t.Errorf("ResendCooldown = %v, want %v", cfg.ResendCooldown, time.Minute)
|
||||
}
|
||||
if cfg.MaxDailyLimit != 10 {
|
||||
t.Errorf("MaxDailyLimit = %d, want 10", cfg.MaxDailyLimit)
|
||||
}
|
||||
if cfg.SiteURL != "http://localhost:8080" {
|
||||
t.Errorf("SiteURL = %q, want %q", cfg.SiteURL, "http://localhost:8080")
|
||||
}
|
||||
if cfg.SiteName != "User Management System" {
|
||||
t.Errorf("SiteName = %q, want %q", cfg.SiteName, "User Management System")
|
||||
}
|
||||
}
|
||||
180
internal/service/request_metadata_test.go
Normal file
180
internal/service/request_metadata_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Request Metadata Context Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestRequestMetadataFallbackStats(t *testing.T) {
|
||||
isMaxTokens, thinking, prefetchAccount, prefetchGroup, singleAccount, accountSwitch := RequestMetadataFallbackStats()
|
||||
|
||||
if isMaxTokens != 0 {
|
||||
t.Errorf("isMaxTokens = %d, want 0", isMaxTokens)
|
||||
}
|
||||
if thinking != 0 {
|
||||
t.Errorf("thinking = %d, want 0", thinking)
|
||||
}
|
||||
if prefetchAccount != 0 {
|
||||
t.Errorf("prefetchAccount = %d, want 0", prefetchAccount)
|
||||
}
|
||||
if prefetchGroup != 0 {
|
||||
t.Errorf("prefetchGroup = %d, want 0", prefetchGroup)
|
||||
}
|
||||
if singleAccount != 0 {
|
||||
t.Errorf("singleAccount = %d, want 0", singleAccount)
|
||||
}
|
||||
if accountSwitch != 0 {
|
||||
t.Errorf("accountSwitch = %d, want 0", accountSwitch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithIsMaxTokensOneHaikuRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test setting true
|
||||
ctx1 := WithIsMaxTokensOneHaikuRequest(ctx, true, false)
|
||||
val, ok := IsMaxTokensOneHaikuRequestFromContext(ctx1)
|
||||
if !ok {
|
||||
t.Error("IsMaxTokensOneHaikuRequestFromContext returned !ok")
|
||||
}
|
||||
if val != true {
|
||||
t.Errorf("IsMaxTokensOneHaikuRequestFromContext = %v, want true", val)
|
||||
}
|
||||
|
||||
// Test setting false
|
||||
ctx2 := WithIsMaxTokensOneHaikuRequest(ctx, false, false)
|
||||
val2, ok2 := IsMaxTokensOneHaikuRequestFromContext(ctx2)
|
||||
if !ok2 {
|
||||
t.Error("IsMaxTokensOneHaikuRequestFromContext returned !ok")
|
||||
}
|
||||
if val2 != false {
|
||||
t.Errorf("IsMaxTokensOneHaikuRequestFromContext = %v, want false", val2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithThinkingEnabled(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test setting true
|
||||
ctx1 := WithThinkingEnabled(ctx, true, false)
|
||||
val, ok := ThinkingEnabledFromContext(ctx1)
|
||||
if !ok {
|
||||
t.Error("ThinkingEnabledFromContext returned !ok")
|
||||
}
|
||||
if val != true {
|
||||
t.Errorf("ThinkingEnabledFromContext = %v, want true", val)
|
||||
}
|
||||
|
||||
// Test setting false
|
||||
ctx2 := WithThinkingEnabled(ctx, false, false)
|
||||
val2, ok2 := ThinkingEnabledFromContext(ctx2)
|
||||
if !ok2 {
|
||||
t.Error("ThinkingEnabledFromContext returned !ok")
|
||||
}
|
||||
if val2 != false {
|
||||
t.Errorf("ThinkingEnabledFromContext = %v, want false", val2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithPrefetchedStickySession(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test setting values
|
||||
ctx1 := WithPrefetchedStickySession(ctx, 123, 456, false)
|
||||
accountID, ok := PrefetchedStickyAccountIDFromContext(ctx1)
|
||||
if !ok {
|
||||
t.Error("PrefetchedStickyAccountIDFromContext returned !ok")
|
||||
}
|
||||
if accountID != 123 {
|
||||
t.Errorf("PrefetchedStickyAccountIDFromContext = %d, want 123", accountID)
|
||||
}
|
||||
|
||||
groupID, ok2 := PrefetchedStickyGroupIDFromContext(ctx1)
|
||||
if !ok2 {
|
||||
t.Error("PrefetchedStickyGroupIDFromContext returned !ok")
|
||||
}
|
||||
if groupID != 456 {
|
||||
t.Errorf("PrefetchedStickyGroupIDFromContext = %d, want 456", groupID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithSingleAccountRetry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test setting true
|
||||
ctx1 := WithSingleAccountRetry(ctx, true, false)
|
||||
val, ok := SingleAccountRetryFromContext(ctx1)
|
||||
if !ok {
|
||||
t.Error("SingleAccountRetryFromContext returned !ok")
|
||||
}
|
||||
if val != true {
|
||||
t.Errorf("SingleAccountRetryFromContext = %v, want true", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithAccountSwitchCount(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test setting count
|
||||
ctx1 := WithAccountSwitchCount(ctx, 5, false)
|
||||
val, ok := AccountSwitchCountFromContext(ctx1)
|
||||
if !ok {
|
||||
t.Error("AccountSwitchCountFromContext returned !ok")
|
||||
}
|
||||
if val != 5 {
|
||||
t.Errorf("AccountSwitchCountFromContext = %d, want 5", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextDefaults(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// All context getters should return !ok for fresh context
|
||||
_, ok := IsMaxTokensOneHaikuRequestFromContext(ctx)
|
||||
if ok {
|
||||
t.Error("IsMaxTokensOneHaikuRequestFromContext returned ok for fresh context")
|
||||
}
|
||||
|
||||
_, ok = ThinkingEnabledFromContext(ctx)
|
||||
if ok {
|
||||
t.Error("ThinkingEnabledFromContext returned ok for fresh context")
|
||||
}
|
||||
|
||||
_, ok = PrefetchedStickyAccountIDFromContext(ctx)
|
||||
if ok {
|
||||
t.Error("PrefetchedStickyAccountIDFromContext returned ok for fresh context")
|
||||
}
|
||||
|
||||
_, ok = PrefetchedStickyGroupIDFromContext(ctx)
|
||||
if ok {
|
||||
t.Error("PrefetchedStickyGroupIDFromContext returned ok for fresh context")
|
||||
}
|
||||
|
||||
_, ok = SingleAccountRetryFromContext(ctx)
|
||||
if ok {
|
||||
t.Error("SingleAccountRetryFromContext returned ok for fresh context")
|
||||
}
|
||||
|
||||
_, ok = AccountSwitchCountFromContext(ctx)
|
||||
if ok {
|
||||
t.Error("AccountSwitchCountFromContext returned ok for fresh context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeOldKeys(t *testing.T) {
|
||||
// Test that bridgeOldKeys=true allows setting values
|
||||
// even when old keys might already exist
|
||||
ctx := context.Background()
|
||||
ctx1 := WithIsMaxTokensOneHaikuRequest(ctx, true, true) // bridgeOldKeys=true
|
||||
val, ok := IsMaxTokensOneHaikuRequestFromContext(ctx1)
|
||||
if !ok {
|
||||
t.Error("IsMaxTokensOneHaikuRequestFromContext returned !ok with bridgeOldKeys=true")
|
||||
}
|
||||
if val != true {
|
||||
t.Errorf("IsMaxTokensOneHaikuRequestFromContext = %v, want true", val)
|
||||
}
|
||||
}
|
||||
201
internal/service/webhook_service_test.go
Normal file
201
internal/service/webhook_service_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Webhook Security Functions Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestIsPrivateIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
expected bool
|
||||
}{
|
||||
// Private ranges - 10.0.0.0/8
|
||||
{"10.0.0.0", "10.0.0.0", true},
|
||||
{"10.255.255.255", "10.255.255.255", true},
|
||||
{"10.1.2.3", "10.1.2.3", true},
|
||||
|
||||
// Private ranges - 172.16.0.0/12
|
||||
{"172.16.0.0", "172.16.0.0", true},
|
||||
{"172.31.255.255", "172.31.255.255", true},
|
||||
{"172.20.1.1", "172.20.1.1", true},
|
||||
|
||||
// Private ranges - 192.168.0.0/16
|
||||
{"192.168.0.0", "192.168.0.0", true},
|
||||
{"192.168.255.255", "192.168.255.255", true},
|
||||
{"192.168.1.100", "192.168.1.100", true},
|
||||
|
||||
// Loopback
|
||||
{"127.0.0.1", "127.0.0.1", true},
|
||||
{"127.255.255.255", "127.255.255.255", true},
|
||||
{"::1", "::1", true},
|
||||
|
||||
// Public IPs
|
||||
{"8.8.8.8", "8.8.8.8", false},
|
||||
{"1.1.1.1", "1.1.1.1", false},
|
||||
{"93.184.216.34", "93.184.216.34", false},
|
||||
{"142.250.80.46", "142.250.80.46", false},
|
||||
|
||||
// Edge cases
|
||||
{"", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ip := net.ParseIP(tt.ip)
|
||||
if tt.ip == "" {
|
||||
// Empty IP should return false
|
||||
result := isPrivateIP(nil)
|
||||
if result != false {
|
||||
t.Errorf("isPrivateIP(nil) = %v, want %v", result, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ip == nil {
|
||||
t.Skipf("could not parse IP: %s", tt.ip)
|
||||
}
|
||||
result := isPrivateIP(ip)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isPrivateIP(%s) = %v, want %v", tt.ip, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSafeURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
// Valid public HTTPS URLs
|
||||
{"https example.com", "https://example.com/webhook", true},
|
||||
{"https with path", "https://example.com/api/v1/hook", true},
|
||||
{"https with query", "https://example.com/hook?a=1&b=2", true},
|
||||
{"https with port", "https://example.com:8443/hook", true},
|
||||
{"https subdomains", "https://sub.example.com/hook", true},
|
||||
|
||||
// HTTP (allowed but public only)
|
||||
{"http public", "http://example.com/hook", true},
|
||||
{"http with port", "http://example.com:8080/hook", true},
|
||||
|
||||
// Invalid schemes
|
||||
{"ftp scheme", "ftp://example.com/hook", false},
|
||||
{"file scheme", "file:///etc/passwd", false},
|
||||
{"data scheme", "data:text/html,<script>alert(1)</script>", false},
|
||||
{"javascript scheme", "javascript:alert(1)", false},
|
||||
|
||||
// Localhost blocked
|
||||
{"localhost http", "http://localhost/hook", false},
|
||||
{"localhost https", "https://localhost/hook", false},
|
||||
{"127.0.0.1", "http://127.0.0.1/hook", false},
|
||||
{"::1", "http://[::1]/hook", false},
|
||||
|
||||
// Private IPs blocked
|
||||
{"10.x.x.x", "http://10.0.0.1/hook", false},
|
||||
{"172.16.x.x", "http://172.16.0.1/hook", false},
|
||||
{"192.168.x.x", "http://192.168.1.1/hook", false},
|
||||
|
||||
// Internal domains blocked
|
||||
{"internal domain", "https://server.internal/hook", false},
|
||||
{"local domain", "https://host.local/hook", false},
|
||||
{"corp domain", "https://host.corp/hook", false},
|
||||
{"lan domain", "https://host.lan/hook", false},
|
||||
{"intranet domain", "https://host.intranet/hook", false},
|
||||
|
||||
// Cloud metadata IPs blocked
|
||||
{"gcp metadata", "http://metadata.google.internal/", false},
|
||||
{"aws metadata", "http://169.254.169.254/latest/meta-data/", false},
|
||||
{"azure metadata", "http://metadata.azure.internal/", false},
|
||||
{"aliyun metadata", "http://100.100.100.200/latest/meta-data/", false},
|
||||
|
||||
// Invalid URLs
|
||||
{"empty", "", false},
|
||||
{"no scheme", "example.com/hook", false},
|
||||
{"relative", "/hook", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isSafeURL(tt.url)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isSafeURL(%q) = %v, want %v", tt.url, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeHMAC(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
secret string
|
||||
}{
|
||||
{
|
||||
name: "simple payload",
|
||||
payload: []byte(`{"event":"user.created"}`),
|
||||
secret: "test-secret",
|
||||
},
|
||||
{
|
||||
name: "empty payload",
|
||||
payload: []byte{},
|
||||
secret: "test-secret",
|
||||
},
|
||||
{
|
||||
name: "empty secret",
|
||||
payload: []byte(`{"event":"user.deleted"}`),
|
||||
secret: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result1 := computeHMAC(tt.payload, tt.secret)
|
||||
result2 := computeHMAC(tt.payload, tt.secret)
|
||||
|
||||
// Same input should produce same output
|
||||
if result1 != result2 {
|
||||
t.Errorf("computeHMAC not deterministic: got %s and %s", result1, result2)
|
||||
}
|
||||
|
||||
// Result should not be empty for non-empty payload
|
||||
if len(tt.payload) > 0 && result1 == "" {
|
||||
t.Error("computeHMAC returned empty string for non-empty payload")
|
||||
}
|
||||
|
||||
// Result should be hex-encoded (64 chars for SHA256)
|
||||
if len(result1) != 64 {
|
||||
t.Errorf("computeHMAC returned %d chars, want 64 (SHA256 hex)", len(result1))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeHMAC_DifferentInputs(t *testing.T) {
|
||||
payload1 := []byte(`{"event":"user.created"}`)
|
||||
payload2 := []byte(`{"event":"user.deleted"}`)
|
||||
secret := "test-secret"
|
||||
|
||||
result1 := computeHMAC(payload1, secret)
|
||||
result2 := computeHMAC(payload2, secret)
|
||||
|
||||
if result1 == result2 {
|
||||
t.Error("Different payloads should produce different HMACs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeHMAC_DifferentSecrets(t *testing.T) {
|
||||
payload := []byte(`{"event":"user.created"}`)
|
||||
|
||||
result1 := computeHMAC(payload, "secret1")
|
||||
result2 := computeHMAC(payload, "secret2")
|
||||
|
||||
if result1 == result2 {
|
||||
t.Error("Different secrets should produce different HMACs")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user