diff --git a/skills/java-coding-standards/SKILL.md b/skills/java-coding-standards/SKILL.md new file mode 100644 index 0000000..9a03a41 --- /dev/null +++ b/skills/java-coding-standards/SKILL.md @@ -0,0 +1,138 @@ +--- +name: java-coding-standards +description: Java coding standards for Spring Boot services: naming, immutability, Optional usage, streams, exceptions, generics, and project layout. +--- + +# Java Coding Standards + +Standards for readable, maintainable Java (17+) code in Spring Boot services. + +## Core Principles + +- Prefer clarity over cleverness +- Immutable by default; minimize shared mutable state +- Fail fast with meaningful exceptions +- Consistent naming and package structure + +## Naming + +```java +// ✅ Classes/Records: PascalCase +public class MarketService {} +public record Money(BigDecimal amount, Currency currency) {} + +// ✅ Methods/fields: camelCase +private final MarketRepository marketRepository; +public Market findBySlug(String slug) {} + +// ✅ Constants: UPPER_SNAKE_CASE +private static final int MAX_PAGE_SIZE = 100; +``` + +## Immutability + +```java +// ✅ Favor records and final fields +public record MarketDto(Long id, String name, MarketStatus status) {} + +public class Market { + private final Long id; + private final String name; + // getters only, no setters +} +``` + +## Optional Usage + +```java +// ✅ Return Optional from find* methods +Optional market = marketRepository.findBySlug(slug); + +// ✅ Map/flatMap instead of get() +return market + .map(MarketResponse::from) + .orElseThrow(() -> new EntityNotFoundException("Market not found")); +``` + +## Streams Best Practices + +```java +// ✅ Use streams for transformations, keep pipelines short +List names = markets.stream() + .map(Market::name) + .filter(Objects::nonNull) + .toList(); + +// ❌ Avoid complex nested streams; prefer loops for clarity +``` + +## Exceptions + +- Use unchecked exceptions for domain errors; wrap technical exceptions with context +- Create domain-specific exceptions (e.g., `MarketNotFoundException`) +- Avoid broad `catch (Exception ex)` unless rethrowing/logging centrally + +```java +throw new MarketNotFoundException(slug); +``` + +## Generics and Type Safety + +- Avoid raw types; declare generic parameters +- Prefer bounded generics for reusable utilities + +```java +public Map indexById(Collection items) { ... } +``` + +## Project Structure (Maven/Gradle) + +``` +src/main/java/com/example/app/ + config/ + controller/ + service/ + repository/ + domain/ + dto/ + util/ +src/main/resources/ + application.yml +src/test/java/... (mirrors main) +``` + +## Formatting and Style + +- Use 2 or 4 spaces consistently (project standard) +- One public top-level type per file +- Keep methods short and focused; extract helpers +- Order members: constants, fields, constructors, public methods, protected, private + +## Code Smells to Avoid + +- Long parameter lists → use DTO/builders +- Deep nesting → early returns +- Magic numbers → named constants +- Static mutable state → prefer dependency injection +- Silent catch blocks → log and act or rethrow + +## Logging + +```java +private static final Logger log = LoggerFactory.getLogger(MarketService.class); +log.info("fetch_market slug={}", slug); +log.error("failed_fetch_market slug={}", slug, ex); +``` + +## Null Handling + +- Accept `@Nullable` only when unavoidable; otherwise use `@NonNull` +- Use Bean Validation (`@NotNull`, `@NotBlank`) on inputs + +## Testing Expectations + +- JUnit 5 + AssertJ for fluent assertions +- Mockito for mocking; avoid partial mocks where possible +- Favor deterministic tests; no hidden sleeps + +**Remember**: Keep code intentional, typed, and observable. Optimize for maintainability over micro-optimizations unless proven necessary. diff --git a/skills/jpa-patterns/SKILL.md b/skills/jpa-patterns/SKILL.md new file mode 100644 index 0000000..2bf3213 --- /dev/null +++ b/skills/jpa-patterns/SKILL.md @@ -0,0 +1,141 @@ +--- +name: jpa-patterns +description: JPA/Hibernate patterns for entity design, relationships, query optimization, transactions, auditing, indexing, pagination, and pooling in Spring Boot. +--- + +# JPA/Hibernate Patterns + +Use for data modeling, repositories, and performance tuning in Spring Boot. + +## Entity Design + +```java +@Entity +@Table(name = "markets", indexes = { + @Index(name = "idx_markets_slug", columnList = "slug", unique = true) +}) +@EntityListeners(AuditingEntityListener.class) +public class MarketEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 200) + private String name; + + @Column(nullable = false, unique = true, length = 120) + private String slug; + + @Enumerated(EnumType.STRING) + private MarketStatus status = MarketStatus.ACTIVE; + + @CreatedDate private Instant createdAt; + @LastModifiedDate private Instant updatedAt; +} +``` + +Enable auditing: +```java +@Configuration +@EnableJpaAuditing +class JpaConfig {} +``` + +## Relationships and N+1 Prevention + +```java +@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true) +private List positions = new ArrayList<>(); +``` + +- Default to lazy loading; use `JOIN FETCH` in queries when needed +- Avoid `EAGER` on collections; use DTO projections for read paths + +```java +@Query("select m from MarketEntity m left join fetch m.positions where m.id = :id") +Optional findWithPositions(@Param("id") Long id); +``` + +## Repository Patterns + +```java +public interface MarketRepository extends JpaRepository { + Optional findBySlug(String slug); + + @Query("select m from MarketEntity m where m.status = :status") + Page findByStatus(@Param("status") MarketStatus status, Pageable pageable); +} +``` + +- Use projections for lightweight queries: +```java +public interface MarketSummary { + Long getId(); + String getName(); + MarketStatus getStatus(); +} +Page findAllBy(Pageable pageable); +``` + +## Transactions + +- Annotate service methods with `@Transactional` +- Use `@Transactional(readOnly = true)` for read paths to optimize +- Choose propagation carefully; avoid long-running transactions + +```java +@Transactional +public Market updateStatus(Long id, MarketStatus status) { + MarketEntity entity = repo.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Market")); + entity.setStatus(status); + return Market.from(entity); +} +``` + +## Pagination + +```java +PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); +Page markets = repo.findByStatus(MarketStatus.ACTIVE, page); +``` + +For cursor-like pagination, include `id > :lastId` in JPQL with ordering. + +## Indexing and Performance + +- Add indexes for common filters (`status`, `slug`, foreign keys) +- Use composite indexes matching query patterns (`status, created_at`) +- Avoid `select *`; project only needed columns +- Batch writes with `saveAll` and `hibernate.jdbc.batch_size` + +## Connection Pooling (HikariCP) + +Recommended properties: +``` +spring.datasource.hikari.maximum-pool-size=20 +spring.datasource.hikari.minimum-idle=5 +spring.datasource.hikari.connection-timeout=30000 +spring.datasource.hikari.validation-timeout=5000 +``` + +For PostgreSQL LOB handling, add: +``` +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +``` + +## Caching + +- 1st-level cache is per EntityManager; avoid keeping entities across transactions +- For read-heavy entities, consider second-level cache cautiously; validate eviction strategy + +## Migrations + +- Use Flyway or Liquibase; never rely on Hibernate auto DDL in production +- Keep migrations idempotent and additive; avoid dropping columns without plan + +## Testing Data Access + +- Prefer `@DataJpaTest` with Testcontainers to mirror production +- Assert SQL efficiency using logs: set `logging.level.org.hibernate.SQL=DEBUG` and `logging.level.org.hibernate.orm.jdbc.bind=TRACE` for parameter values + +**Remember**: Keep entities lean, queries intentional, and transactions short. Prevent N+1 with fetch strategies and projections, and index for your read/write paths. diff --git a/skills/springboot-patterns/SKILL.md b/skills/springboot-patterns/SKILL.md new file mode 100644 index 0000000..2270dc9 --- /dev/null +++ b/skills/springboot-patterns/SKILL.md @@ -0,0 +1,304 @@ +--- +name: springboot-patterns +description: Spring Boot architecture patterns, REST API design, layered services, data access, caching, async processing, and logging. Use for Java Spring Boot backend work. +--- + +# Spring Boot Development Patterns + +Spring Boot architecture and API patterns for scalable, production-grade services. + +## REST API Structure + +```java +@RestController +@RequestMapping("/api/markets") +@Validated +class MarketController { + private final MarketService marketService; + + MarketController(MarketService marketService) { + this.marketService = marketService; + } + + @GetMapping + ResponseEntity> list( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + Page markets = marketService.list(PageRequest.of(page, size)); + return ResponseEntity.ok(markets.map(MarketResponse::from)); + } + + @PostMapping + ResponseEntity create(@Valid @RequestBody CreateMarketRequest request) { + Market market = marketService.create(request); + return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse.from(market)); + } +} +``` + +## Repository Pattern (Spring Data JPA) + +```java +public interface MarketRepository extends JpaRepository { + @Query("select m from MarketEntity m where m.status = :status order by m.volume desc") + List findActive(@Param("status") MarketStatus status, Pageable pageable); +} +``` + +## Service Layer with Transactions + +```java +@Service +public class MarketService { + private final MarketRepository repo; + + public MarketService(MarketRepository repo) { + this.repo = repo; + } + + @Transactional + public Market create(CreateMarketRequest request) { + MarketEntity entity = MarketEntity.from(request); + MarketEntity saved = repo.save(entity); + return Market.from(saved); + } +} +``` + +## DTOs and Validation + +```java +public record CreateMarketRequest( + @NotBlank @Size(max = 200) String name, + @NotBlank @Size(max = 2000) String description, + @NotNull @FutureOrPresent Instant endDate, + @NotEmpty List<@NotBlank String> categories) {} + +public record MarketResponse(Long id, String name, MarketStatus status) { + static MarketResponse from(Market market) { + return new MarketResponse(market.id(), market.name(), market.status()); + } +} +``` + +## Exception Handling + +```java +@ControllerAdvice +class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + ResponseEntity handleValidation(MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors().stream() + .map(e -> e.getField() + ": " + e.getDefaultMessage()) + .collect(Collectors.joining(", ")); + return ResponseEntity.badRequest().body(ApiError.validation(message)); + } + + @ExceptionHandler(AccessDeniedException.class) + ResponseEntity handleAccessDenied() { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.of("Forbidden")); + } + + @ExceptionHandler(Exception.class) + ResponseEntity handleGeneric(Exception ex) { + // Log unexpected errors with stack traces + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiError.of("Internal server error")); + } +} +``` + +## Caching + +Requires `@EnableCaching` on a configuration class. + +```java +@Service +public class MarketCacheService { + private final MarketRepository repo; + + public MarketCacheService(MarketRepository repo) { + this.repo = repo; + } + + @Cacheable(value = "market", key = "#id") + public Market getById(Long id) { + return repo.findById(id) + .map(Market::from) + .orElseThrow(() -> new EntityNotFoundException("Market not found")); + } + + @CacheEvict(value = "market", key = "#id") + public void evict(Long id) {} +} +``` + +## Async Processing + +Requires `@EnableAsync` on a configuration class. + +```java +@Service +public class NotificationService { + @Async + public CompletableFuture sendAsync(Notification notification) { + // send email/SMS + return CompletableFuture.completedFuture(null); + } +} +``` + +## Logging (SLF4J) + +```java +@Service +public class ReportService { + private static final Logger log = LoggerFactory.getLogger(ReportService.class); + + public Report generate(Long marketId) { + log.info("generate_report marketId={}", marketId); + try { + // logic + } catch (Exception ex) { + log.error("generate_report_failed marketId={}", marketId, ex); + throw ex; + } + return new Report(); + } +} +``` + +## Middleware / Filters + +```java +@Component +public class RequestLoggingFilter extends OncePerRequestFilter { + private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + long start = System.currentTimeMillis(); + try { + filterChain.doFilter(request, response); + } finally { + long duration = System.currentTimeMillis() - start; + log.info("req method={} uri={} status={} durationMs={}", + request.getMethod(), request.getRequestURI(), response.getStatus(), duration); + } + } +} +``` + +## Pagination and Sorting + +```java +PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); +Page results = marketService.list(page); +``` + +## Error-Resilient External Calls + +```java +public T withRetry(Supplier supplier, int maxRetries) { + int attempts = 0; + while (true) { + try { + return supplier.get(); + } catch (Exception ex) { + attempts++; + if (attempts >= maxRetries) { + throw ex; + } + try { + Thread.sleep((long) Math.pow(2, attempts) * 100L); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw ex; + } + } + } +} +``` + +## Rate Limiting (Filter + Bucket4j) + +**Security Note**: The `X-Forwarded-For` header is untrusted by default because clients can spoof it. +Only use forwarded headers when: +1. Your app is behind a trusted reverse proxy (nginx, AWS ALB, etc.) +2. You have registered `ForwardedHeaderFilter` as a bean +3. You have configured `server.forward-headers-strategy=NATIVE` or `FRAMEWORK` in application properties +4. Your proxy is configured to overwrite (not append to) the `X-Forwarded-For` header + +When `ForwardedHeaderFilter` is properly configured, `request.getRemoteAddr()` will automatically +return the correct client IP from the forwarded headers. Without this configuration, use +`request.getRemoteAddr()` directly—it returns the immediate connection IP, which is the only +trustworthy value. + +```java +@Component +public class RateLimitFilter extends OncePerRequestFilter { + private final Map buckets = new ConcurrentHashMap<>(); + + /* + * SECURITY: This filter uses request.getRemoteAddr() to identify clients for rate limiting. + * + * If your application is behind a reverse proxy (nginx, AWS ALB, etc.), you MUST configure + * Spring to handle forwarded headers properly for accurate client IP detection: + * + * 1. Set server.forward-headers-strategy=NATIVE (for cloud platforms) or FRAMEWORK in + * application.properties/yaml + * 2. If using FRAMEWORK strategy, register ForwardedHeaderFilter: + * + * @Bean + * ForwardedHeaderFilter forwardedHeaderFilter() { + * return new ForwardedHeaderFilter(); + * } + * + * 3. Ensure your proxy overwrites (not appends) the X-Forwarded-For header to prevent spoofing + * 4. Configure server.tomcat.remoteip.trusted-proxies or equivalent for your container + * + * Without this configuration, request.getRemoteAddr() returns the proxy IP, not the client IP. + * Do NOT read X-Forwarded-For directly—it is trivially spoofable without trusted proxy handling. + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + // Use getRemoteAddr() which returns the correct client IP when ForwardedHeaderFilter + // is configured, or the direct connection IP otherwise. Never trust X-Forwarded-For + // headers directly without proper proxy configuration. + String clientIp = request.getRemoteAddr(); + + Bucket bucket = buckets.computeIfAbsent(clientIp, + k -> Bucket.builder() + .addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1)))) + .build()); + + if (bucket.tryConsume(1)) { + filterChain.doFilter(request, response); + } else { + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + } + } +} +``` + +## Background Jobs + +Use Spring’s `@Scheduled` or integrate with queues (e.g., Kafka, SQS, RabbitMQ). Keep handlers idempotent and observable. + +## Observability + +- Structured logging (JSON) via Logback encoder +- Metrics: Micrometer + Prometheus/OTel +- Tracing: Micrometer Tracing with OpenTelemetry or Brave backend + +## Production Defaults + +- Prefer constructor injection, avoid field injection +- Enable `spring.mvc.problemdetails.enabled=true` for RFC 7807 errors (Spring Boot 3+) +- Configure HikariCP pool sizes for workload, set timeouts +- Use `@Transactional(readOnly = true)` for queries +- Enforce null-safety via `@NonNull` and `Optional` where appropriate + +**Remember**: Keep controllers thin, services focused, repositories simple, and errors handled centrally. Optimize for maintainability and testability. diff --git a/skills/springboot-security/SKILL.md b/skills/springboot-security/SKILL.md new file mode 100644 index 0000000..f9dc6a2 --- /dev/null +++ b/skills/springboot-security/SKILL.md @@ -0,0 +1,119 @@ +--- +name: springboot-security +description: Spring Security best practices for authn/authz, validation, CSRF, secrets, headers, rate limiting, and dependency security in Java Spring Boot services. +--- + +# Spring Boot Security Review + +Use when adding auth, handling input, creating endpoints, or dealing with secrets. + +## Authentication + +- Prefer stateless JWT or opaque tokens with revocation list +- Use `httpOnly`, `Secure`, `SameSite=Strict` cookies for sessions +- Validate tokens with `OncePerRequestFilter` or resource server + +```java +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + private final JwtService jwtService; + + public JwtAuthFilter(JwtService jwtService) { + this.jwtService = jwtService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain chain) throws ServletException, IOException { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header != null && header.startsWith("Bearer ")) { + String token = header.substring(7); + Authentication auth = jwtService.authenticate(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } + chain.doFilter(request, response); + } +} +``` + +## Authorization + +- Enable method security: `@EnableMethodSecurity` +- Use `@PreAuthorize("hasRole('ADMIN')")` or `@PreAuthorize("@authz.canEdit(#id)")` +- Deny by default; expose only required scopes + +## Input Validation + +- Use Bean Validation with `@Valid` on controllers +- Apply constraints on DTOs: `@NotBlank`, `@Email`, `@Size`, custom validators +- Sanitize any HTML with a whitelist before rendering + +## SQL Injection Prevention + +- Use Spring Data repositories or parameterized queries +- For native queries, use `:param` bindings; never concatenate strings + +## CSRF Protection + +- For browser session apps, keep CSRF enabled; include token in forms/headers +- For pure APIs with Bearer tokens, disable CSRF and rely on stateless auth + +```java +http + .csrf(csrf -> csrf.disable()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); +``` + +## Secrets Management + +- No secrets in source; load from env or vault +- Keep `application.yml` free of credentials; use placeholders +- Rotate tokens and DB credentials regularly + +## Security Headers + +```java +http + .headers(headers -> headers + .contentSecurityPolicy(csp -> csp + .policyDirectives("default-src 'self'")) + .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin) + .xssProtection(Customizer.withDefaults()) + .referrerPolicy(rp -> rp.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER))); +``` + +## Rate Limiting + +- Apply Bucket4j or gateway-level limits on expensive endpoints +- Log and alert on bursts; return 429 with retry hints + +## Dependency Security + +- Run OWASP Dependency Check / Snyk in CI +- Keep Spring Boot and Spring Security on supported versions +- Fail builds on known CVEs + +## Logging and PII + +- Never log secrets, tokens, passwords, or full PAN data +- Redact sensitive fields; use structured JSON logging + +## File Uploads + +- Validate size, content type, and extension +- Store outside web root; scan if required + +## Checklist Before Release + +- [ ] Auth tokens validated and expired correctly +- [ ] Authorization guards on every sensitive path +- [ ] All inputs validated and sanitized +- [ ] No string-concatenated SQL +- [ ] CSRF posture correct for app type +- [ ] Secrets externalized; none committed +- [ ] Security headers configured +- [ ] Rate limiting on APIs +- [ ] Dependencies scanned and up to date +- [ ] Logs free of sensitive data + +**Remember**: Deny by default, validate inputs, least privilege, and secure-by-configuration first. diff --git a/skills/springboot-tdd/SKILL.md b/skills/springboot-tdd/SKILL.md new file mode 100644 index 0000000..daaa990 --- /dev/null +++ b/skills/springboot-tdd/SKILL.md @@ -0,0 +1,157 @@ +--- +name: springboot-tdd +description: Test-driven development for Spring Boot using JUnit 5, Mockito, MockMvc, Testcontainers, and JaCoCo. Use when adding features, fixing bugs, or refactoring. +--- + +# Spring Boot TDD Workflow + +TDD guidance for Spring Boot services with 80%+ coverage (unit + integration). + +## When to Use + +- New features or endpoints +- Bug fixes or refactors +- Adding data access logic or security rules + +## Workflow + +1) Write tests first (they should fail) +2) Implement minimal code to pass +3) Refactor with tests green +4) Enforce coverage (JaCoCo) + +## Unit Tests (JUnit 5 + Mockito) + +```java +@ExtendWith(MockitoExtension.class) +class MarketServiceTest { + @Mock MarketRepository repo; + @InjectMocks MarketService service; + + @Test + void createsMarket() { + CreateMarketRequest req = new CreateMarketRequest("name", "desc", Instant.now(), List.of("cat")); + when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + Market result = service.create(req); + + assertThat(result.name()).isEqualTo("name"); + verify(repo).save(any()); + } +} +``` + +Patterns: +- Arrange-Act-Assert +- Avoid partial mocks; prefer explicit stubbing +- Use `@ParameterizedTest` for variants + +## Web Layer Tests (MockMvc) + +```java +@WebMvcTest(MarketController.class) +class MarketControllerTest { + @Autowired MockMvc mockMvc; + @MockBean MarketService marketService; + + @Test + void returnsMarkets() throws Exception { + when(marketService.list(any())).thenReturn(Page.empty()); + + mockMvc.perform(get("/api/markets")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").isArray()); + } +} +``` + +## Integration Tests (SpringBootTest) + +```java +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class MarketIntegrationTest { + @Autowired MockMvc mockMvc; + + @Test + void createsMarket() throws Exception { + mockMvc.perform(post("/api/markets") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + {"name":"Test","description":"Desc","endDate":"2030-01-01T00:00:00Z","categories":["general"]} + """)) + .andExpect(status().isCreated()); + } +} +``` + +## Persistence Tests (DataJpaTest) + +```java +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import(TestContainersConfig.class) +class MarketRepositoryTest { + @Autowired MarketRepository repo; + + @Test + void savesAndFinds() { + MarketEntity entity = new MarketEntity(); + entity.setName("Test"); + repo.save(entity); + + Optional found = repo.findByName("Test"); + assertThat(found).isPresent(); + } +} +``` + +## Testcontainers + +- Use reusable containers for Postgres/Redis to mirror production +- Wire via `@DynamicPropertySource` to inject JDBC URLs into Spring context + +## Coverage (JaCoCo) + +Maven snippet: +```xml + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + prepare-agent + + + report + verify + report + + + +``` + +## Assertions + +- Prefer AssertJ (`assertThat`) for readability +- For JSON responses, use `jsonPath` +- For exceptions: `assertThatThrownBy(...)` + +## Test Data Builders + +```java +class MarketBuilder { + private String name = "Test"; + MarketBuilder withName(String name) { this.name = name; return this; } + Market build() { return new Market(null, name, MarketStatus.ACTIVE); } +} +``` + +## CI Commands + +- Maven: `mvn -T 4 test` or `mvn verify` +- Gradle: `./gradlew test jacocoTestReport` + +**Remember**: Keep tests fast, isolated, and deterministic. Test behavior, not implementation details. diff --git a/skills/springboot-verification/SKILL.md b/skills/springboot-verification/SKILL.md new file mode 100644 index 0000000..909e90a --- /dev/null +++ b/skills/springboot-verification/SKILL.md @@ -0,0 +1,100 @@ +--- +name: springboot-verification +description: Verification loop for Spring Boot projects: build, static analysis, tests with coverage, security scans, and diff review before release or PR. +--- + +# Spring Boot Verification Loop + +Run before PRs, after major changes, and pre-deploy. + +## Phase 1: Build + +```bash +mvn -T 4 clean verify -DskipTests +# or +./gradlew clean assemble -x test +``` + +If build fails, stop and fix. + +## Phase 2: Static Analysis + +Maven (common plugins): +```bash +mvn -T 4 spotbugs:check pmd:check checkstyle:check +``` + +Gradle (if configured): +```bash +./gradlew checkstyleMain pmdMain spotbugsMain +``` + +## Phase 3: Tests + Coverage + +```bash +mvn -T 4 test +mvn jacoco:report # verify 80%+ coverage +# or +./gradlew test jacocoTestReport +``` + +Report: +- Total tests, passed/failed +- Coverage % (lines/branches) + +## Phase 4: Security Scan + +```bash +# Dependency CVEs +mvn org.owasp:dependency-check-maven:check +# or +./gradlew dependencyCheckAnalyze + +# Secrets (git) +git secrets --scan # if configured +``` + +## Phase 5: Lint/Format (optional gate) + +```bash +mvn spotless:apply # if using Spotless plugin +./gradlew spotlessApply +``` + +## Phase 6: Diff Review + +```bash +git diff --stat +git diff +``` + +Checklist: +- No debugging logs left (`System.out`, `log.debug` without guards) +- Meaningful errors and HTTP statuses +- Transactions and validation present where needed +- Config changes documented + +## Output Template + +``` +VERIFICATION REPORT +=================== +Build: [PASS/FAIL] +Static: [PASS/FAIL] (spotbugs/pmd/checkstyle) +Tests: [PASS/FAIL] (X/Y passed, Z% coverage) +Security: [PASS/FAIL] (CVE findings: N) +Diff: [X files changed] + +Overall: [READY / NOT READY] + +Issues to Fix: +1. ... +2. ... +``` + +## Continuous Mode + +- Re-run phases on significant changes or every 30–60 minutes in long sessions +- Keep a short loop: `mvn -T 4 test` + spotbugs for quick feedback + +**Remember**: Fast feedback beats late surprises. Keep the gate strict—treat warnings as defects in production systems.