@deepstorm/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +72 -0
  2. package/dist/agents/reef-inspect-figma.md +77 -0
  3. package/dist/agents/reef-review-backend.md.tmpl +112 -0
  4. package/dist/agents/reef-review-frontend.md.tmpl +78 -0
  5. package/dist/agents/reef-review-infra.md +47 -0
  6. package/dist/agents/reef-review-security.md.tmpl +80 -0
  7. package/dist/agents/reef-scope-analysis.md +64 -0
  8. package/dist/build-registry.js +375 -0
  9. package/dist/cli.js +8581 -0
  10. package/dist/config-schema.json +133 -0
  11. package/dist/env-examples/context7.env-example +19 -0
  12. package/dist/env-examples/feishu-wiki.env-example +16 -0
  13. package/dist/env-examples/figma.env-example +16 -0
  14. package/dist/env-examples/github.env-example +20 -0
  15. package/dist/env-examples/jira.env-example +20 -0
  16. package/dist/hooks/mcp-hook.sh +77 -0
  17. package/dist/hooks/reef-auto-format.sh.tmpl +72 -0
  18. package/dist/hooks/reef-block-dangerous.sh +70 -0
  19. package/dist/hooks/reef-hooks.json +72 -0
  20. package/dist/hooks/reef-intent-detect.sh +129 -0
  21. package/dist/hooks/reef-protect-files.sh +55 -0
  22. package/dist/hooks/reef-run-tests.sh +84 -0
  23. package/dist/hooks/reef-scope-check.sh +386 -0
  24. package/dist/hooks/reef-scope-ci.sh +28 -0
  25. package/dist/hooks/reef-scope-gate.sh +115 -0
  26. package/dist/hooks/reef-scope-pre-commit.sh.tmpl +28 -0
  27. package/dist/hooks/reef-scope-setup.sh +204 -0
  28. package/dist/hooks/reef-scope-split.sh +203 -0
  29. package/dist/hooks/sweep-hooks.json +14 -0
  30. package/dist/hooks/sweep-mcp-hook.sh +77 -0
  31. package/dist/hooks/tide-hooks.json +14 -0
  32. package/dist/hooks/tide-session-preload.sh +17 -0
  33. package/dist/mcp/code-hosting/github.json +20 -0
  34. package/dist/mcp/design-tools/figma.json +19 -0
  35. package/dist/mcp/docs-reference/context7.json +28 -0
  36. package/dist/mcp/e2e-testing/playwright.json +13 -0
  37. package/dist/mcp/knowledge-base/feishu-wiki.json +19 -0
  38. package/dist/mcp/project-management/jira.json +27 -0
  39. package/dist/mcp-skills/deepflow-mcp-feishu-wiki-read/SKILL.md +65 -0
  40. package/dist/mcp-skills/deepflow-mcp-feishu-wiki-write/SKILL.md +63 -0
  41. package/dist/mcp-skills/deepflow-mcp-figma-read/SKILL.md +98 -0
  42. package/dist/mcp-skills/deepflow-mcp-github-read/SKILL.md +62 -0
  43. package/dist/mcp-skills/deepflow-mcp-github-write/SKILL.md +63 -0
  44. package/dist/mcp-skills/deepflow-mcp-jira-read/SKILL.md +80 -0
  45. package/dist/mcp-skills/deepflow-mcp-jira-write/SKILL.md +74 -0
  46. package/dist/mcp-skills/deepflow-mcp-playwright-read/SKILL.md +79 -0
  47. package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-read/SKILL.md +65 -0
  48. package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-write/SKILL.md +63 -0
  49. package/dist/mcp-skills/deepstorm-mcp-figma-read/SKILL.md +98 -0
  50. package/dist/mcp-skills/deepstorm-mcp-github-read/SKILL.md +62 -0
  51. package/dist/mcp-skills/deepstorm-mcp-github-write/SKILL.md +63 -0
  52. package/dist/mcp-skills/deepstorm-mcp-jira-read/SKILL.md +80 -0
  53. package/dist/mcp-skills/deepstorm-mcp-jira-write/SKILL.md +74 -0
  54. package/dist/mcp-skills/deepstorm-mcp-playwright-read/SKILL.md +79 -0
  55. package/dist/registry.json +818 -0
  56. package/dist/skills/atoll-ops/SKILL.md +46 -0
  57. package/dist/skills/reef-commit/SKILL.md +127 -0
  58. package/dist/skills/reef-gen-backend/SKILL.md.tmpl +87 -0
  59. package/dist/skills/reef-gen-backend/variants/java/steps.md +28 -0
  60. package/dist/skills/reef-gen-backend/variants/python/steps.md +70 -0
  61. package/dist/skills/reef-gen-frontend/SKILL.md.tmpl +83 -0
  62. package/dist/skills/reef-gen-frontend/variants/angular/steps.md +30 -0
  63. package/dist/skills/reef-harden/EXAMPLES.md +89 -0
  64. package/dist/skills/reef-harden/SKILL.md +136 -0
  65. package/dist/skills/reef-pr/SKILL.md +97 -0
  66. package/dist/skills/reef-review/SKILL.md.tmpl +107 -0
  67. package/dist/skills/reef-scope/SKILL.md +134 -0
  68. package/dist/skills/reef-start/SKILL.md.tmpl +562 -0
  69. package/dist/skills/reef-start/references/jira-start-subagent.md +60 -0
  70. package/dist/skills/reef-style-backend/SKILL.md.tmpl +134 -0
  71. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/chat-client.md +96 -0
  72. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/rag.md +94 -0
  73. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/structured-output.md +62 -0
  74. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/tool-calling.md +68 -0
  75. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/quick-reference.md +220 -0
  76. package/dist/skills/reef-style-backend/fragments/java/api-spec/quick-reference.md +148 -0
  77. package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/examples/database-migration.md +131 -0
  78. package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/quick-reference.md +103 -0
  79. package/dist/skills/reef-style-backend/fragments/java/dependency-management/quick-reference.md +119 -0
  80. package/dist/skills/reef-style-backend/fragments/java/exception-handling/examples/error-code-enum.md +101 -0
  81. package/dist/skills/reef-style-backend/fragments/java/exception-handling/quick-reference.md +181 -0
  82. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/controller.md +95 -0
  83. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/dto-mapper.md +121 -0
  84. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/infrastructure.md +179 -0
  85. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/service-entity.md +202 -0
  86. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/testing.md +107 -0
  87. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/quick-reference.md +83 -0
  88. package/dist/skills/reef-style-backend/fragments/java/orm/hibernate/quick-reference.md +150 -0
  89. package/dist/skills/reef-style-backend/fragments/java/security-redlines/quick-reference.md +197 -0
  90. package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/examples/user-repository-test.md +69 -0
  91. package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/quick-reference.md +101 -0
  92. package/dist/skills/reef-style-backend/fragments/java/test/junit5/examples/user-service-test.md +61 -0
  93. package/dist/skills/reef-style-backend/fragments/java/test/junit5/quick-reference.md +100 -0
  94. package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/examples/user-controller-test.md +61 -0
  95. package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/quick-reference.md +85 -0
  96. package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/examples/user-service-integration-test.md +56 -0
  97. package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/quick-reference.md +83 -0
  98. package/dist/skills/reef-style-backend/fragments/python/alembic-migration/quick-reference.md +77 -0
  99. package/dist/skills/reef-style-backend/fragments/python/api-spec/quick-reference.md +164 -0
  100. package/dist/skills/reef-style-backend/fragments/python/dependency-management/quick-reference.md +139 -0
  101. package/dist/skills/reef-style-backend/fragments/python/exception-handling/quick-reference.md +177 -0
  102. package/dist/skills/reef-style-backend/fragments/python/fastapi-quick-reference/quick-reference.md +101 -0
  103. package/dist/skills/reef-style-backend/fragments/python/langchain/quick-reference.md +135 -0
  104. package/dist/skills/reef-style-backend/fragments/python/pytest-testing/quick-reference.md +111 -0
  105. package/dist/skills/reef-style-backend/fragments/python/ruff-mypy-toolchain/quick-reference.md +83 -0
  106. package/dist/skills/reef-style-backend/fragments/python/security-redlines/quick-reference.md +207 -0
  107. package/dist/skills/reef-style-backend/fragments/python/sqlalchemy-orm/quick-reference.md +91 -0
  108. package/dist/skills/reef-style-backend/variants/java/examples/code-wrapping.md +227 -0
  109. package/dist/skills/reef-style-backend/variants/java/examples/contributor-pattern.md +97 -0
  110. package/dist/skills/reef-style-backend/variants/java/quick-reference.md +117 -0
  111. package/dist/skills/reef-style-backend/variants/python/examples/crud-router.md +71 -0
  112. package/dist/skills/reef-style-backend/variants/python/examples/pydantic-schema.md +45 -0
  113. package/dist/skills/reef-style-backend/variants/python/examples/pytest-fixture.md +60 -0
  114. package/dist/skills/reef-style-backend/variants/python/examples/sqlalchemy-model.md +31 -0
  115. package/dist/skills/reef-style-backend/variants/python/quick-reference.md +111 -0
  116. package/dist/skills/reef-style-frontend/SKILL.md.tmpl +70 -0
  117. package/dist/skills/reef-style-frontend/fragments/css/tailwind/quick-reference.md +79 -0
  118. package/dist/skills/reef-style-frontend/fragments/test/vitest/examples/testing.md +150 -0
  119. package/dist/skills/reef-style-frontend/fragments/test/vitest/quick-reference.md +141 -0
  120. package/dist/skills/reef-style-frontend/fragments/ts-config/strict/quick-reference.md +112 -0
  121. package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/examples/ui-components.md +134 -0
  122. package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/quick-reference.md +66 -0
  123. package/dist/skills/reef-style-frontend/variants/angular/examples/code-wrapping.md +252 -0
  124. package/dist/skills/reef-style-frontend/variants/angular/examples/component-types-pipes.md +56 -0
  125. package/dist/skills/reef-style-frontend/variants/angular/examples/entity-types.md +100 -0
  126. package/dist/skills/reef-style-frontend/variants/angular/examples/forms-layer.md +119 -0
  127. package/dist/skills/reef-style-frontend/variants/angular/examples/service-routing.md +95 -0
  128. package/dist/skills/reef-style-frontend/variants/angular/quick-reference.md +100 -0
  129. package/dist/skills/reef-testcase/SKILL.md +138 -0
  130. package/dist/skills/reef-testcase/references/coverage-dimensions.md +99 -0
  131. package/dist/skills/reef-testcase/references/test-case-template.md +63 -0
  132. package/dist/skills/sweep-init/SKILL.md +286 -0
  133. package/dist/skills/sweep-init/scripts/flow-selector.mjs +305 -0
  134. package/dist/skills/sweep-plan/SKILL.md.tmpl +312 -0
  135. package/dist/skills/sweep-plan/references/test-flow-template.md +46 -0
  136. package/dist/skills/sweep-run/SKILL.md +437 -0
  137. package/dist/skills/sweep-run/scripts/env-manager.mjs +208 -0
  138. package/dist/skills/sweep-run/scripts/flow-parser.mjs +329 -0
  139. package/dist/skills/sweep-run/scripts/flow-selector.mjs +483 -0
  140. package/dist/skills/sweep-run/scripts/mcp-manager.mjs +208 -0
  141. package/dist/skills/sweep-run/scripts/spec-compiler.mjs +303 -0
  142. package/dist/skills/tide-discuss/SKILL.md.tmpl +449 -0
  143. package/dist/skills/tide-discuss/references/checklists.md +88 -0
  144. package/dist/skills/tide-discuss/references/data-format.md +237 -0
  145. package/dist/skills/tide-discuss/references/prd-template.md +134 -0
  146. package/dist/skills/tide-discuss/references/publish-flow.md +167 -0
  147. package/dist/skills/tide-discuss/references/role-prompts.md +105 -0
  148. package/package.json +38 -0
