cc-codeconductor 0.2.1
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/LICENSE +18 -0
- package/README.md +319 -0
- package/dist/index.js +12741 -0
- package/package.json +51 -0
- package/policy.yml +124 -0
- package/presets/claude/CLAUDE.md +598 -0
- package/presets/claude/commands/cc/feature.md +115 -0
- package/presets/claude/commands/cc/fix.md +121 -0
- package/presets/claude/commands/cc/refactor.md +148 -0
- package/presets/claude/commands/cc/review.md +126 -0
- package/presets/claude/commands/cc/tdd-cycle.md +226 -0
- package/presets/claude/commands/cc/test-plan.md +138 -0
- package/presets/claude/settings.json +37 -0
- package/presets/claude/skills/api-versioning/SKILL.md +389 -0
- package/presets/claude/skills/django-orm/SKILL.md +455 -0
- package/presets/claude/skills/django-testing/SKILL.md +409 -0
- package/presets/claude/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/claude/skills/python/SKILL.md +606 -0
- package/presets/claude/skills/python-django-stack/SKILL.md +492 -0
- package/presets/claude/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/claude/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/claude/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/claude/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/claude/skills/testing-strategy/SKILL.md +479 -0
- package/presets/codex/AGENTS.md +883 -0
- package/presets/codex/README.md +102 -0
- package/presets/codex/skills/api-versioning/SKILL.md +389 -0
- package/presets/codex/skills/django-orm/SKILL.md +455 -0
- package/presets/codex/skills/django-testing/SKILL.md +409 -0
- package/presets/codex/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/codex/skills/python/SKILL.md +606 -0
- package/presets/codex/skills/python-django-stack/SKILL.md +492 -0
- package/presets/codex/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/codex/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/codex/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/codex/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/codex/skills/testing-strategy/SKILL.md +479 -0
- package/presets/opencode/README.md +191 -0
- package/presets/opencode/agents/architect.md +133 -0
- package/presets/opencode/agents/docs.md +113 -0
- package/presets/opencode/agents/implementer.md +116 -0
- package/presets/opencode/agents/orchestrator.md +248 -0
- package/presets/opencode/agents/repo-explorer.md +119 -0
- package/presets/opencode/agents/reviewer.md +130 -0
- package/presets/opencode/agents/task-coach.md +103 -0
- package/presets/opencode/agents/tester.md +269 -0
- package/presets/opencode/commands/cc-feature.md +114 -0
- package/presets/opencode/commands/cc-fix.md +123 -0
- package/presets/opencode/commands/cc-refactor.md +148 -0
- package/presets/opencode/commands/cc-review.md +141 -0
- package/presets/opencode/commands/cc-tdd-cycle.md +225 -0
- package/presets/opencode/commands/cc-test-plan.md +144 -0
- package/presets/opencode/opencode.jsonc +89 -0
- package/presets/opencode/prompts/v0.1.0/architect.md +213 -0
- package/presets/opencode/prompts/v0.1.0/docs.md +181 -0
- package/presets/opencode/prompts/v0.1.0/implementer.md +154 -0
- package/presets/opencode/prompts/v0.1.0/orchestrator.md +169 -0
- package/presets/opencode/prompts/v0.1.0/repo-explorer.md +102 -0
- package/presets/opencode/prompts/v0.1.0/reviewer.md +183 -0
- package/presets/opencode/prompts/v0.1.0/task-coach.md +142 -0
- package/presets/opencode/prompts/v0.1.0/tester.md +160 -0
- package/presets/opencode/prompts/v0.2.0/architect.md +219 -0
- package/presets/opencode/prompts/v0.2.0/docs.md +187 -0
- package/presets/opencode/prompts/v0.2.0/implementer.md +160 -0
- package/presets/opencode/prompts/v0.2.0/orchestrator.md +238 -0
- package/presets/opencode/prompts/v0.2.0/repo-explorer.md +108 -0
- package/presets/opencode/prompts/v0.2.0/reviewer.md +190 -0
- package/presets/opencode/prompts/v0.2.0/task-coach.md +153 -0
- package/presets/opencode/prompts/v0.2.0/tester.md +249 -0
- package/presets/opencode/skills/api-versioning/SKILL.md +388 -0
- package/presets/opencode/skills/astro/SKILL.md +313 -0
- package/presets/opencode/skills/code-review/SKILL.md +202 -0
- package/presets/opencode/skills/django-orm/SKILL.md +455 -0
- package/presets/opencode/skills/django-testing/SKILL.md +408 -0
- package/presets/opencode/skills/django-uv/SKILL.md +400 -0
- package/presets/opencode/skills/jpa-postgres/SKILL.md +617 -0
- package/presets/opencode/skills/nextjs-typescript/SKILL.md +385 -0
- package/presets/opencode/skills/python/SKILL.md +605 -0
- package/presets/opencode/skills/python-django-stack/SKILL.md +491 -0
- package/presets/opencode/skills/python-fastapi-stack/SKILL.md +456 -0
- package/presets/opencode/skills/security/SKILL.md +376 -0
- package/presets/opencode/skills/spring-boot-feature/SKILL.md +557 -0
- package/presets/opencode/skills/spring-boot-kotlin/SKILL.md +399 -0
- package/presets/opencode/skills/spring-boot-testing-strategy/SKILL.md +470 -0
- package/presets/opencode/skills/sqlalchemy/SKILL.md +465 -0
- package/presets/opencode/skills/testing-tdd/SKILL.md +586 -0
- package/src/presets/council/council.yml +59 -0
- package/src/presets/manifests/claude.yml +23 -0
- package/src/presets/manifests/codex.yml +12 -0
- package/src/presets/manifests/opencode.yml +18 -0
- package/src/presets/models/claude.yml +37 -0
- package/src/presets/models/codex.yml +38 -0
- package/src/presets/models/opencode.yml +37 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: spring-boot-kotlin
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
name: Spring Boot + Kotlin
|
|
5
|
+
description:
|
|
6
|
+
Provides expert knowledge of Spring Boot conventions, Kotlin idioms, and MVC
|
|
7
|
+
patterns for backend API development.
|
|
8
|
+
|
|
9
|
+
compatibility:
|
|
10
|
+
tools: [claude, codex, opencode]
|
|
11
|
+
stacks:
|
|
12
|
+
languages: [kotlin]
|
|
13
|
+
frameworks: [spring-boot, spring-mvc, spring-data-jpa, spring-security]
|
|
14
|
+
databases: [postgresql, h2]
|
|
15
|
+
|
|
16
|
+
risk:
|
|
17
|
+
level: medium
|
|
18
|
+
can_execute_shell: false
|
|
19
|
+
can_modify_files: true
|
|
20
|
+
requires_network: false
|
|
21
|
+
|
|
22
|
+
inputs:
|
|
23
|
+
- source_files
|
|
24
|
+
- test_files
|
|
25
|
+
- build.gradle.kts
|
|
26
|
+
- application.yml
|
|
27
|
+
|
|
28
|
+
outputs:
|
|
29
|
+
- controller classes
|
|
30
|
+
- service classes
|
|
31
|
+
- repository interfaces
|
|
32
|
+
- domain entities
|
|
33
|
+
- DTO classes
|
|
34
|
+
- test classes
|
|
35
|
+
|
|
36
|
+
quality:
|
|
37
|
+
reviewed_by: codeconductor-core
|
|
38
|
+
version: 0.1.0
|
|
39
|
+
---
|
|
40
|
+
# Spring Boot + Kotlin
|
|
41
|
+
|
|
42
|
+
## Project Structure
|
|
43
|
+
|
|
44
|
+
Use feature-oriented MVC. One package per feature, not one package per layer.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
src/main/kotlin/{base-package}/{feature}/
|
|
48
|
+
controller/ # HTTP layer only — no business logic
|
|
49
|
+
service/ # Business logic
|
|
50
|
+
repository/ # Data access — extends JpaRepository or CrudRepository
|
|
51
|
+
domain/ # JPA entities
|
|
52
|
+
dto/ # Request/response objects — no @Entity here
|
|
53
|
+
|
|
54
|
+
src/test/kotlin/{base-package}/{feature}/
|
|
55
|
+
controller/ # MockMvc tests
|
|
56
|
+
service/ # Unit tests with MockK
|
|
57
|
+
repository/ # @DataJpaTest tests
|
|
58
|
+
|
|
59
|
+
src/main/resources/
|
|
60
|
+
application.yml
|
|
61
|
+
application-prod.yml # NOT in repo — use env vars
|
|
62
|
+
db/migration/ # Flyway scripts
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Never use `src/main/kotlin/controllers/`, `src/main/kotlin/services/`, etc. That
|
|
66
|
+
is layer-first structure and it does not scale.
|
|
67
|
+
|
|
68
|
+
## Kotlin Idioms for Spring
|
|
69
|
+
|
|
70
|
+
**Data classes for DTOs.** No setters, no mutable state.
|
|
71
|
+
|
|
72
|
+
```kotlin
|
|
73
|
+
data class CreateUserRequest(
|
|
74
|
+
@field:NotBlank val email: String,
|
|
75
|
+
@field:Size(min = 2, max = 100) val name: String
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
data class UserResponse(
|
|
79
|
+
val id: UUID,
|
|
80
|
+
val email: String,
|
|
81
|
+
val name: String
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Sealed classes for domain results and errors.**
|
|
86
|
+
|
|
87
|
+
```kotlin
|
|
88
|
+
sealed class UserResult {
|
|
89
|
+
data class Found(val user: User) : UserResult()
|
|
90
|
+
data class NotFound(val id: UUID) : UserResult()
|
|
91
|
+
data class Conflict(val email: String) : UserResult()
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Null safety.** Never use `!!` unless you have already verified the value is
|
|
96
|
+
non-null and can document why. Prefer:
|
|
97
|
+
|
|
98
|
+
```kotlin
|
|
99
|
+
// bad
|
|
100
|
+
val name = user!!.name
|
|
101
|
+
|
|
102
|
+
// good
|
|
103
|
+
val name = user?.name ?: throw UserNotFoundException(id)
|
|
104
|
+
|
|
105
|
+
// good — scoped null check
|
|
106
|
+
user?.let { sendWelcomeEmail(it.email) }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Extension functions** instead of utility classes.
|
|
110
|
+
|
|
111
|
+
```kotlin
|
|
112
|
+
// bad — Java style utility class
|
|
113
|
+
object UserMapper {
|
|
114
|
+
fun toResponse(user: User): UserResponse { ... }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// good — extension function
|
|
118
|
+
fun User.toResponse(): UserResponse = UserResponse(
|
|
119
|
+
id = id,
|
|
120
|
+
email = email,
|
|
121
|
+
name = name
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Coroutines**: do not introduce coroutines unless the project already uses
|
|
126
|
+
them. Adding coroutines requires a Task Card with explicit scope.
|
|
127
|
+
|
|
128
|
+
## Spring Boot Patterns
|
|
129
|
+
|
|
130
|
+
### Controller
|
|
131
|
+
|
|
132
|
+
```kotlin
|
|
133
|
+
@RestController
|
|
134
|
+
@RequestMapping("/api/v1/users")
|
|
135
|
+
class UserController(private val userService: UserService) {
|
|
136
|
+
|
|
137
|
+
@GetMapping("/{id}")
|
|
138
|
+
fun getUser(@PathVariable id: UUID): ResponseEntity<UserResponse> {
|
|
139
|
+
return when (val result = userService.getById(id)) {
|
|
140
|
+
is UserResult.Found -> ResponseEntity.ok(result.user.toResponse())
|
|
141
|
+
is UserResult.NotFound -> ResponseEntity.notFound().build()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@PostMapping
|
|
146
|
+
fun createUser(
|
|
147
|
+
@Valid @RequestBody request: CreateUserRequest
|
|
148
|
+
): ResponseEntity<UserResponse> {
|
|
149
|
+
val result = userService.create(request)
|
|
150
|
+
return ResponseEntity.status(HttpStatus.CREATED).body(result.toResponse())
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Rules:
|
|
156
|
+
|
|
157
|
+
- One controller per feature
|
|
158
|
+
- No business logic in controllers — delegate to service
|
|
159
|
+
- Use `@Valid` on request body params
|
|
160
|
+
- Return `ResponseEntity<T>` for explicit HTTP status control
|
|
161
|
+
|
|
162
|
+
### Service
|
|
163
|
+
|
|
164
|
+
```kotlin
|
|
165
|
+
@Service
|
|
166
|
+
@Transactional
|
|
167
|
+
class UserService(private val userRepository: UserRepository) {
|
|
168
|
+
|
|
169
|
+
@Transactional(readOnly = true)
|
|
170
|
+
fun getById(id: UUID): UserResult {
|
|
171
|
+
val user = userRepository.findById(id).orElse(null)
|
|
172
|
+
?: return UserResult.NotFound(id)
|
|
173
|
+
return UserResult.Found(user)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fun create(request: CreateUserRequest): User {
|
|
177
|
+
if (userRepository.existsByEmail(request.email)) {
|
|
178
|
+
throw UserConflictException(request.email)
|
|
179
|
+
}
|
|
180
|
+
return userRepository.save(User(email = request.email, name = request.name))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Rules:
|
|
186
|
+
|
|
187
|
+
- All business logic lives here
|
|
188
|
+
- `@Transactional` at class level, `readOnly = true` for queries
|
|
189
|
+
- Never `@Transactional` on private methods — Spring proxy does not intercept
|
|
190
|
+
them
|
|
191
|
+
- Never inject repositories into controllers
|
|
192
|
+
|
|
193
|
+
### Repository
|
|
194
|
+
|
|
195
|
+
```kotlin
|
|
196
|
+
@Repository
|
|
197
|
+
interface UserRepository : JpaRepository<User, UUID> {
|
|
198
|
+
fun findByEmail(email: String): User?
|
|
199
|
+
fun existsByEmail(email: String): Boolean
|
|
200
|
+
|
|
201
|
+
@Query("SELECT u FROM User u WHERE u.role = :role AND u.active = true")
|
|
202
|
+
fun findActiveByRole(@Param("role") role: UserRole): List<User>
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Rules:
|
|
207
|
+
|
|
208
|
+
- Extend `JpaRepository<Entity, IdType>` or `CrudRepository`
|
|
209
|
+
- Derived query methods for simple lookups
|
|
210
|
+
- `@Query` with JPQL for complex queries
|
|
211
|
+
- Native SQL only when JPQL cannot express it
|
|
212
|
+
|
|
213
|
+
### Entity
|
|
214
|
+
|
|
215
|
+
```kotlin
|
|
216
|
+
@Entity
|
|
217
|
+
@Table(
|
|
218
|
+
name = "users",
|
|
219
|
+
indexes = [Index(columnList = "email", unique = true)]
|
|
220
|
+
)
|
|
221
|
+
class User(
|
|
222
|
+
@Id
|
|
223
|
+
@GeneratedValue(strategy = GenerationType.UUID)
|
|
224
|
+
val id: UUID = UUID.randomUUID(),
|
|
225
|
+
|
|
226
|
+
@Column(name = "email", nullable = false, unique = true)
|
|
227
|
+
var email: String,
|
|
228
|
+
|
|
229
|
+
@Column(name = "name", nullable = false)
|
|
230
|
+
var name: String,
|
|
231
|
+
|
|
232
|
+
@Column(name = "created_at", nullable = false, updatable = false)
|
|
233
|
+
@CreatedDate
|
|
234
|
+
val createdAt: Instant = Instant.now()
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Rules:
|
|
239
|
+
|
|
240
|
+
- `@Table` with explicit name — never rely on inferred names
|
|
241
|
+
- `@Column(nullable = false)` always explicit
|
|
242
|
+
- UUIDs as PKs with `GenerationType.UUID`
|
|
243
|
+
- Kotlin data class with JPA requires the Kotlin JPA plugin (adds no-arg
|
|
244
|
+
constructor)
|
|
245
|
+
- `equals`/`hashCode` based only on `id`, not all fields — or omit and use
|
|
246
|
+
reference equality
|
|
247
|
+
|
|
248
|
+
### DTO Separation
|
|
249
|
+
|
|
250
|
+
Never expose `@Entity` directly in API responses. Always map to a DTO.
|
|
251
|
+
|
|
252
|
+
```kotlin
|
|
253
|
+
// bad
|
|
254
|
+
@GetMapping("/{id}")
|
|
255
|
+
fun getUser(@PathVariable id: UUID): User // exposes entity internals
|
|
256
|
+
|
|
257
|
+
// good
|
|
258
|
+
@GetMapping("/{id}")
|
|
259
|
+
fun getUser(@PathVariable id: UUID): UserResponse // controlled surface
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Validation
|
|
263
|
+
|
|
264
|
+
Validate at the DTO layer, not the service layer.
|
|
265
|
+
|
|
266
|
+
```kotlin
|
|
267
|
+
data class CreateUserRequest(
|
|
268
|
+
@field:NotBlank(message = "Email is required")
|
|
269
|
+
@field:Email(message = "Email must be valid")
|
|
270
|
+
val email: String,
|
|
271
|
+
|
|
272
|
+
@field:NotBlank(message = "Name is required")
|
|
273
|
+
@field:Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
|
|
274
|
+
val name: String
|
|
275
|
+
)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Use `@field:` prefix on annotations — Kotlin applies annotations to the property
|
|
279
|
+
by default, not the backing field.
|
|
280
|
+
|
|
281
|
+
## Error Handling
|
|
282
|
+
|
|
283
|
+
```kotlin
|
|
284
|
+
@ControllerAdvice
|
|
285
|
+
class GlobalExceptionHandler {
|
|
286
|
+
|
|
287
|
+
@ExceptionHandler(UserNotFoundException::class)
|
|
288
|
+
fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ErrorResponse> {
|
|
289
|
+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
|
290
|
+
.body(ErrorResponse(error = ex.message ?: "User not found", code = "USER_NOT_FOUND"))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@ExceptionHandler(MethodArgumentNotValidException::class)
|
|
294
|
+
fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
|
|
295
|
+
val message = ex.bindingResult.fieldErrors
|
|
296
|
+
.joinToString(", ") { "${it.field}: ${it.defaultMessage}" }
|
|
297
|
+
return ResponseEntity.badRequest()
|
|
298
|
+
.body(ErrorResponse(error = message, code = "VALIDATION_ERROR"))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
data class ErrorResponse(val error: String, val code: String)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Rules:
|
|
306
|
+
|
|
307
|
+
- One `@ControllerAdvice` — do not scatter `@ExceptionHandler` across
|
|
308
|
+
controllers
|
|
309
|
+
- Typed domain exceptions — never throw `RuntimeException` directly
|
|
310
|
+
- Consistent error body: `{ "error": "...", "code": "..." }`
|
|
311
|
+
- Never expose stack traces in responses
|
|
312
|
+
|
|
313
|
+
## Testing
|
|
314
|
+
|
|
315
|
+
**Unit tests with MockK.**
|
|
316
|
+
|
|
317
|
+
```kotlin
|
|
318
|
+
@ExtendWith(MockKExtension::class)
|
|
319
|
+
class UserServiceTest {
|
|
320
|
+
|
|
321
|
+
@MockK
|
|
322
|
+
private lateinit var userRepository: UserRepository
|
|
323
|
+
|
|
324
|
+
private lateinit var userService: UserService
|
|
325
|
+
|
|
326
|
+
@BeforeEach
|
|
327
|
+
fun setUp() {
|
|
328
|
+
userService = UserService(userRepository)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@Test
|
|
332
|
+
fun `should return NotFound when user does not exist`() {
|
|
333
|
+
// Arrange
|
|
334
|
+
val id = UUID.randomUUID()
|
|
335
|
+
every { userRepository.findById(id) } returns Optional.empty()
|
|
336
|
+
|
|
337
|
+
// Act
|
|
338
|
+
val result = userService.getById(id)
|
|
339
|
+
|
|
340
|
+
// Assert
|
|
341
|
+
assertThat(result).isInstanceOf(UserResult.NotFound::class.java)
|
|
342
|
+
verify(exactly = 1) { userRepository.findById(id) }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Integration tests with MockMvc.**
|
|
348
|
+
|
|
349
|
+
```kotlin
|
|
350
|
+
@SpringBootTest
|
|
351
|
+
@AutoConfigureMockMvc
|
|
352
|
+
class UserControllerTest {
|
|
353
|
+
|
|
354
|
+
@Autowired
|
|
355
|
+
private lateinit var mockMvc: MockMvc
|
|
356
|
+
|
|
357
|
+
@Test
|
|
358
|
+
fun `should return 404 when user is not found`() {
|
|
359
|
+
mockMvc.perform(get("/api/v1/users/${UUID.randomUUID()}"))
|
|
360
|
+
.andExpect(status().isNotFound)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Repository tests.**
|
|
366
|
+
|
|
367
|
+
```kotlin
|
|
368
|
+
@DataJpaTest
|
|
369
|
+
class UserRepositoryTest {
|
|
370
|
+
|
|
371
|
+
@Autowired
|
|
372
|
+
private lateinit var userRepository: UserRepository
|
|
373
|
+
|
|
374
|
+
@Test
|
|
375
|
+
fun `should find user by email`() {
|
|
376
|
+
val user = userRepository.save(User(email = "test@example.com", name = "Test"))
|
|
377
|
+
val found = userRepository.findByEmail("test@example.com")
|
|
378
|
+
assertThat(found).isEqualTo(user)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Rules:
|
|
384
|
+
|
|
385
|
+
- Use MockK, not Mockito — the Kotlin API is cleaner and null-safe
|
|
386
|
+
- Do not use `@MockBean` where `mockk<T>()` is sufficient
|
|
387
|
+
- `@DataJpaTest` with H2 for repository tests (fast)
|
|
388
|
+
- `@SpringBootTest` + `@AutoConfigureMockMvc` for controller tests
|
|
389
|
+
|
|
390
|
+
## Security Rules
|
|
391
|
+
|
|
392
|
+
- No stack traces in API responses — only `@ControllerAdvice` with controlled
|
|
393
|
+
messages
|
|
394
|
+
- Validate all input in DTO layer — service layer should receive
|
|
395
|
+
already-validated data
|
|
396
|
+
- `application-prod.yml` must not be committed — use environment variables or a
|
|
397
|
+
secrets manager
|
|
398
|
+
- Credentials, tokens, and keys are never hardcoded — use `@Value` with
|
|
399
|
+
externalized config
|