@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.
- package/README.md +72 -0
- package/dist/agents/reef-inspect-figma.md +77 -0
- package/dist/agents/reef-review-backend.md.tmpl +112 -0
- package/dist/agents/reef-review-frontend.md.tmpl +78 -0
- package/dist/agents/reef-review-infra.md +47 -0
- package/dist/agents/reef-review-security.md.tmpl +80 -0
- package/dist/agents/reef-scope-analysis.md +64 -0
- package/dist/build-registry.js +375 -0
- package/dist/cli.js +8581 -0
- package/dist/config-schema.json +133 -0
- package/dist/env-examples/context7.env-example +19 -0
- package/dist/env-examples/feishu-wiki.env-example +16 -0
- package/dist/env-examples/figma.env-example +16 -0
- package/dist/env-examples/github.env-example +20 -0
- package/dist/env-examples/jira.env-example +20 -0
- package/dist/hooks/mcp-hook.sh +77 -0
- package/dist/hooks/reef-auto-format.sh.tmpl +72 -0
- package/dist/hooks/reef-block-dangerous.sh +70 -0
- package/dist/hooks/reef-hooks.json +72 -0
- package/dist/hooks/reef-intent-detect.sh +129 -0
- package/dist/hooks/reef-protect-files.sh +55 -0
- package/dist/hooks/reef-run-tests.sh +84 -0
- package/dist/hooks/reef-scope-check.sh +386 -0
- package/dist/hooks/reef-scope-ci.sh +28 -0
- package/dist/hooks/reef-scope-gate.sh +115 -0
- package/dist/hooks/reef-scope-pre-commit.sh.tmpl +28 -0
- package/dist/hooks/reef-scope-setup.sh +204 -0
- package/dist/hooks/reef-scope-split.sh +203 -0
- package/dist/hooks/sweep-hooks.json +14 -0
- package/dist/hooks/sweep-mcp-hook.sh +77 -0
- package/dist/hooks/tide-hooks.json +14 -0
- package/dist/hooks/tide-session-preload.sh +17 -0
- package/dist/mcp/code-hosting/github.json +20 -0
- package/dist/mcp/design-tools/figma.json +19 -0
- package/dist/mcp/docs-reference/context7.json +28 -0
- package/dist/mcp/e2e-testing/playwright.json +13 -0
- package/dist/mcp/knowledge-base/feishu-wiki.json +19 -0
- package/dist/mcp/project-management/jira.json +27 -0
- package/dist/mcp-skills/deepflow-mcp-feishu-wiki-read/SKILL.md +65 -0
- package/dist/mcp-skills/deepflow-mcp-feishu-wiki-write/SKILL.md +63 -0
- package/dist/mcp-skills/deepflow-mcp-figma-read/SKILL.md +98 -0
- package/dist/mcp-skills/deepflow-mcp-github-read/SKILL.md +62 -0
- package/dist/mcp-skills/deepflow-mcp-github-write/SKILL.md +63 -0
- package/dist/mcp-skills/deepflow-mcp-jira-read/SKILL.md +80 -0
- package/dist/mcp-skills/deepflow-mcp-jira-write/SKILL.md +74 -0
- package/dist/mcp-skills/deepflow-mcp-playwright-read/SKILL.md +79 -0
- package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-read/SKILL.md +65 -0
- package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-write/SKILL.md +63 -0
- package/dist/mcp-skills/deepstorm-mcp-figma-read/SKILL.md +98 -0
- package/dist/mcp-skills/deepstorm-mcp-github-read/SKILL.md +62 -0
- package/dist/mcp-skills/deepstorm-mcp-github-write/SKILL.md +63 -0
- package/dist/mcp-skills/deepstorm-mcp-jira-read/SKILL.md +80 -0
- package/dist/mcp-skills/deepstorm-mcp-jira-write/SKILL.md +74 -0
- package/dist/mcp-skills/deepstorm-mcp-playwright-read/SKILL.md +79 -0
- package/dist/registry.json +818 -0
- package/dist/skills/atoll-ops/SKILL.md +46 -0
- package/dist/skills/reef-commit/SKILL.md +127 -0
- package/dist/skills/reef-gen-backend/SKILL.md.tmpl +87 -0
- package/dist/skills/reef-gen-backend/variants/java/steps.md +28 -0
- package/dist/skills/reef-gen-backend/variants/python/steps.md +70 -0
- package/dist/skills/reef-gen-frontend/SKILL.md.tmpl +83 -0
- package/dist/skills/reef-gen-frontend/variants/angular/steps.md +30 -0
- package/dist/skills/reef-harden/EXAMPLES.md +89 -0
- package/dist/skills/reef-harden/SKILL.md +136 -0
- package/dist/skills/reef-pr/SKILL.md +97 -0
- package/dist/skills/reef-review/SKILL.md.tmpl +107 -0
- package/dist/skills/reef-scope/SKILL.md +134 -0
- package/dist/skills/reef-start/SKILL.md.tmpl +562 -0
- package/dist/skills/reef-start/references/jira-start-subagent.md +60 -0
- package/dist/skills/reef-style-backend/SKILL.md.tmpl +134 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/chat-client.md +96 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/rag.md +94 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/structured-output.md +62 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/tool-calling.md +68 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/quick-reference.md +220 -0
- package/dist/skills/reef-style-backend/fragments/java/api-spec/quick-reference.md +148 -0
- package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/examples/database-migration.md +131 -0
- package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/quick-reference.md +103 -0
- package/dist/skills/reef-style-backend/fragments/java/dependency-management/quick-reference.md +119 -0
- package/dist/skills/reef-style-backend/fragments/java/exception-handling/examples/error-code-enum.md +101 -0
- package/dist/skills/reef-style-backend/fragments/java/exception-handling/quick-reference.md +181 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/controller.md +95 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/dto-mapper.md +121 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/infrastructure.md +179 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/service-entity.md +202 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/testing.md +107 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/quick-reference.md +83 -0
- package/dist/skills/reef-style-backend/fragments/java/orm/hibernate/quick-reference.md +150 -0
- package/dist/skills/reef-style-backend/fragments/java/security-redlines/quick-reference.md +197 -0
- package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/examples/user-repository-test.md +69 -0
- package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/quick-reference.md +101 -0
- package/dist/skills/reef-style-backend/fragments/java/test/junit5/examples/user-service-test.md +61 -0
- package/dist/skills/reef-style-backend/fragments/java/test/junit5/quick-reference.md +100 -0
- package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/examples/user-controller-test.md +61 -0
- package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/quick-reference.md +85 -0
- package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/examples/user-service-integration-test.md +56 -0
- package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/quick-reference.md +83 -0
- package/dist/skills/reef-style-backend/fragments/python/alembic-migration/quick-reference.md +77 -0
- package/dist/skills/reef-style-backend/fragments/python/api-spec/quick-reference.md +164 -0
- package/dist/skills/reef-style-backend/fragments/python/dependency-management/quick-reference.md +139 -0
- package/dist/skills/reef-style-backend/fragments/python/exception-handling/quick-reference.md +177 -0
- package/dist/skills/reef-style-backend/fragments/python/fastapi-quick-reference/quick-reference.md +101 -0
- package/dist/skills/reef-style-backend/fragments/python/langchain/quick-reference.md +135 -0
- package/dist/skills/reef-style-backend/fragments/python/pytest-testing/quick-reference.md +111 -0
- package/dist/skills/reef-style-backend/fragments/python/ruff-mypy-toolchain/quick-reference.md +83 -0
- package/dist/skills/reef-style-backend/fragments/python/security-redlines/quick-reference.md +207 -0
- package/dist/skills/reef-style-backend/fragments/python/sqlalchemy-orm/quick-reference.md +91 -0
- package/dist/skills/reef-style-backend/variants/java/examples/code-wrapping.md +227 -0
- package/dist/skills/reef-style-backend/variants/java/examples/contributor-pattern.md +97 -0
- package/dist/skills/reef-style-backend/variants/java/quick-reference.md +117 -0
- package/dist/skills/reef-style-backend/variants/python/examples/crud-router.md +71 -0
- package/dist/skills/reef-style-backend/variants/python/examples/pydantic-schema.md +45 -0
- package/dist/skills/reef-style-backend/variants/python/examples/pytest-fixture.md +60 -0
- package/dist/skills/reef-style-backend/variants/python/examples/sqlalchemy-model.md +31 -0
- package/dist/skills/reef-style-backend/variants/python/quick-reference.md +111 -0
- package/dist/skills/reef-style-frontend/SKILL.md.tmpl +70 -0
- package/dist/skills/reef-style-frontend/fragments/css/tailwind/quick-reference.md +79 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest/examples/testing.md +150 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest/quick-reference.md +141 -0
- package/dist/skills/reef-style-frontend/fragments/ts-config/strict/quick-reference.md +112 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/examples/ui-components.md +134 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/quick-reference.md +66 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/code-wrapping.md +252 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/component-types-pipes.md +56 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/entity-types.md +100 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/forms-layer.md +119 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/service-routing.md +95 -0
- package/dist/skills/reef-style-frontend/variants/angular/quick-reference.md +100 -0
- package/dist/skills/reef-testcase/SKILL.md +138 -0
- package/dist/skills/reef-testcase/references/coverage-dimensions.md +99 -0
- package/dist/skills/reef-testcase/references/test-case-template.md +63 -0
- package/dist/skills/sweep-init/SKILL.md +286 -0
- package/dist/skills/sweep-init/scripts/flow-selector.mjs +305 -0
- package/dist/skills/sweep-plan/SKILL.md.tmpl +312 -0
- package/dist/skills/sweep-plan/references/test-flow-template.md +46 -0
- package/dist/skills/sweep-run/SKILL.md +437 -0
- package/dist/skills/sweep-run/scripts/env-manager.mjs +208 -0
- package/dist/skills/sweep-run/scripts/flow-parser.mjs +329 -0
- package/dist/skills/sweep-run/scripts/flow-selector.mjs +483 -0
- package/dist/skills/sweep-run/scripts/mcp-manager.mjs +208 -0
- package/dist/skills/sweep-run/scripts/spec-compiler.mjs +303 -0
- package/dist/skills/tide-discuss/SKILL.md.tmpl +449 -0
- package/dist/skills/tide-discuss/references/checklists.md +88 -0
- package/dist/skills/tide-discuss/references/data-format.md +237 -0
- package/dist/skills/tide-discuss/references/prd-template.md +134 -0
- package/dist/skills/tide-discuss/references/publish-flow.md +167 -0
- package/dist/skills/tide-discuss/references/role-prompts.md +105 -0
- 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` 返回值 |
|
package/dist/skills/reef-style-backend/fragments/java/test/junit5/examples/user-service-test.md
ADDED
|
@@ -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 |
|