@@ -0,0 +1,197 @@
1
+ # 安全红线
2
+
3
+ ## 🔴 红线速查
4
+
5
+ | 红线 | 禁止行为 | 正确做法 | 违反后果 |
6
+ |------|---------|---------|---------|
7
+ | 密码存储 | 明文存储密码 | `BCryptPasswordEncoder`(Spring Security) | P0 — 安全漏洞 |
8
+ | SQL 拼接 | 字符串拼接 SQL | JPA `@Query` + 命名参数,或 QueryDSL/JPA Criteria | P0 — SQL 注入 |
9
+ | 危险初始化 | `@PostConstruct` 中触发写操作 | 延迟到首次调用或独立 `@EventListener(ApplicationReadyEvent.class)` | P1 — 非预期行为 |
10
+ | 硬编码密钥 | 代码中硬编码 API Key / Secret | 环境变量或 `application-{profile}.yml`(.env 不提交) | P0 — 凭据泄露 |
11
+ | 敏感信息打印 | `System.out.println` 敏感数据 | 用 SLF4J,生产日志级别过滤敏感字段 | P1 — 信息泄露 |
12
+ | 文件上传验证 | 无限制的文件上传 | 限制类型 + 大小 + 重命名 + 非 webroot 存储 | P1 — 任意文件上传 |
13
+ | 不安全的反序列化 | 信任外部反序列化数据 | 验证输入类型 + 数字签名 + 白名单 | P1 — 远程代码执行 |
14
+
15
+ ## 🔴 红线详情
16
+
17
+ ### 1. 密码存储 — 禁止明文 (P0)
18
+
19
+ ```java
20
+ // ❌ 坏:明文存储
21
+ user.setPassword(plainPassword);
22
+
23
+ // ✅ 好:使用 BCryptPasswordEncoder
24
+ @Service
25
+ public class UserService {
26
+ private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
27
+
28
+ public User createUser(CreateUserRequest request) {
29
+ User user = new User();
30
+ user.setPassword(passwordEncoder.encode(request.password()));
31
+ return userRepository.save(user);
32
+ }
33
+ }
34
+
35
+ // ✅ 验证密码
36
+ boolean matches = passwordEncoder.matches(rawPassword, user.getPassword());
37
+ ```
38
+
39
+ ### 2. SQL 拼接 — 禁止字符串拼接 (P0)
40
+
41
+ ```java
42
+ // ❌ 坏:字符串拼接 — SQL 注入
43
+ @Query("SELECT u FROM User u WHERE u.name = '" + name + "'")
44
+ List<User> findByName(String name);
45
+
46
+ // ✅ 好:命名参数
47
+ @Query("SELECT u FROM User u WHERE u.name = :name")
48
+ List<User> findByName(@Param("name") String name);
49
+
50
+ // ✅ 好:Spring Data 派生查询(简单场景)
51
+ List<User> findByName(String name);
52
+
53
+ // ❌ 坏:原生 SQL 拼接
54
+ @Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
55
+ List<User> findByRawSQL(String name);
56
+
57
+ // ✅ 好:原生 SQL + 命名参数
58
+ @Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)
59
+ List<User> findByRawSQL(@Param("name") String name);
60
+ ```
61
+
62
+ **红线规则:**
63
+ - Repository 中禁止使用 `+ name +` 拼接查询条件
64
+ - 禁止 `createNativeQuery`(也绕过多租户过滤)
65
+ - `@Query` 必须使用命名参数 `:paramName`,禁止使用 `?1` 位置参数
66
+ - Service 层禁止 `entityManager.createQuery(sqlString)` 传参拼接字符串
67
+
68
+ ### 3. 危险初始化 — 禁止 @PostConstruct 触发写操作 (P1)
69
+
70
+ ```java
71
+ // ❌ 坏:@PostConstruct 中操作数据库
72
+ @Component
73
+ public class DataInitializer {
74
+ @PostConstruct // ← 启动时必然执行,测试和回滚场景无法控制
75
+ public void init() {
76
+ userRepository.save(new User("admin", "admin"));
77
+ }
78
+ }
79
+
80
+ // ✅ 好:按需初始化或 EventListener(可控制执行时机)
81
+ @Component
82
+ public class DataInitializer {
83
+ @EventListener(ApplicationReadyEvent.class) // ← 应用完全启动后执行
84
+ @Profile("!test") // ← 测试环境跳过
85
+ public void init() {
86
+ log.info("Initializing default data...");
87
+ if (userRepository.count() == 0) {
88
+ userRepository.save(new User("admin", encodePassword("admin")));
89
+ }
90
+ }
91
+ }
92
+
93
+ // 或者更安全的模式:显式调用 + 幂等检查
94
+ @Component
95
+ public class DataInitializer {
96
+ private boolean initialized = false;
97
+
98
+ public synchronized void initializeIfNeeded() {
99
+ if (!initialized && userRepository.count() == 0) {
100
+ userRepository.save(new User("admin", encodePassword("admin")));
101
+ initialized = true;
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ ### 4. 硬编码密钥 — 禁止代码中内联 (P0)
108
+
109
+ ```java
110
+ // ❌ 坏:硬编码 API Key
111
+ private static final String API_KEY = "sk-abc123...";
112
+
113
+ // ✅ 好:环境变量
114
+ @Value("${app.api-key}")
115
+ private String apiKey;
116
+
117
+ // ✅ 好:Spring Cloud Config / Vault
118
+ @Value("${app.api-key}")
119
+ private String apiKey;
120
+ ```
121
+
122
+ ```yaml
123
+ # ❌ 坏:提交到 Git
124
+ api-key: sk-abc123...
125
+
126
+ # ✅ 好:application.yml 中只声明占位
127
+ app:
128
+ api-key: ${APP_API_KEY}
129
+ ```
130
+
131
+ ```properties
132
+ # ✅ .env 本地开发(不提交 Git)
133
+ APP_API_KEY=sk-abc123...
134
+ ```
135
+
136
+ ### 5. 敏感脱敏 — 日志中禁止打印敏感信息 (P1)
137
+
138
+ ```java
139
+ // ❌ 坏:直接打印敏感信息
140
+ log.info("User login: {} {}", user.getEmail(), user.getPassword()); // ← 密码泄露
141
+ log.info("Token: {}", authToken); // ← Token 泄露
142
+
143
+ // ✅ 好:脱敏后打印
144
+ log.info("User login: {}", maskEmail(user.getEmail()));
145
+ log.info("User [id={}] logged in", user.getId());
146
+
147
+ // 脱敏工具方法
148
+ public static String maskEmail(String email) {
149
+ int at = email.indexOf('@');
150
+ if (at <= 1) return email;
151
+ return email.charAt(0) + "***" + email.substring(at);
152
+ }
153
+
154
+ public static String maskPhone(String phone) {
155
+ if (phone == null || phone.length() < 7) return phone;
156
+ return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
157
+ }
158
+ ```
159
+
160
+ **红线规则:**
161
+ - 禁止打印密码、Token、完整手机号、完整身份证号
162
+ - DTO 中密码字段加 `@JsonIgnore` 或从响应 DTO 中排除
163
+ - 禁止 `System.out.println` — 必须通过 SLF4J 日志框架
164
+
165
+ ### 6. 文件上传验证 (P1)
166
+
167
+ ```java
168
+ // ✅ 好:文件上传安全检查
169
+ public class FileUploadService {
170
+ private static final List<String> ALLOWED_TYPES = List.of("image/jpeg", "image/png", "application/pdf");
171
+ private static final long MAX_SIZE = 10 * 1024 * 1024; // 10MB
172
+ private static final Path UPLOAD_DIR = Path.of("/var/data/uploads"); // ← 非 webroot
173
+
174
+ public void upload(MultipartFile file) {
175
+ // 1. 校验文件类型
176
+ String contentType = file.getContentType();
177
+ if (contentType == null || !ALLOWED_TYPES.contains(contentType)) {
178
+ throw new InvalidArgumentException(ErrorCode.VALIDATION_001, "不支持的文件类型");
179
+ }
180
+
181
+ // 2. 校验文件大小
182
+ if (file.getSize() > MAX_SIZE) {
183
+ throw new InvalidArgumentException(ErrorCode.VALIDATION_001, "文件大小超限");
184
+ }
185
+
186
+ // 3. 重命名防止路径穿越
187
+ String safeName = UUID.randomUUID() + "-" + cleanFileName(file.getOriginalFilename());
188
+
189
+ // 4. 存储到非 webroot 目录
190
+ file.transferTo(UPLOAD_DIR.resolve(safeName).toFile());
191
+ }
192
+
193
+ private String cleanFileName(String name) {
194
+ return name.replaceAll("[^a-zA-Z0-9._-]", "_");
195
+ }
196
+ }
197
+ ```
@@ -0,0 +1,69 @@
1
+ ```java
2
+ @DataJpaTest
3
+ @ActiveProfiles("test")
4
+ @Testcontainers
5
+ class UserRepositoryTest {
6
+
7
+ @Container @ServiceConnection
8
+ static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
9
+
10
+ @Autowired private UserRepository userRepository;
11
+ @Autowired private TestEntityManager entityManager;
12
+
13
+ // ──── 基本查询 ────
14
+
15
+ @Test
16
+ void should_findByEmail() {
17
+ entityManager.persistAndFlush(new User("alice@test.com", "Alice"));
18
+ assertThat(userRepository.findByEmail("alice@test.com")).isPresent();
19
+ }
20
+
21
+ @Test
22
+ void should_returnEmpty_when_notFound() {
23
+ assertThat(userRepository.findByEmail("missing@test.com")).isEmpty();
24
+ }
25
+
26
+ // ──── 多租户 ────
27
+
28
+ @Test
29
+ void should_findByIdAndAppId() {
30
+ User user = entityManager.persistAndFlush(
31
+ new User("multi@test.com", "Multi").setAppId(1L));
32
+ assertThat(userRepository.findByIdAndAppId(user.getId(), 1L)).isPresent();
33
+ assertThat(userRepository.findByIdAndAppId(user.getId(), 999L)).isEmpty();
34
+ }
35
+
36
+ @Test @Sql("/user-repository-test-data.sql")
37
+ void should_findAllByAppId() {
38
+ assertThat(userRepository.findAllByAppId(1L)).hasSize(2);
39
+ assertThat(userRepository.findAllByAppId(2L)).hasSize(1);
40
+ }
41
+
42
+ // ──── 存在性 & 计数 ────
43
+
44
+ @Test
45
+ void should_checkEmailExists() {
46
+ entityManager.persistAndFlush(new User("exists@test.com", "Exists"));
47
+ assertThat(userRepository.existsByEmail("exists@test.com")).isTrue();
48
+ assertThat(userRepository.existsByEmail("nobody@test.com")).isFalse();
49
+ }
50
+
51
+ @Test
52
+ void should_countByAppId() {
53
+ entityManager.persistAndFlush(new User("a@test.com", "A").setAppId(1L));
54
+ entityManager.persistAndFlush(new User("b@test.com", "B").setAppId(1L));
55
+ assertThat(userRepository.countByAppId(1L)).isEqualTo(2);
56
+ assertThat(userRepository.countByAppId(2L)).isZero();
57
+ }
58
+
59
+ // ──── @Query ────
60
+
61
+ @Test @Sql("/user-repository-test-data.sql")
62
+ void should_findActiveUsers() {
63
+ assertThat(userRepository.findActiveUsers())
64
+ .extracting(User::getEmail)
65
+ .contains("alice@test.com", "bob@test.com")
66
+ .doesNotContain("inactive@test.com");
67
+ }
68
+ }
69
+ ```
@@ -0,0 +1,101 @@
1
+ # DataJPA 测试规范
2
+
3
+ `@DataJpaTest` + Testcontainers:只加载 JPA 切片(Repository / Entity / DataSource),不加载 Service/Controller。
4
+
5
+ ## 基础结构
6
+
7
+ ```java
8
+ @DataJpaTest
9
+ @ActiveProfiles("test")
10
+ @Testcontainers
11
+ class UserRepositoryTest {
12
+ @Container @ServiceConnection
13
+ static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
14
+ @Autowired private UserRepository userRepository;
15
+ @Autowired private TestEntityManager entityManager;
16
+ }
17
+ ```
18
+
19
+ > **💡 Testcontainers vs H2:** Testcontainers 使用真实数据库容器,避免 H2 与 PostgreSQL/MySQL 的方言差异。简单项目可用 H2 兼容模式:`@AutoConfigureTestDatabase(replace = ANY)`
20
+
21
+ ## 测试模式
22
+
23
+ ### 基本查询
24
+
25
+ ```java
26
+ @Test
27
+ void should_findByEmail() {
28
+ entityManager.persistAndFlush(new User("alice@test.com", "Alice"));
29
+ Optional<User> result = userRepository.findByEmail("alice@test.com");
30
+ assertThat(result).isPresent();
31
+ }
32
+
33
+ @Test
34
+ void should_returnEmpty_when_notFound() {
35
+ assertThat(userRepository.findByEmail("missing@test.com")).isEmpty();
36
+ }
37
+ ```
38
+
39
+ ### `@Query` 自定义查询
40
+
41
+ ```java
42
+ @Test
43
+ void should_findActiveUsers() {
44
+ entityManager.persistAndFlush(new User("active@test.com", "Active").setActive(true));
45
+ entityManager.persistAndFlush(new User("inactive@test.com", "Inactive").setActive(false));
46
+
47
+ assertThat(userRepository.findActiveUsers())
48
+ .extracting(User::getEmail)
49
+ .containsExactly("active@test.com");
50
+ }
51
+ ```
52
+
53
+ ### `@Modifying` 写操作
54
+
55
+ ```java
56
+ @Test
57
+ void should_deactivateUser() {
58
+ entityManager.persistAndFlush(new User("leave@test.com", "Leaving"));
59
+ assertThat(userRepository.deactivateByEmail("leave@test.com")).isEqualTo(1);
60
+ }
61
+ ```
62
+
63
+ ### 多租户查询
64
+
65
+ ```java
66
+ @Test
67
+ void should_findByAppId() {
68
+ entityManager.persistAndFlush(new User("a@test.com", "A").setAppId(1L));
69
+ entityManager.persistAndFlush(new User("b@test.com", "B").setAppId(2L));
70
+
71
+ assertThat(userRepository.findAllByAppId(1L)).hasSize(1);
72
+ // 必须传 appId 过滤,禁止裸 findById
73
+ assertThat(userRepository.findByIdAndAppId(id, 1L)).isPresent();
74
+ assertThat(userRepository.findByIdAndAppId(id, 999L)).isEmpty();
75
+ }
76
+ ```
77
+
78
+ ## 数据准备
79
+
80
+ ```java
81
+ // 方式一:@Sql 加载脚本(适合多数据)
82
+ @Test @Sql("/user-test-data.sql")
83
+ void should_findAll() { ... }
84
+
85
+ // 方式二:entityManager(适合少量)
86
+ @Test
87
+ void should_findByEmail() {
88
+ entityManager.persistAndFlush(new User("test@test.com", "Test"));
89
+ ...
90
+ }
91
+ ```
92
+
93
+ ## 关键规则
94
+
95
+ | 场景 | 做法 |
96
+ |------|------|
97
+ | Repository 测试 | `@DataJpaTest` |
98
+ | 测试数据库 | Testcontainers + `@ServiceConnection` |
99
+ | 数据准备 | `entityManager.persistAndFlush()` 或 `@Sql` |
100
+ | 多租户 | `findByIdAndAppId(id, appId)` **禁止裸 `findById`** |
101
+ | 写操作 | `@Modifying` + 验证 `int updated` 返回值 |
@@ -0,0 +1,61 @@
1
+ ```java
2
+ @ExtendWith(MockitoExtension.class)
3
+ class UserServiceTest {
4
+
5
+ @Mock private UserRepository userRepository;
6
+ @Mock private UserMapper userMapper;
7
+ @InjectMocks private UserService userService;
8
+
9
+ private User userEntity;
10
+ private UserDTO userDTO;
11
+
12
+ @BeforeEach
13
+ void setUp() {
14
+ userEntity = new User(1L, "test@example.com", "Test User");
15
+ userDTO = new UserDTO(1L, "test@example.com", "Test User");
16
+ }
17
+
18
+ @Test
19
+ void should_returnUser_when_findById() {
20
+ when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
21
+ when(userMapper.toDTO(userEntity)).thenReturn(userDTO);
22
+
23
+ UserDTO result = userService.findById(1L);
24
+
25
+ assertThat(result)
26
+ .extracting(UserDTO::id, UserDTO::email)
27
+ .containsExactly(1L, "test@example.com");
28
+ }
29
+
30
+ @Test
31
+ void should_throw_when_userNotFound() {
32
+ when(userRepository.findById(999L)).thenReturn(Optional.empty());
33
+
34
+ assertThatThrownBy(() -> userService.findById(999L))
35
+ .isInstanceOf(NotFoundException.class)
36
+ .hasMessageContaining("User");
37
+ }
38
+
39
+ @ParameterizedTest
40
+ @CsvSource({"''", "' '", "null"})
41
+ void should_throw_when_nameIsBlank(String name) {
42
+ assertThatThrownBy(() -> userService.create(
43
+ new CreateUserRequest(name, "a@test.com")))
44
+ .isInstanceOf(InvalidArgumentException.class);
45
+ }
46
+
47
+ @Test
48
+ void should_createUser_andReturnDTO() {
49
+ when(userMapper.toEntity(any())).thenReturn(new User(null, "new@test.com", "New"));
50
+ when(userRepository.save(any())).thenReturn(new User(2L, "new@test.com", "New"));
51
+ when(userMapper.toDTO(any())).thenReturn(new UserDTO(2L, "new@test.com", "New"));
52
+
53
+ UserDTO result = userService.create(
54
+ new CreateUserRequest("New User", "new@test.com"));
55
+
56
+ assertThat(result).usingRecursiveComparison()
57
+ .isEqualTo(new UserDTO(2L, "new@test.com", "New"));
58
+ verify(userRepository).save(any());
59
+ }
60
+ }
61
+ ```
@@ -0,0 +1,100 @@
1
+ # JUnit 5 测试规范
2
+
3
+ ## 文件与命名规范
4
+
5
+ - 文件位置:`src/test/java/`,包路径与被测类一致
6
+ - 类名:`XxxTest.java`(`Test` 后缀)
7
+ - 方法名:`should_expectedBehavior_when_condition`
8
+ - 例:`should_returnUser_when_findById`
9
+
10
+ ## 基本注解
11
+
12
+ | 注解 | 用途 |
13
+ |------|------|
14
+ | `@Test` | 标记测试方法,每个方法一个独立场景 |
15
+ | `@BeforeEach` | 每个测试前执行(初始化、Mock 设置) |
16
+ | `@AfterEach` | 每个测试后执行(清理资源) |
17
+ | `@BeforeAll` | 所有测试前(static) |
18
+ | `@AfterAll` | 所有测试后(static) |
19
+
20
+ ## AAA 模式
21
+
22
+ ```java
23
+ @Test
24
+ void should_returnUser_when_findById() {
25
+ // Arrange
26
+ when(userRepository.findById(userId)).thenReturn(Optional.of(user));
27
+ // Act
28
+ UserDTO result = userService.findById(userId);
29
+ // Assert
30
+ assertThat(result).extracting(UserDTO::id, UserDTO::email)
31
+ .containsExactly(1L, "test@example.com");
32
+ }
33
+ ```
34
+
35
+ ## 断言(AssertJ 流畅 API)
36
+
37
+ ```java
38
+ // 基本
39
+ assertThat(actual).isEqualTo(expected).isNotNull();
40
+ // 集合
41
+ assertThat(list).hasSize(3).containsExactly(a, b, c);
42
+ assertThat(list).extracting(User::id).containsExactly(1L, 2L, 3L);
43
+ assertThat(list).extracting(User::id, User::email)
44
+ .containsExactly(tuple(1L, "a@test.com"), tuple(2L, "b@test.com"));
45
+ // 异常
46
+ assertThatThrownBy(() -> service.findById(999L))
47
+ .isInstanceOf(NotFoundException.class)
48
+ .hasMessageContaining("User");
49
+ ```
50
+
51
+ ## 参数化测试
52
+
53
+ ```java
54
+ @ParameterizedTest
55
+ @ValueSource(strings = {"", " "})
56
+ void should_throw_when_nameIsBlank(String name) {
57
+ assertThatThrownBy(() -> validator.validateName(name))
58
+ .isInstanceOf(InvalidArgumentException.class);
59
+ }
60
+
61
+ @ParameterizedTest
62
+ @CsvSource({"user@test.com, true", "invalid-email, false"})
63
+ void should_validateEmail(String email, boolean expected) {
64
+ assertThat(validator.isValidEmail(email)).isEqualTo(expected);
65
+ }
66
+
67
+ @ParameterizedTest
68
+ @MethodSource("invalidUserProvider")
69
+ void should_throw_when_userInvalid(CreateUserRequest request) { ... }
70
+
71
+ static Stream<CreateUserRequest> invalidUserProvider() { ... }
72
+ ```
73
+
74
+ ## Mock(Mockito)
75
+
76
+ ```java
77
+ @ExtendWith(MockitoExtension.class)
78
+ class UserServiceTest {
79
+ @Mock private UserRepository userRepository;
80
+ @InjectMocks private UserService userService;
81
+ @Captor private ArgumentCaptor<User> userCaptor;
82
+
83
+ @Test
84
+ void should_createUser() {
85
+ when(userMapper.toEntity(request)).thenReturn(entity);
86
+ when(userRepository.save(entity)).thenReturn(saved);
87
+ when(userMapper.toDTO(saved)).thenReturn(expected);
88
+ UserDTO result = userService.create(request);
89
+ assertThat(result).isEqualTo(expected);
90
+ verify(userRepository).save(entity);
91
+ }
92
+
93
+ @Test
94
+ void should_throw_when_userNotFound() {
95
+ when(userRepository.findById(999L)).thenReturn(Optional.empty());
96
+ assertThatThrownBy(() -> userService.findById(999L))
97
+ .isInstanceOf(NotFoundException.class);
98
+ }
99
+ }
100
+ ```
@@ -0,0 +1,61 @@
1
+ ```java
2
+ @WebMvcTest(UserController.class)
3
+ class UserControllerTest {
4
+
5
+ @Autowired private MockMvc mockMvc;
6
+ @Autowired private ObjectMapper objectMapper;
7
+ @MockitoBean private UserService userService;
8
+
9
+ // ──────── GET ────────
10
+
11
+ @Test
12
+ void should_returnAllUsers() throws Exception {
13
+ when(userService.findAll()).thenReturn(List.of(
14
+ new UserDTO(1L, "a@test.com", "Alice"),
15
+ new UserDTO(2L, "b@test.com", "Bob")));
16
+
17
+ mockMvc.perform(get("/api/v1/users"))
18
+ .andExpect(status().isOk())
19
+ .andExpect(jsonPath("$", hasSize(2)))
20
+ .andExpect(jsonPath("$[0].email").value("a@test.com"));
21
+ }
22
+
23
+ @Test
24
+ void should_return404_when_userNotFound() throws Exception {
25
+ when(userService.findById(999L)).thenThrow(new NotFoundException("User", 999L));
26
+
27
+ mockMvc.perform(get("/api/v1/users/{id}", 999L))
28
+ .andExpect(status().isNotFound());
29
+ }
30
+
31
+ // ──────── POST ────────
32
+
33
+ @Test
34
+ @WithMockUser(authorities = "USER_CREATE")
35
+ void should_createUser() throws Exception {
36
+ when(userService.create(any())).thenReturn(new UserDTO(3L, "carol@test.com", "Carol"));
37
+
38
+ mockMvc.perform(post("/api/v1/users")
39
+ .contentType(MediaType.APPLICATION_JSON)
40
+ .content(objectMapper.writeValueAsString(new CreateUserRequest("carol@test.com", "Carol"))))
41
+ .andExpect(status().isCreated())
42
+ .andExpect(jsonPath("$.id").value(3L));
43
+ }
44
+
45
+ @Test
46
+ void should_return403_when_notAuthorized() throws Exception {
47
+ mockMvc.perform(post("/api/v1/users")
48
+ .contentType(MediaType.APPLICATION_JSON)
49
+ .content(objectMapper.writeValueAsString(new CreateUserRequest("carol@test.com", "Carol"))))
50
+ .andExpect(status().isForbidden());
51
+ }
52
+
53
+ @Test
54
+ void should_return400_when_invalidInput() throws Exception {
55
+ mockMvc.perform(post("/api/v1/users")
56
+ .contentType(MediaType.APPLICATION_JSON)
57
+ .content(objectMapper.writeValueAsString(new CreateUserRequest("", ""))))
58
+ .andExpect(status().isBadRequest());
59
+ }
60
+ }
61
+ ```
@@ -0,0 +1,85 @@
1
+ # Spring MVC 测试规范
2
+
3
+ `@WebMvcTest` + `MockMvc`:只加载 Web 切片,不加载完整 Spring 上下文。
4
+
5
+ ## 基础结构
6
+
7
+ ```java
8
+ @WebMvcTest(UserController.class)
9
+ class UserControllerTest {
10
+ @Autowired private MockMvc mockMvc;
11
+ @MockitoBean private UserService userService; // 注入 Mock Service
12
+ }
13
+ ```
14
+
15
+ ## 测试模式
16
+
17
+ ### GET 列表 / 详情 / 404
18
+
19
+ ```java
20
+ @Test
21
+ void should_returnAllUsers() throws Exception {
22
+ when(userService.findAll()).thenReturn(List.of(new UserDTO(1L, "a@test.com")));
23
+
24
+ mockMvc.perform(get("/api/v1/users"))
25
+ .andExpect(status().isOk())
26
+ .andExpect(jsonPath("$", hasSize(1)))
27
+ .andExpect(jsonPath("$[0].email").value("a@test.com"));
28
+ }
29
+
30
+ @Test
31
+ void should_return404_when_userNotFound() throws Exception {
32
+ when(userService.findById(999L)).thenThrow(new NotFoundException("User", 999L));
33
+
34
+ mockMvc.perform(get("/api/v1/users/{id}", 999L))
35
+ .andExpect(status().isNotFound())
36
+ .andExpect(jsonPath("$.message").value(containsString("User")));
37
+ }
38
+ ```
39
+
40
+ ### POST 创建(含权限)
41
+
42
+ ```java
43
+ @Test
44
+ @WithMockUser(authorities = "USER_CREATE")
45
+ void should_createUser() throws Exception {
46
+ when(userService.create(any(CreateUserRequest.class))).thenReturn(new UserDTO(...));
47
+
48
+ mockMvc.perform(post("/api/v1/users")
49
+ .contentType(MediaType.APPLICATION_JSON)
50
+ .content(objectMapper.writeValueAsString(request)))
51
+ .andExpect(status().isCreated())
52
+ .andExpect(jsonPath("$.id").value(1L));
53
+ }
54
+
55
+ @Test
56
+ void should_return403_when_noPermission() throws Exception {
57
+ mockMvc.perform(post("/api/v1/users")
58
+ .contentType(MediaType.APPLICATION_JSON)
59
+ .content(objectMapper.writeValueAsString(request)))
60
+ .andExpect(status().isForbidden());
61
+ }
62
+ ```
63
+
64
+ ### 参数校验失败
65
+
66
+ ```java
67
+ @Test
68
+ void should_return400_when_invalidInput() throws Exception {
69
+ mockMvc.perform(post("/api/v1/users")
70
+ .contentType(MediaType.APPLICATION_JSON)
71
+ .content(objectMapper.writeValueAsString(new CreateUserRequest("", ""))))
72
+ .andExpect(status().isBadRequest());
73
+ }
74
+ ```
75
+
76
+ ## 关键规则
77
+
78
+ | 场景 | 做法 |
79
+ |------|------|
80
+ | Controller 测试 | `@WebMvcTest` + `MockMvc` |
81
+ | Service Mock | `@MockitoBean` |
82
+ | 权限测试 | `@WithMockUser` + `authorities` |
83
+ | JSON 验证 | `jsonPath("$.field").value(...)` |
84
+ | 请求体 | `objectMapper.writeValueAsString(dto)` |
85
+ | 异常路径 | Service 抛出异常,验证状态码 + message |