209021e7d2
- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源 - 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调) - Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录 - 门禁:mkdocs build --strict 通过(零警告)
207 lines
20 KiB
Markdown
207 lines
20 KiB
Markdown
# 06 证据驱动质量审计(开工前基线)
|
||
|
||
- 审计人:EvidenceQA(Evidence Collector)
|
||
- 日期:2026-09-03
|
||
- 方式:只读审计(ls / grep / 读文件 / git 只读命令)。未运行构建、未启动服务,所有涉及运行时行为的结论均标注「静态分析,需运行验证」。
|
||
- 验收依据:`/home/lx/workspace/patbond/patbond-doc/docs/development/development-plan.md` 第 9 节(测试与质量门禁)、第 10 节(Definition of Done)、第 8 节(第一迭代任务)、M1 验收标准(第 212-223 行)。
|
||
|
||
---
|
||
|
||
## 1. 测试资产审计
|
||
|
||
### 1.1 patbond-api:零测试
|
||
|
||
- 全仓库文件清单中**不存在任何 `src/test` 目录**(`find patbond-api -type f` 结果仅含 `src/main`,见仓库文件列表)。
|
||
- 三个模块的 pom 中**均无 `spring-boot-starter-test` 依赖**(`grep -rn "spring-boot-starter-test" patbond-api --include="pom.xml"` 无任何匹配)——即写测试的前置依赖都还没有。
|
||
- 与文档第 9 节要求的差距(`development-plan.md:294-300`):单元测试、集成测试(Testcontainers)、契约测试、安全测试全部缺失,缺口为 100%。
|
||
- 后果:CI 最低门禁 `mvn clean test`(`development-plan.md:313-314`)在当前仓库上是**空转通过**——0 个测试也会绿。这正是文档第 11 节风险 6(`development-plan.md:354`)自认的状态,审计确认属实。
|
||
|
||
### 1.2 patbond-flutter:仅 1 个冒烟测试
|
||
|
||
- `test/` 目录只有 1 个文件:`/home/lx/workspace/patbond/patbond-flutter/test/widget_test.dart`,共 21 行。
|
||
- 内容:单个 `testWidgets`,断言主导航 5 个 Tab 文案,以及两条**写死的演示数据字符串**:
|
||
- `test/widget_test.dart:18` → `expect(find.text('北京 · 朝阳区'), findsOneWidget)`
|
||
- `test/widget_test.dart:19` → `expect(find.text('28°C 晴'), findsOneWidget)`
|
||
- 这两个值来自 `lib/data/demo_data.dart:11-19`(`locationWeatherOptions` 第一项:city '北京'、district '朝阳区'、temperature 28、conditionText '晴')。演示数据一改,唯一的测试即失败。
|
||
- 与文档第 9 节 Flutter 要求的差距(`development-plan.md:302-308`):
|
||
- 单元测试(DTO 映射、Repository、缓存、状态转换):0 个。`lib/state/app_state.dart` 有可测的加载/持久化/回退逻辑(约 120 行),无对应测试。
|
||
- Widget 测试(登录、宠物编辑、动态互动等):0 个(登录页本身不存在)。
|
||
- Golden/响应式测试:0 个。
|
||
- Integration/E2E:0 个(`integration_test/` 目录不存在)。
|
||
- loading/empty/error/retry/离线状态覆盖:0 个。
|
||
- 工具链本身可用:`analysis_options.yaml` 引入 `package:flutter_lints/flutter.yaml`(`analysis_options.yaml:10`),`pubspec.yaml:39-48` 有 `flutter_test` 与 `flutter_lints ^6.0.0`。
|
||
|
||
### 1.3 门禁落地情况
|
||
|
||
- **两个代码仓库均无任何 CI 配置**:`ls -a` 未见 `.github`、`.gitlab-ci.yml`、Jenkinsfile 等(patbond-api 根目录仅 `.git`、`.gitignore`、`.idea`、三个模块与 `pom.xml`、`Readme.md`)。
|
||
- **无 Maven Wrapper**(`mvnw` 不存在),与 M0 目标(`development-plan.md:118`、204 行前后)尚有差距——这是计划内待办,但意味着「CI 最低门禁」目前只存在于文档里,没有任何机制执行它。
|
||
|
||
---
|
||
|
||
## 2. 代码质量风险审计(逐条附证据)
|
||
|
||
### 2.1 认证与鉴权(后端)
|
||
|
||
- **token 不可验证**:`patbond-api/patbond-auth/src/main/java/com/patbond/patbond/auth/service/AuthService.java:47-56` —— `buildToken` 返回 `UUID.randomUUID().toString().replace("-", "")`,`expiresAt` 只是响应里的展示字段。无签名、无存储、无校验途径、无 refresh token。与 M1 验收标准(`development-plan.md:223`)和风险 2(`development-plan.md:350`)一致,审计确认。
|
||
- **`/internal/**` 完全无访问控制**:`patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/controller/UserController.java:18` 映射 `/internal/users`,全仓库 grep 无任何 Spring Security、拦截器或过滤器。`GET /internal/users/{id}`(UserController.java:37-40)返回的 `UserProfile` 含 `phone` 字段(`UserService.java:69-71` 的 `toProfile` 传入 `user.getPhone()`)——任何能访问 8082 端口的人可枚举用户资料含手机号。对应风险 3(`development-plan.md:351`)。
|
||
- **用户仅存内存**:`patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/service/UserService.java:21-23` —— `AtomicLong idGenerator` + 两个 `ConcurrentHashMap`。重启即丢全部用户,直接不满足 M1 验收「注册后重启服务数据不丢失」(`development-plan.md:223`)。
|
||
- **错误状态码折叠**(静态分析,需运行验证):`AuthService.java:58-63` 的 `requireData` 把所有失败折叠为 `HttpStatus.BAD_REQUEST`。而 user 服务用 `ResponseStatusException` 返回 409/401/404(`UserService.java:29,48,56,64`),Feign 客户端(`patbond-auth/.../client/UserClient.java`)收到非 2xx 时会抛 `FeignException` 而非进入 `requireData` 的业务分支——登录密码错误的调用链大概率对客户端表现为 500 或语义丢失的 400,违反契约规范「错误必须同时返回正确 HTTP 状态码和稳定业务错误码」(`development-plan.md:171`)。
|
||
|
||
### 2.2 硬编码密钥 / 密码 / 敏感信息
|
||
|
||
- **后端源码中未发现硬编码密钥、密码或密文**。`grep -rniE "password|secret|api[_-]?key|token"` 在 `patbond-api` 三个模块 `src` 下的命中全部是 DTO 字段赋值(如 `LoginRequest.java:26`、`CreateUserRequest.java:27,45`),无真实凭据。诚实结论:这一项没有问题,不编造。
|
||
- **配置卫生良好**:仓库只提交 `.yml.sample`(`patbond-auth/src/main/resources/application.yml.sample`、`patbond-user/.../application.yml.sample`),真实 `application.yml` 被 `.gitignore` 忽略(`patbond-api/.gitignore` 末段:`patbond-*/src/main/resources/application.yml` + `!...yml.sample`)。sample 中仅有 `${NACOS_SERVER_ADDR:127.0.0.1:8848}` 之类的本地默认值,无敏感值。
|
||
- **开发 fixture 凭据集中在 SQL**:`patbond-doc/docs/database/patbond_postgresql.sql`:
|
||
- 第 1269 行:注释明文声明 `accounts use the password: Patbond@123`;
|
||
- 第 1289-1293 行:三个 fixture 账号共用同一 bcrypt 哈希 `$2y$12$UYCn5Zm...`;
|
||
- 第 1283-1286 行:fixture 手机号 `+8613800000001` 等(明显假号段,风险低);
|
||
- 约第 1296-1308 行:`identity.auth_sessions` 插入固定 `refresh_token_hash = decode(repeat('ab', 32), 'hex')` 和 `access_token_jti = 'fixture-access-token-jti'`。
|
||
- 文件自身已注明「Remove this section from the production Flyway baseline」(第 1266-1269 行),且这是开发种子数据而非泄露的生产密钥。风险在于:bootstrap 是**单文件**,结构与种子数据未拆分(第 4.3 节第 105 行的拆分要求未完成),误导入生产即产生三个已知密码账号和一个可预测 refresh token。
|
||
|
||
### 2.3 TODO / FIXME / 注释掉的代码
|
||
|
||
- `grep -rn "TODO|FIXME|HACK|XXX"` 在 `patbond-api`、`patbond-flutter/lib`、`patbond-flutter/test` 的 java/dart/yml 文件中:**0 命中**。
|
||
- 注释掉的代码块:Java 侧 `grep -rnE "^\s*//.*(;|\{)\s*$"` 0 命中;Dart 侧同类模式 0 命中(仅 `pubspec.yaml`、`analysis_options.yaml` 中的脚手架模板注释,属正常)。
|
||
- 诚实结论:这两类问题当前不存在,不虚报。
|
||
|
||
### 2.4 硬编码 URL 与展示型字段(Flutter)
|
||
|
||
- 22 处硬编码 Unsplash 图片 URL,集中在:`lib/data/demo_data.dart`(4、6、8、128、136、146、151、159、178、191、203、216、229、245、252、259 行)、`lib/features/home/home_page.dart:551,556`、`lib/features/pets/pets_page.dart:357-358`。属演示数据(文档第 33 行认可其非契约地位),但 DoD 要求「不依赖 Demo 常量」(`development-plan.md:340`),验收时必须逐页替换。
|
||
- 展示字符串被当数据持久化:`lib/data/demo_data.dart:117`(`time: '2小时前'`)、130、138、147、161 行;`distance: '1.2km'` 等在 172、185、197、210、223 行。违反第 4.3 节「相对时间、距离由事实字段计算,不持久化展示字符串」(`development-plan.md:111`),对应风险 4(`development-plan.md:352`)。
|
||
- 无网络层、无登录页、无安全存储:`lib/` 下无 `http`/`dio` 依赖(`pubspec.yaml:30-37` 仅 `cupertino_icons` + `shared_preferences`),`AppState` 直接持久化宠物/疫苗/帖子/天气到 `SharedPreferences`(`lib/state/app_state.dart:9-12,97-118`)。第一迭代需按 4.2 节新建全部网络与鉴权层;届时 token 必须走安全存储而非 `SharedPreferences`(`development-plan.md:97`)。
|
||
|
||
---
|
||
|
||
## 3. 文档一致性审计
|
||
|
||
| 检查项 | 结论 | 证据 |
|
||
| --- | --- | --- |
|
||
| API Readme 端点 vs 代码 | 一致 | `Readme.md:34-44` 的 6 个端点与 `AuthController.java:24-31`、`UserController.java:27-45` 逐一对应 |
|
||
| API Readme 端口 vs 配置 | 一致 | `Readme.md:33,40`(8081/8082)= 两个 `application.yml.sample:2` |
|
||
| API Readme 的 NACOS_SERVER_ADDR 说明 | 一致 | `Readme.md:48-53` vs sample 第 12 行 `${NACOS_SERVER_ADDR:127.0.0.1:8848}` |
|
||
| **API Readme 启动步骤完整性** | **不一致** | `Readme.md:22-26` 只有 `mvn compile`,**未提及**必须先复制 `application.yml.sample` 为 `application.yml`(该步骤只在 `development-plan.md:127-138`),也无 `spring-boot:run` 命令。干净检出仅按 README 无法把服务跑起来——文档风险 5(`development-plan.md:353`)确认属实 |
|
||
| Readme 技术栈 RabbitMQ vs 计划 | 内部一致、与计划冲突 | `Readme.md:19` 列 RabbitMQ,`patbond-common/pom.xml:120-123` 确实引入 `spring-boot-starter-amqp`;但计划 4.1 节明确 common「不应让所有服务被动引入 RabbitMQ、Feign 等依赖」(`development-plan.md:79`) |
|
||
| Flutter README 平台声明 | 一致 | `README.md:5` 声明的 Android/iOS/Web/桌面对应仓库 `android/ ios/ web/ linux/ macos/ windows/` 目录均存在 |
|
||
| Flutter README shared_preferences 声明 | 一致 | `README.md:13` vs `lib/state/app_state.dart:9-12` 四个持久化 key |
|
||
| **Flutter README 校验命令 vs 计划 CI 门禁** | **不一致** | `README.md:34` 为 `dart format --set-exit-if-changed lib test`(缺 `--output=none`,实际会**改写文件**);计划门禁是 `dart format --output=none --set-exit-if-changed lib test`(`development-plan.md:317`) |
|
||
| 计划 5.2 节命令中的文件路径 | 一致 | `development-plan.md:130-133` 引用的两个 `.yml.sample` 均存在于对应路径 |
|
||
| mkdocs.yml 导航 | 一致 | `patbond-doc/mkdocs.yml` nav 引用的 `index.md`、`development/development-plan.md` 均存在;`docs/index.md:14` 链接的 `database/patbond_postgresql.sql` 存在 |
|
||
| 其他观察 | — | `patbond-flutter` 工作区有未提交改动(`git status`:` M README.md`);`patbond-api/.idea/` 在磁盘上但未被 git 跟踪(`git ls-files` 无 .idea 条目),无泄露 |
|
||
|
||
`mkdocs build --strict` 门禁未实际执行(审计约束禁止构建),仅完成静态引用核对。
|
||
|
||
---
|
||
|
||
## 4. 问题清单(按严重程度排序)
|
||
|
||
### Blocker
|
||
|
||
**B1 后端自动化测试为零,CI 门禁空转**
|
||
证据:`patbond-api` 无任何 `src/test` 目录;三个 pom 均无 `spring-boot-starter-test`。
|
||
影响:第 9 节全部后端测试类别缺口 100%;`mvn clean test` 0 测试也通过,门禁无意义。第一迭代任务 5「注册、登录、刷新、退出和鉴权集成测试」(`development-plan.md:285`)从零开始。
|
||
|
||
**B2 认证纵切三件套全部缺位:内存用户 + 不可验证 token + `/internal` 裸奔**
|
||
证据:`UserService.java:21-23`(ConcurrentHashMap 存储)、`AuthService.java:47-56`(随机 UUID 当 token)、`UserController.java:18`(`/internal/users` 无鉴权且经 `UserService.java:69-71` 返回手机号)。
|
||
影响:M1 全部四条验收标准(`development-plan.md:223`)当前均不满足;未鉴权的 `/internal` 是当前唯一对外可见的真实安全暴露面。
|
||
|
||
### Major
|
||
|
||
**M1 错误状态码在 auth→user 调用链上丢失(静态分析,需运行验证)**
|
||
证据:`AuthService.java:58-63` 将一切失败折叠为 400;user 侧用 `ResponseStatusException` 抛 409/401/404(`UserService.java:29,48,56,64`),Feign 非 2xx 会抛异常绕过该分支。
|
||
影响:违反契约规范 `development-plan.md:171`;客户端无法区分「用户名已存在」「密码错误」。第一迭代任务 3 的统一异常响应必须覆盖此链路,验收时需用真实 curl 记录证明。
|
||
|
||
**M2 patbond-common 强制全体服务引入 AMQP/Feign/LoadBalancer/Nacos**
|
||
证据:`patbond-common/pom.xml:120-139`(`spring-boot-starter-amqp`、`spring-cloud-starter-openfeign`、`spring-cloud-starter-loadbalancer`、两个 nacos starter 全部为 compile 依赖)。
|
||
影响:与架构原则 `development-plan.md:79` 直接冲突;后续每个新业务模块都会被动携带消息队列与服务发现依赖。
|
||
|
||
**M3 两个代码仓库均无 CI 配置和 Maven Wrapper**
|
||
证据:`ls -a` 无 `.github`/`.gitlab-ci.yml`/`mvnw`。
|
||
影响:第 9 节 CI 最低门禁(`development-plan.md:310-323`)没有执行载体,DoD 中「代码通过 CI」(`development-plan.md:345`)无法核查。
|
||
|
||
**M4 API Readme 启动步骤不完整,干净检出无法照做启动**
|
||
证据:`patbond-api/Readme.md:22-26` 仅 `mvn compile`;配置复制步骤只存在于 `development-plan.md:127-138`;`.gitignore` 忽略 `application.yml` 且仓库只有 `.sample`。
|
||
影响:违反 M0 验收「新机器仅依据仓库文档即可启动」(`development-plan.md:210`)。
|
||
|
||
**M5 数据库 bootstrap 单文件混装结构与开发凭据**
|
||
证据:`patbond_postgresql.sql:1266-1269`(明文声明 fixture 密码 `Patbond@123`)、1289-1293(三账号同 bcrypt 哈希)、约 1296-1308(固定 `refresh_token_hash` 与 JTI 的预置会话)。
|
||
影响:结构/种子未拆分(`development-plan.md:105` 要求拆开),一旦整文件被当生产 baseline 导入,即产生已知密码账号与可预测会话。风险 7(`development-plan.md:355`)确认属实。
|
||
|
||
### Minor
|
||
|
||
**m1 Flutter 唯一测试断言写死演示数据**
|
||
证据:`test/widget_test.dart:18-19` 断言 `'北京 · 朝阳区'`、`'28°C 晴'`,值来自 `lib/data/demo_data.dart:11-19`。
|
||
影响:演示数据或默认城市一改,唯一的测试即挂;该测试对回归防护价值趋近于零。
|
||
|
||
**m2 Flutter README 校验命令与 CI 门禁不一致**
|
||
证据:`patbond-flutter/README.md:34` 缺 `--output=none`,会改写文件;门禁版本在 `development-plan.md:317`。
|
||
影响:开发者本机「校验」实际是格式化,CI(若建立)与本机行为不一致。
|
||
|
||
**m3 展示字符串与硬编码图片 URL 持久化在演示数据中**
|
||
证据:`lib/data/demo_data.dart:117,130,138,147,161`('2小时前' 等)、172,185,197,210,223('1.2km' 等)、22 处 Unsplash URL(见 2.4 节行号清单)。
|
||
影响:与 `development-plan.md:111` 及 DoD「不依赖 Demo 常量」冲突;属已知风险 4,逐页替换时必须清除,验收时应 grep 证明。
|
||
|
||
**m4 patbond-flutter 工作区有未提交改动**
|
||
证据:`git status --short` → ` M README.md`。
|
||
影响:基线不干净,审计快照与远端不一致;开工前应提交或还原。
|
||
|
||
---
|
||
|
||
## 5. 第一迭代「登录纵切」验收证据清单
|
||
|
||
依据:第 8 节任务 1-8、M1 验收标准(`development-plan.md:223`)、第 9 节门禁、第 10 节 DoD。**没有下列证据即不通过验收,任何「已完成」的口头声明不作数。**
|
||
|
||
### 5.1 自动化测试输出(原始终端输出,不接受转述)
|
||
|
||
1. `mvn clean test` 完整输出:显示测试总数 > 0,且包含注册/登录/刷新/退出/鉴权的集成测试类名与用例数;Testcontainers 启动 PostgreSQL 16 的日志行可见。
|
||
2. 每个接口至少覆盖:成功、参数错误、资源不存在、无权限、并发冲突、幂等重试(`development-plan.md:300`)——以测试报告中的用例名逐条对应。
|
||
3. `dart format --output=none --set-exit-if-changed lib test`、`flutter analyze`、`flutter test` 三条命令的退出码为 0 的完整输出;`flutter test` 中包含登录页 widget 测试(loading/error/成功三态)。
|
||
4. `mkdocs build --strict` 成功输出(文档更新后)。
|
||
5. CI 运行链接或日志:以上门禁在 CI 中执行并全绿(M3 问题修复的证明)。
|
||
|
||
### 5.2 接口调用记录(curl/httpie 全文:请求 + 响应头 + 响应体)
|
||
|
||
按顺序一份完整 transcript:
|
||
1. `POST /api/v1/auth/register` → 201/200,响应体含 UUID 格式 userId 与 `{code, message, data}` 包裹。
|
||
2. 重复用户名注册 → HTTP 409 + 稳定业务错误码(验证 M1 问题修复:不再折叠为 400/500)。
|
||
3. 错误密码登录 → HTTP 401 + 业务错误码。
|
||
4. `POST /api/v1/auth/login` 成功 → 含 access + refresh token。
|
||
5. `GET /api/v1/me` 带 token → 200;不带/伪造 token → 401(证明 token 可验证)。
|
||
6. `POST /api/v1/auth/refresh` → 新 token 对;随后用**旧 refresh token 重放** → 401(轮换生效)。
|
||
7. `POST /api/v1/auth/logout` → 成功;再用已撤销 refresh token → 401(M1 验收「退出后 refresh token 不可再次使用」)。
|
||
8. 未携带服务间凭据直接调用 `/internal/users/{id}` → 被拒绝(401/403),对照当前裸奔状态(B2)。
|
||
|
||
### 5.3 数据库查询结果(psql 原始输出)
|
||
|
||
1. 注册后:`SELECT id, username, status FROM identity.users WHERE username='...'` 显示 UUID 主键行。
|
||
2. `SELECT hash_algorithm, left(password_hash, 7) FROM identity.user_credentials ...` 显示 bcrypt/argon2id 前缀——同时证明**非明文**。
|
||
3. 重启持久化证据:注册 → 服务重启(附带重启时间戳的服务日志)→ 登录成功 + 上述查询仍有该行(M1 验收「重启数据不丢失」,直接针对 B2 内存存储)。
|
||
4. refresh 轮换后:`SELECT ... FROM identity.auth_sessions` 显示旧会话 revoked/新会话 active。
|
||
5. `SELECT version, description, success FROM flyway_schema_history` 显示 identity/media baseline 迁移(任务 1),且在全新 PostgreSQL 16 实例执行过一次(`development-plan.md:325`)。
|
||
6. 生产 baseline 不含 fixture:对迁移产物 `grep -c "Patbond@123"` 为 0(针对 M5)。
|
||
|
||
### 5.4 界面截图(真机或模拟器,标注设备与时间)
|
||
|
||
1. 登录页:初始态、提交中 loading 态、错误态(错误密码后的可读提示)、成功跳转后首页。
|
||
2. 注册页同三态。
|
||
3. 登录态恢复:登录 → 完全杀掉 App → 重新打开直接进入已登录态(M1 验收),两张前后截图 + 中间的杀进程操作说明。
|
||
4. 退出登录后回到未登录态的截图。
|
||
5. 安全存储证据:代码评审指向 token 写入 secure storage 的调用点(文件+行号),并 `grep -rn "SharedPreferences" lib` 输出证明 token 未落入 `SharedPreferences`(`development-plan.md:97`)。
|
||
|
||
### 5.5 附加核查项(DoD)
|
||
|
||
1. 日志片段 + `grep -inE "password|token" <日志文件>` 输出:证明日志不含密码与 token 全文(`development-plan.md:175,344`)。
|
||
2. OpenAPI 文件路径 + 契约测试输出(任务 6)。
|
||
3. 端到端用例(注册 → 登录 → 获取当前用户 → 退出,任务 8)的单次完整执行记录,与 5.2 的 transcript 可为同一份。
|
||
|
||
### 验收纪律
|
||
|
||
- 每条证据必须可复现:附命令、路径、时间。截图必须来自本次交付的构建,不接受历史截图。
|
||
- 声明「零问题」「production ready」而不附上述证据的交付,直接按 FAILED 处理并退回。
|
||
- 本报告第 4 节的 B1、B2 未关闭前,第一迭代不具备进入验收的资格。
|
||
|
||
---
|
||
|
||
## 附:本次审计执行的命令类别
|
||
|
||
`find`(文件清单)、`ls -a`(CI/wrapper 探测)、`grep -rn`(TODO/密钥/URL/展示字符串/测试依赖)、`cat`/`sed`(读源码与 SQL)、`wc -l`(体量)、`git status --short` / `git ls-files` / `git log --oneline`(只读仓库状态)。未修改任何被审计仓库的文件。
|