omgkit 2.1.0 → 2.2.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/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,70 +1,563 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spring
|
|
3
|
-
description: Spring Boot development
|
|
3
|
+
description: Enterprise Spring Boot development with JPA, security, testing, and microservices patterns
|
|
4
|
+
category: frameworks
|
|
5
|
+
triggers:
|
|
6
|
+
- spring
|
|
7
|
+
- spring boot
|
|
8
|
+
- java spring
|
|
9
|
+
- spring mvc
|
|
10
|
+
- spring security
|
|
11
|
+
- jpa
|
|
12
|
+
- hibernate
|
|
13
|
+
- java api
|
|
4
14
|
---
|
|
5
15
|
|
|
6
|
-
# Spring Boot
|
|
16
|
+
# Spring Boot
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
Enterprise-grade **Spring Boot development** following industry best practices. This skill covers Spring Data JPA, Spring Security, REST APIs, validation, testing patterns, and microservices configurations used by top engineering teams.
|
|
19
|
+
|
|
20
|
+
## Purpose
|
|
21
|
+
|
|
22
|
+
Build scalable Java applications with confidence:
|
|
23
|
+
|
|
24
|
+
- Design clean architectures with Spring Boot
|
|
25
|
+
- Implement REST APIs with proper validation
|
|
26
|
+
- Use Spring Data JPA for database operations
|
|
27
|
+
- Handle authentication with Spring Security
|
|
28
|
+
- Write comprehensive tests with JUnit and MockMvc
|
|
29
|
+
- Deploy production-ready applications
|
|
30
|
+
- Build microservices with Spring Cloud
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
### 1. Entity Design and Relationships
|
|
35
|
+
|
|
36
|
+
```java
|
|
37
|
+
// src/main/java/com/example/model/User.java
|
|
38
|
+
package com.example.model;
|
|
39
|
+
|
|
40
|
+
import jakarta.persistence.*;
|
|
41
|
+
import lombok.*;
|
|
42
|
+
import org.hibernate.annotations.CreationTimestamp;
|
|
43
|
+
import org.hibernate.annotations.UpdateTimestamp;
|
|
44
|
+
import org.hibernate.annotations.UuidGenerator;
|
|
45
|
+
|
|
46
|
+
import java.time.LocalDateTime;
|
|
47
|
+
import java.util.HashSet;
|
|
48
|
+
import java.util.Set;
|
|
49
|
+
import java.util.UUID;
|
|
50
|
+
|
|
51
|
+
@Entity
|
|
52
|
+
@Table(name = "users")
|
|
53
|
+
@Getter @Setter
|
|
54
|
+
@NoArgsConstructor @AllArgsConstructor
|
|
55
|
+
@Builder
|
|
56
|
+
public class User {
|
|
57
|
+
@Id
|
|
58
|
+
@UuidGenerator
|
|
59
|
+
private UUID id;
|
|
60
|
+
|
|
61
|
+
@Column(nullable = false, unique = true)
|
|
62
|
+
private String email;
|
|
63
|
+
|
|
64
|
+
@Column(nullable = false)
|
|
65
|
+
private String name;
|
|
66
|
+
|
|
67
|
+
@Column(nullable = false)
|
|
68
|
+
private String password;
|
|
69
|
+
|
|
70
|
+
@Enumerated(EnumType.STRING)
|
|
71
|
+
@Builder.Default
|
|
72
|
+
private UserRole role = UserRole.USER;
|
|
73
|
+
|
|
74
|
+
@Builder.Default
|
|
75
|
+
private Boolean isActive = true;
|
|
76
|
+
|
|
77
|
+
@CreationTimestamp
|
|
78
|
+
private LocalDateTime createdAt;
|
|
79
|
+
|
|
80
|
+
@UpdateTimestamp
|
|
81
|
+
private LocalDateTime updatedAt;
|
|
82
|
+
|
|
83
|
+
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
|
|
84
|
+
@Builder.Default
|
|
85
|
+
private Set<Membership> memberships = new HashSet<>();
|
|
86
|
+
|
|
87
|
+
public boolean isAdmin() {
|
|
88
|
+
return this.role == UserRole.ADMIN;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
// src/main/java/com/example/model/Organization.java
|
|
94
|
+
@Entity
|
|
95
|
+
@Table(name = "organizations")
|
|
96
|
+
@Getter @Setter
|
|
97
|
+
@NoArgsConstructor @AllArgsConstructor
|
|
98
|
+
@Builder
|
|
99
|
+
public class Organization {
|
|
100
|
+
@Id
|
|
101
|
+
@UuidGenerator
|
|
102
|
+
private UUID id;
|
|
103
|
+
|
|
104
|
+
@Column(nullable = false)
|
|
105
|
+
private String name;
|
|
106
|
+
|
|
107
|
+
@Column(nullable = false, unique = true)
|
|
108
|
+
private String slug;
|
|
109
|
+
|
|
110
|
+
@ManyToOne(fetch = FetchType.LAZY)
|
|
111
|
+
@JoinColumn(name = "owner_id", nullable = false)
|
|
112
|
+
private User owner;
|
|
113
|
+
|
|
114
|
+
@OneToMany(mappedBy = "organization", cascade = CascadeType.ALL)
|
|
115
|
+
@Builder.Default
|
|
116
|
+
private Set<Membership> memberships = new HashSet<>();
|
|
117
|
+
|
|
118
|
+
@CreationTimestamp
|
|
119
|
+
private LocalDateTime createdAt;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 2. DTOs and Validation
|
|
124
|
+
|
|
125
|
+
```java
|
|
126
|
+
// src/main/java/com/example/dto/user/CreateUserRequest.java
|
|
127
|
+
package com.example.dto.user;
|
|
128
|
+
|
|
129
|
+
import jakarta.validation.constraints.*;
|
|
130
|
+
import lombok.Data;
|
|
131
|
+
|
|
132
|
+
@Data
|
|
133
|
+
public class CreateUserRequest {
|
|
134
|
+
@NotBlank(message = "Name is required")
|
|
135
|
+
@Size(min = 2, max = 100)
|
|
136
|
+
private String name;
|
|
137
|
+
|
|
138
|
+
@NotBlank(message = "Email is required")
|
|
139
|
+
@Email(message = "Invalid email format")
|
|
140
|
+
private String email;
|
|
141
|
+
|
|
142
|
+
@NotBlank(message = "Password is required")
|
|
143
|
+
@Size(min = 8, max = 128)
|
|
144
|
+
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).*$",
|
|
145
|
+
message = "Password must contain uppercase, lowercase, number and special character")
|
|
146
|
+
private String password;
|
|
147
|
+
|
|
148
|
+
private String role;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
// src/main/java/com/example/dto/user/UserResponse.java
|
|
153
|
+
@Data
|
|
154
|
+
@Builder
|
|
155
|
+
public class UserResponse {
|
|
156
|
+
private UUID id;
|
|
157
|
+
private String email;
|
|
158
|
+
private String name;
|
|
159
|
+
private UserRole role;
|
|
160
|
+
private Boolean isActive;
|
|
161
|
+
private LocalDateTime createdAt;
|
|
162
|
+
|
|
163
|
+
public static UserResponse fromEntity(User user) {
|
|
164
|
+
return UserResponse.builder()
|
|
165
|
+
.id(user.getId())
|
|
166
|
+
.email(user.getEmail())
|
|
167
|
+
.name(user.getName())
|
|
168
|
+
.role(user.getRole())
|
|
169
|
+
.isActive(user.getIsActive())
|
|
170
|
+
.createdAt(user.getCreatedAt())
|
|
171
|
+
.build();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
// src/main/java/com/example/dto/common/PaginatedResponse.java
|
|
177
|
+
@Data
|
|
178
|
+
@Builder
|
|
179
|
+
public class PaginatedResponse<T> {
|
|
180
|
+
private List<T> data;
|
|
181
|
+
private int page;
|
|
182
|
+
private int limit;
|
|
183
|
+
private long total;
|
|
184
|
+
private int totalPages;
|
|
185
|
+
private boolean hasMore;
|
|
186
|
+
|
|
187
|
+
public static <T, E> PaginatedResponse<T> fromPage(
|
|
188
|
+
Page<E> page,
|
|
189
|
+
java.util.function.Function<E, T> mapper
|
|
190
|
+
) {
|
|
191
|
+
return PaginatedResponse.<T>builder()
|
|
192
|
+
.data(page.getContent().stream().map(mapper).toList())
|
|
193
|
+
.page(page.getNumber() + 1)
|
|
194
|
+
.limit(page.getSize())
|
|
195
|
+
.total(page.getTotalElements())
|
|
196
|
+
.totalPages(page.getTotalPages())
|
|
197
|
+
.hasMore(page.hasNext())
|
|
198
|
+
.build();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 3. Repositories
|
|
204
|
+
|
|
205
|
+
```java
|
|
206
|
+
// src/main/java/com/example/repository/UserRepository.java
|
|
207
|
+
@Repository
|
|
208
|
+
public interface UserRepository extends JpaRepository<User, UUID> {
|
|
209
|
+
Optional<User> findByEmail(String email);
|
|
210
|
+
|
|
211
|
+
boolean existsByEmail(String email);
|
|
212
|
+
|
|
213
|
+
@Query("""
|
|
214
|
+
SELECT u FROM User u
|
|
215
|
+
WHERE u.isActive = true
|
|
216
|
+
AND (:search IS NULL OR LOWER(u.name) LIKE LOWER(CONCAT('%', :search, '%'))
|
|
217
|
+
OR LOWER(u.email) LIKE LOWER(CONCAT('%', :search, '%')))
|
|
218
|
+
AND (:role IS NULL OR u.role = :role)
|
|
219
|
+
""")
|
|
220
|
+
Page<User> findAllWithFilters(
|
|
221
|
+
@Param("search") String search,
|
|
222
|
+
@Param("role") UserRole role,
|
|
223
|
+
Pageable pageable
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 4. Services
|
|
229
|
+
|
|
230
|
+
```java
|
|
231
|
+
// src/main/java/com/example/service/UserService.java
|
|
232
|
+
@Service
|
|
233
|
+
@RequiredArgsConstructor
|
|
234
|
+
@Transactional(readOnly = true)
|
|
235
|
+
public class UserService {
|
|
236
|
+
private final UserRepository userRepository;
|
|
237
|
+
private final PasswordEncoder passwordEncoder;
|
|
238
|
+
|
|
239
|
+
public PaginatedResponse<UserResponse> findAll(String search, String role, int page, int limit) {
|
|
240
|
+
UserRole userRole = role != null ? UserRole.valueOf(role.toUpperCase()) : null;
|
|
241
|
+
PageRequest pageRequest = PageRequest.of(page - 1, limit, Sort.by("createdAt").descending());
|
|
242
|
+
|
|
243
|
+
Page<User> users = userRepository.findAllWithFilters(search, userRole, pageRequest);
|
|
244
|
+
|
|
245
|
+
return PaginatedResponse.fromPage(users, UserResponse::fromEntity);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public UserResponse findById(UUID id) {
|
|
249
|
+
User user = userRepository.findById(id)
|
|
250
|
+
.orElseThrow(() -> new NotFoundException("User not found with id: " + id));
|
|
251
|
+
return UserResponse.fromEntity(user);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@Transactional
|
|
255
|
+
public UserResponse create(CreateUserRequest request) {
|
|
256
|
+
if (userRepository.existsByEmail(request.getEmail())) {
|
|
257
|
+
throw new ConflictException("Email already in use");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
User user = User.builder()
|
|
261
|
+
.email(request.getEmail().toLowerCase())
|
|
262
|
+
.name(request.getName())
|
|
263
|
+
.password(passwordEncoder.encode(request.getPassword()))
|
|
264
|
+
.role(request.getRole() != null ? UserRole.valueOf(request.getRole().toUpperCase()) : UserRole.USER)
|
|
265
|
+
.build();
|
|
266
|
+
|
|
267
|
+
return UserResponse.fromEntity(userRepository.save(user));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@Transactional
|
|
271
|
+
public UserResponse update(UUID id, UpdateUserRequest request) {
|
|
272
|
+
User user = userRepository.findById(id)
|
|
273
|
+
.orElseThrow(() -> new NotFoundException("User not found"));
|
|
274
|
+
|
|
275
|
+
if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
|
|
276
|
+
if (userRepository.existsByEmail(request.getEmail())) {
|
|
277
|
+
throw new ConflictException("Email already in use");
|
|
278
|
+
}
|
|
279
|
+
user.setEmail(request.getEmail().toLowerCase());
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (request.getName() != null) user.setName(request.getName());
|
|
283
|
+
if (request.getIsActive() != null) user.setIsActive(request.getIsActive());
|
|
284
|
+
|
|
285
|
+
return UserResponse.fromEntity(userRepository.save(user));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@Transactional
|
|
289
|
+
public void delete(UUID id) {
|
|
290
|
+
if (!userRepository.existsById(id)) {
|
|
291
|
+
throw new NotFoundException("User not found");
|
|
292
|
+
}
|
|
293
|
+
userRepository.deleteById(id);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### 5. Controllers
|
|
9
299
|
|
|
10
|
-
### Controller
|
|
11
300
|
```java
|
|
301
|
+
// src/main/java/com/example/controller/UserController.java
|
|
12
302
|
@RestController
|
|
13
|
-
@RequestMapping("/api/users")
|
|
303
|
+
@RequestMapping("/api/v1/users")
|
|
304
|
+
@RequiredArgsConstructor
|
|
305
|
+
@Tag(name = "Users", description = "User management endpoints")
|
|
14
306
|
public class UserController {
|
|
15
|
-
|
|
16
|
-
private UserService userService;
|
|
307
|
+
private final UserService userService;
|
|
17
308
|
|
|
18
309
|
@GetMapping
|
|
19
|
-
|
|
20
|
-
|
|
310
|
+
@PreAuthorize("hasRole('ADMIN')")
|
|
311
|
+
@Operation(summary = "List all users")
|
|
312
|
+
public ResponseEntity<PaginatedResponse<UserResponse>> findAll(
|
|
313
|
+
@RequestParam(required = false) String search,
|
|
314
|
+
@RequestParam(required = false) String role,
|
|
315
|
+
@RequestParam(defaultValue = "1") int page,
|
|
316
|
+
@RequestParam(defaultValue = "20") int limit
|
|
317
|
+
) {
|
|
318
|
+
return ResponseEntity.ok(userService.findAll(search, role, page, limit));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@GetMapping("/me")
|
|
322
|
+
@Operation(summary = "Get current user profile")
|
|
323
|
+
public ResponseEntity<UserResponse> getCurrentUser(@AuthenticationPrincipal User currentUser) {
|
|
324
|
+
return ResponseEntity.ok(userService.findById(currentUser.getId()));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@GetMapping("/{id}")
|
|
328
|
+
@PreAuthorize("hasRole('ADMIN')")
|
|
329
|
+
public ResponseEntity<UserResponse> findById(@PathVariable UUID id) {
|
|
330
|
+
return ResponseEntity.ok(userService.findById(id));
|
|
21
331
|
}
|
|
22
332
|
|
|
23
333
|
@PostMapping
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return ResponseEntity.status(HttpStatus.CREATED).body(
|
|
334
|
+
@PreAuthorize("hasRole('ADMIN')")
|
|
335
|
+
public ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest request) {
|
|
336
|
+
return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(request));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@PatchMapping("/{id}")
|
|
340
|
+
@PreAuthorize("hasRole('ADMIN')")
|
|
341
|
+
public ResponseEntity<UserResponse> update(@PathVariable UUID id, @Valid @RequestBody UpdateUserRequest request) {
|
|
342
|
+
return ResponseEntity.ok(userService.update(id, request));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@DeleteMapping("/{id}")
|
|
346
|
+
@PreAuthorize("hasRole('ADMIN')")
|
|
347
|
+
@ResponseStatus(HttpStatus.NO_CONTENT)
|
|
348
|
+
public void delete(@PathVariable UUID id) {
|
|
349
|
+
userService.delete(id);
|
|
27
350
|
}
|
|
28
351
|
}
|
|
29
352
|
```
|
|
30
353
|
|
|
31
|
-
###
|
|
354
|
+
### 6. Security Configuration
|
|
355
|
+
|
|
32
356
|
```java
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
357
|
+
// src/main/java/com/example/config/SecurityConfig.java
|
|
358
|
+
@Configuration
|
|
359
|
+
@EnableWebSecurity
|
|
360
|
+
@EnableMethodSecurity
|
|
361
|
+
@RequiredArgsConstructor
|
|
362
|
+
public class SecurityConfig {
|
|
363
|
+
private final JwtAuthenticationFilter jwtAuthFilter;
|
|
364
|
+
|
|
365
|
+
@Bean
|
|
366
|
+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
|
367
|
+
http
|
|
368
|
+
.csrf(csrf -> csrf.disable())
|
|
369
|
+
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
370
|
+
.authorizeHttpRequests(auth -> auth
|
|
371
|
+
.requestMatchers("/api/v1/auth/**").permitAll()
|
|
372
|
+
.requestMatchers("/actuator/health").permitAll()
|
|
373
|
+
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
|
374
|
+
.anyRequest().authenticated()
|
|
375
|
+
)
|
|
376
|
+
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
|
377
|
+
|
|
378
|
+
return http.build();
|
|
379
|
+
}
|
|
37
380
|
|
|
38
|
-
|
|
39
|
-
|
|
381
|
+
@Bean
|
|
382
|
+
public PasswordEncoder passwordEncoder() {
|
|
383
|
+
return new BCryptPasswordEncoder();
|
|
40
384
|
}
|
|
41
385
|
}
|
|
42
386
|
```
|
|
43
387
|
|
|
44
|
-
###
|
|
388
|
+
### 7. Testing
|
|
389
|
+
|
|
45
390
|
```java
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
391
|
+
// src/test/java/com/example/service/UserServiceTest.java
|
|
392
|
+
@ExtendWith(MockitoExtension.class)
|
|
393
|
+
class UserServiceTest {
|
|
394
|
+
@Mock
|
|
395
|
+
private UserRepository userRepository;
|
|
396
|
+
|
|
397
|
+
@Mock
|
|
398
|
+
private PasswordEncoder passwordEncoder;
|
|
399
|
+
|
|
400
|
+
@InjectMocks
|
|
401
|
+
private UserService userService;
|
|
402
|
+
|
|
403
|
+
private User testUser;
|
|
404
|
+
|
|
405
|
+
@BeforeEach
|
|
406
|
+
void setUp() {
|
|
407
|
+
testUser = User.builder()
|
|
408
|
+
.id(UUID.randomUUID())
|
|
409
|
+
.email("test@example.com")
|
|
410
|
+
.name("Test User")
|
|
411
|
+
.role(UserRole.USER)
|
|
412
|
+
.isActive(true)
|
|
413
|
+
.build();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
@Test
|
|
417
|
+
void findById_ShouldReturnUser_WhenUserExists() {
|
|
418
|
+
when(userRepository.findById(testUser.getId())).thenReturn(Optional.of(testUser));
|
|
419
|
+
|
|
420
|
+
UserResponse result = userService.findById(testUser.getId());
|
|
421
|
+
|
|
422
|
+
assertThat(result.getEmail()).isEqualTo(testUser.getEmail());
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@Test
|
|
426
|
+
void findById_ShouldThrowNotFoundException_WhenUserNotFound() {
|
|
427
|
+
UUID id = UUID.randomUUID();
|
|
428
|
+
when(userRepository.findById(id)).thenReturn(Optional.empty());
|
|
429
|
+
|
|
430
|
+
assertThatThrownBy(() -> userService.findById(id))
|
|
431
|
+
.isInstanceOf(NotFoundException.class);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@Test
|
|
435
|
+
void create_ShouldCreateUser_WhenEmailIsUnique() {
|
|
436
|
+
CreateUserRequest request = new CreateUserRequest();
|
|
437
|
+
request.setEmail("new@example.com");
|
|
438
|
+
request.setName("New User");
|
|
439
|
+
request.setPassword("Password123!");
|
|
440
|
+
|
|
441
|
+
when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
|
|
442
|
+
when(passwordEncoder.encode(request.getPassword())).thenReturn("encoded");
|
|
443
|
+
when(userRepository.save(any(User.class))).thenReturn(testUser);
|
|
444
|
+
|
|
445
|
+
UserResponse result = userService.create(request);
|
|
446
|
+
|
|
447
|
+
assertThat(result).isNotNull();
|
|
448
|
+
verify(userRepository).save(any(User.class));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
@Test
|
|
452
|
+
void create_ShouldThrowConflictException_WhenEmailExists() {
|
|
453
|
+
CreateUserRequest request = new CreateUserRequest();
|
|
454
|
+
request.setEmail("existing@example.com");
|
|
455
|
+
|
|
456
|
+
when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);
|
|
457
|
+
|
|
458
|
+
assertThatThrownBy(() -> userService.create(request))
|
|
459
|
+
.isInstanceOf(ConflictException.class);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
// src/test/java/com/example/controller/UserControllerTest.java
|
|
465
|
+
@WebMvcTest(UserController.class)
|
|
466
|
+
class UserControllerTest {
|
|
467
|
+
@Autowired
|
|
468
|
+
private MockMvc mockMvc;
|
|
469
|
+
|
|
470
|
+
@Autowired
|
|
471
|
+
private ObjectMapper objectMapper;
|
|
472
|
+
|
|
473
|
+
@MockBean
|
|
474
|
+
private UserService userService;
|
|
475
|
+
|
|
476
|
+
@Test
|
|
477
|
+
@WithMockUser(roles = "ADMIN")
|
|
478
|
+
void create_ShouldReturn201_WhenValidRequest() throws Exception {
|
|
479
|
+
CreateUserRequest request = new CreateUserRequest();
|
|
480
|
+
request.setEmail("test@example.com");
|
|
481
|
+
request.setName("Test User");
|
|
482
|
+
request.setPassword("Password123!");
|
|
483
|
+
|
|
484
|
+
UserResponse response = UserResponse.builder()
|
|
485
|
+
.email("test@example.com")
|
|
486
|
+
.name("Test User")
|
|
487
|
+
.build();
|
|
488
|
+
|
|
489
|
+
when(userService.create(any())).thenReturn(response);
|
|
490
|
+
|
|
491
|
+
mockMvc.perform(post("/api/v1/users")
|
|
492
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
493
|
+
.content(objectMapper.writeValueAsString(request)))
|
|
494
|
+
.andExpect(status().isCreated())
|
|
495
|
+
.andExpect(jsonPath("$.email").value("test@example.com"));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
@Test
|
|
499
|
+
@WithMockUser(roles = "USER")
|
|
500
|
+
void findAll_ShouldReturn403_WhenNotAdmin() throws Exception {
|
|
501
|
+
mockMvc.perform(get("/api/v1/users"))
|
|
502
|
+
.andExpect(status().isForbidden());
|
|
503
|
+
}
|
|
49
504
|
}
|
|
50
505
|
```
|
|
51
506
|
|
|
52
|
-
|
|
507
|
+
## Use Cases
|
|
508
|
+
|
|
509
|
+
### Caching with Redis
|
|
510
|
+
|
|
53
511
|
```java
|
|
54
|
-
@
|
|
55
|
-
@
|
|
56
|
-
public class
|
|
57
|
-
|
|
58
|
-
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
59
|
-
private Long id;
|
|
512
|
+
@Service
|
|
513
|
+
@RequiredArgsConstructor
|
|
514
|
+
public class CachedUserService {
|
|
515
|
+
private final UserRepository userRepository;
|
|
60
516
|
|
|
61
|
-
@
|
|
62
|
-
|
|
517
|
+
@Cacheable(value = "users", key = "#id")
|
|
518
|
+
public UserResponse findById(UUID id) {
|
|
519
|
+
User user = userRepository.findById(id)
|
|
520
|
+
.orElseThrow(() -> new NotFoundException("User not found"));
|
|
521
|
+
return UserResponse.fromEntity(user);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@CacheEvict(value = "users", key = "#id")
|
|
525
|
+
public void evictCache(UUID id) {}
|
|
63
526
|
}
|
|
64
527
|
```
|
|
65
528
|
|
|
66
529
|
## Best Practices
|
|
530
|
+
|
|
531
|
+
### Do's
|
|
532
|
+
|
|
533
|
+
- Use UUID primary keys for public APIs
|
|
534
|
+
- Use DTOs for request/response separation
|
|
535
|
+
- Use Spring Data JPA specifications for complex queries
|
|
536
|
+
- Use @Transactional appropriately
|
|
537
|
+
- Use proper validation annotations
|
|
538
|
+
- Write unit and integration tests
|
|
539
|
+
- Use Spring Security for authentication
|
|
540
|
+
- Use proper exception handling
|
|
67
541
|
- Use constructor injection
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
542
|
+
- Document APIs with OpenAPI
|
|
543
|
+
|
|
544
|
+
### Don'ts
|
|
545
|
+
|
|
546
|
+
- Don't expose entities directly in APIs
|
|
547
|
+
- Don't use field injection
|
|
548
|
+
- Don't ignore N+1 query problems
|
|
549
|
+
- Don't skip validation
|
|
550
|
+
- Don't hardcode configuration
|
|
551
|
+
- Don't ignore security headers
|
|
552
|
+
- Don't skip error handling
|
|
553
|
+
- Don't use raw SQL without parameterization
|
|
554
|
+
- Don't forget to handle exceptions globally
|
|
555
|
+
- Don't skip testing
|
|
556
|
+
|
|
557
|
+
## References
|
|
558
|
+
|
|
559
|
+
- [Spring Boot Documentation](https://spring.io/projects/spring-boot)
|
|
560
|
+
- [Spring Data JPA](https://spring.io/projects/spring-data-jpa)
|
|
561
|
+
- [Spring Security](https://spring.io/projects/spring-security)
|
|
562
|
+
- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/)
|
|
563
|
+
- [Baeldung Spring Tutorials](https://www.baeldung.com/spring-boot)
|