@booklib/skills 1.4.0 → 1.5.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.
@@ -0,0 +1,39 @@
1
+ {
2
+ "evals": [
3
+ {
4
+ "id": "eval-01-autoconfig-injection-hardcoding",
5
+ "prompt": "Review this Spring Boot code:\n\n```java\n@Configuration\npublic class AppConfig {\n @Bean\n public DataSource dataSource() {\n DriverManagerDataSource ds = new DriverManagerDataSource();\n ds.setUrl(\"jdbc:postgresql://prod-db.internal/orders\");\n ds.setUsername(\"orders_user\");\n ds.setPassword(\"S3cr3tP@ss\");\n return ds;\n }\n\n @Bean\n public ObjectMapper objectMapper() {\n return new ObjectMapper();\n }\n}\n\n@RestController\npublic class OrderController {\n @Autowired\n private OrderRepository orderRepository;\n\n @Autowired\n private OrderService orderService;\n\n @GetMapping(\"/orders/{id}\")\n public Order getOrder(@PathVariable Long id) {\n return orderRepository.findById(id).orElse(null);\n }\n\n @PostMapping(\"/orders\")\n public Order createOrder(@RequestBody Order order) {\n return orderService.place(order);\n }\n}\n```",
6
+ "expectations": [
7
+ "Flag Ch 2/3: Manual DataSource @Bean fights Spring Boot auto-configuration — delete AppConfig.dataSource() and move connection details to application.properties using spring.datasource.url/username/password",
8
+ "Flag Ch 3: Credentials hardcoded in source code — must be externalized to environment variables: spring.datasource.password=${DB_PASS}",
9
+ "Flag Ch 2: ObjectMapper @Bean is unnecessary — Spring Boot auto-configures Jackson; only define a custom ObjectMapper when you need to change behavior",
10
+ "Flag Ch 2: @Autowired field injection on OrderRepository and OrderService — replace with constructor injection for testability",
11
+ "Flag Ch 2: getOrder returns null (200 with null body) when not found — use Optional.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build()) or throw ResponseStatusException(NOT_FOUND)",
12
+ "Flag Ch 2: createOrder returns 200 — POST that creates a resource should return 201 Created with a Location header; use ResponseEntity.created(uri).body(saved)"
13
+ ]
14
+ },
15
+ {
16
+ "id": "eval-02-testing-antipatterns",
17
+ "prompt": "Review this Spring Boot test code:\n\n```java\n@SpringBootTest\npublic class ProductControllerTest {\n @Autowired\n private ProductController controller;\n\n @Autowired\n private ProductRepository repo;\n\n @Test\n public void testGetProduct() {\n Product p = new Product(null, \"Widget\", 9.99);\n repo.save(p);\n Product result = controller.getProduct(p.getId());\n assertNotNull(result);\n assertEquals(\"Widget\", result.getName());\n }\n\n @Test\n public void testCreateProduct() {\n Product p = new Product(null, \"Gadget\", 19.99);\n Product result = controller.createProduct(p);\n assertNotNull(result.getId());\n }\n\n @Test\n public void testAdminEndpoint() {\n // No auth setup — just calls controller directly\n String result = controller.adminDashboard();\n assertNotNull(result);\n }\n}\n```",
18
+ "expectations": [
19
+ "Flag Ch 4: @SpringBootTest loads the full application context for what are simple controller tests — use @WebMvcTest(ProductController.class) with MockMvc for fast, isolated controller tests",
20
+ "Flag Ch 4: Directly calling controller.getProduct() bypasses HTTP layer — no status code, content-type, or header assertions are possible; use MockMvc.perform(get(...)).andExpect(status().isOk())",
21
+ "Flag Ch 4: testAdminEndpoint calls controller directly with no authentication context — use @WithMockUser(roles='ADMIN') and MockMvc to assert 403 for unauthorized and 200 for authorized access",
22
+ "Flag Ch 4: Tests use a real ProductRepository writing to the database — in a @WebMvcTest test, use @MockBean ProductService to isolate the controller from persistence",
23
+ "Flag Ch 4: No negative test cases — missing test for product not found (expect 404), and no test verifying createProduct returns 201 with a Location header",
24
+ "Provide corrected test class using @WebMvcTest, MockMvc, @MockBean, @WithMockUser, and assertions on HTTP status codes and response JSON"
25
+ ]
26
+ },
27
+ {
28
+ "id": "eval-03-idiomatic-spring-boot",
29
+ "prompt": "Review this Spring Boot code:\n\n```java\n@SpringBootApplication\npublic class LibraryApp {\n public static void main(String[] args) {\n SpringApplication.run(LibraryApp.class, args);\n }\n}\n\n@RestController\n@RequestMapping(\"/api/books\")\npublic class BookController {\n private final BookService service;\n\n public BookController(BookService service) {\n this.service = service;\n }\n\n @GetMapping(\"/{id}\")\n public ResponseEntity<Book> getBook(@PathVariable Long id) {\n return ResponseEntity.ok(service.findById(id));\n }\n\n @PostMapping\n public ResponseEntity<Book> createBook(@RequestBody Book book) {\n Book saved = service.save(book);\n URI location = URI.create(\"/api/books/\" + saved.getId());\n return ResponseEntity.created(location).body(saved);\n }\n}\n\n@Service\npublic class BookService {\n private static final Logger log = LoggerFactory.getLogger(BookService.class);\n private final BookRepository repo;\n\n public BookService(BookRepository repo) { this.repo = repo; }\n\n public Book findById(Long id) {\n return repo.findById(id)\n .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));\n }\n\n public Book save(Book book) { return repo.save(book); }\n}\n```\n\n```properties\nspring.datasource.url=${DB_URL:jdbc:h2:mem:library}\nspring.datasource.username=${DB_USER:sa}\nspring.datasource.password=${DB_PASS:}\nspring.jpa.hibernate.ddl-auto=validate\nmanagement.endpoints.web.exposure.include=health,info\n```",
30
+ "expectations": [
31
+ "Recognize this code is idiomatic Spring Boot — do NOT manufacture issues",
32
+ "Acknowledge correct patterns: @SpringBootApplication (Ch 1), constructor injection in both controller and service (Ch 2), ResponseEntity with correct 200/201 status codes and Location header (Ch 2), Optional.orElseThrow with ResponseStatusException for clean 404 (Ch 2), SLF4J logger (Ch 3), externalized config with env-var defaults (Ch 3), Actuator locked to health+info (Ch 7)",
33
+ "At most note: ResponseStatusException could include a descriptive message like 'Book ' + id + ' not found' for better client error messages",
34
+ "At most suggest: adding spring-boot-starter-actuator dependency if not already present to enable the management.endpoints config",
35
+ "Do NOT flag the absence of @Autowired — constructor injection is the preferred style and Spring auto-wires single constructors without any annotation"
36
+ ]
37
+ }
38
+ ]
39
+ }
@@ -0,0 +1,185 @@
1
+ # After: Spring Boot in Action
2
+
3
+ The same library API rewritten with idiomatic Spring Boot — auto-configuration, constructor injection, externalized config, profiles, proper testing, and Actuator.
4
+
5
+ ```java
6
+ // @SpringBootApplication enables auto-config, component scan, config (Ch 1, 2)
7
+ @SpringBootApplication
8
+ public class LibraryApp {
9
+ public static void main(String[] args) {
10
+ SpringApplication.run(LibraryApp.class, args);
11
+ }
12
+ }
13
+
14
+ // No DatabaseConfig class needed — auto-configuration handles DataSource (Ch 2, 3)
15
+ // Credentials externalized to application.properties via environment variables
16
+
17
+ // Constructor injection — testable without Spring context (Ch 2)
18
+ @RestController
19
+ @RequestMapping("/api/books")
20
+ public class BookController {
21
+ private final BookService service;
22
+
23
+ public BookController(BookService service) {
24
+ this.service = service;
25
+ }
26
+
27
+ // Returns 404 when not found, not null (Ch 2)
28
+ @GetMapping("/{id}")
29
+ public ResponseEntity<Book> getBook(@PathVariable Long id) {
30
+ return ResponseEntity.ok(service.findById(id));
31
+ }
32
+
33
+ // Returns 201 Created with Location header (Ch 2)
34
+ @PostMapping
35
+ public ResponseEntity<Book> createBook(@RequestBody Book book) {
36
+ Book saved = service.save(book);
37
+ URI location = URI.create("/api/books/" + saved.getId());
38
+ return ResponseEntity.created(location).body(saved);
39
+ }
40
+
41
+ @GetMapping
42
+ public List<Book> search(@RequestParam(required = false, defaultValue = "") String q) {
43
+ return service.search(q);
44
+ }
45
+ }
46
+
47
+ // Service with constructor injection and proper logging (Ch 2)
48
+ @Service
49
+ public class BookService {
50
+ private static final Logger log = LoggerFactory.getLogger(BookService.class);
51
+ private final BookRepository repo;
52
+
53
+ public BookService(BookRepository repo) {
54
+ this.repo = repo;
55
+ }
56
+
57
+ public Book findById(Long id) {
58
+ // Optional — 404 automatically surfaced (Ch 2)
59
+ return repo.findById(id)
60
+ .orElseThrow(() -> new ResponseStatusException(
61
+ HttpStatus.NOT_FOUND, "Book " + id + " not found"));
62
+ }
63
+
64
+ public Book save(Book book) {
65
+ return repo.save(book);
66
+ }
67
+
68
+ public List<Book> search(String query) {
69
+ log.debug("Searching for: {}", query); // proper logger, not println (Ch 3)
70
+ return query.isBlank()
71
+ ? repo.findAll()
72
+ : repo.findByTitleContainingIgnoreCase(query);
73
+ }
74
+ }
75
+
76
+ // Repository — Spring Data does the rest (Ch 2)
77
+ public interface BookRepository extends JpaRepository<Book, Long> {
78
+ List<Book> findByTitleContainingIgnoreCase(String title);
79
+ }
80
+
81
+ // Type-safe configuration object (Ch 3)
82
+ @ConfigurationProperties(prefix = "app.library")
83
+ @Component
84
+ public class LibraryProperties {
85
+ private int maxSearchResults = 50;
86
+ private String defaultSortField = "title";
87
+ // getters + setters
88
+ }
89
+
90
+ // Custom health indicator for critical dependency (Ch 7)
91
+ @Component
92
+ public class StorageHealthIndicator implements HealthIndicator {
93
+ private final BookRepository repo;
94
+ public StorageHealthIndicator(BookRepository repo) { this.repo = repo; }
95
+
96
+ @Override
97
+ public Health health() {
98
+ try {
99
+ long count = repo.count();
100
+ return Health.up().withDetail("books", count).build();
101
+ } catch (Exception e) {
102
+ return Health.down().withDetail("error", e.getMessage()).build();
103
+ }
104
+ }
105
+ }
106
+
107
+ // Controller slice test — no full context, fast (Ch 4)
108
+ @WebMvcTest(BookController.class)
109
+ public class BookControllerTest {
110
+ @Autowired
111
+ private MockMvc mockMvc;
112
+
113
+ @MockBean
114
+ private BookService service; // real service replaced with mock (Ch 4)
115
+
116
+ @Test
117
+ void getBook_returnsOk() throws Exception {
118
+ Book book = new Book(1L, "Spring Boot in Action", "9781617292545");
119
+ given(service.findById(1L)).willReturn(book);
120
+
121
+ mockMvc.perform(get("/api/books/1"))
122
+ .andExpect(status().isOk())
123
+ .andExpect(jsonPath("$.title").value("Spring Boot in Action"));
124
+ }
125
+
126
+ @Test
127
+ void getBook_returns404WhenNotFound() throws Exception {
128
+ given(service.findById(99L))
129
+ .willThrow(new ResponseStatusException(HttpStatus.NOT_FOUND));
130
+
131
+ mockMvc.perform(get("/api/books/99"))
132
+ .andExpect(status().isNotFound());
133
+ }
134
+
135
+ @Test
136
+ @WithMockUser(roles = "USER")
137
+ void createBook_returns201() throws Exception {
138
+ Book book = new Book(null, "New Book", "1234567890");
139
+ Book saved = new Book(1L, "New Book", "1234567890");
140
+ given(service.save(any())).willReturn(saved);
141
+
142
+ mockMvc.perform(post("/api/books")
143
+ .contentType(MediaType.APPLICATION_JSON)
144
+ .content("{\"title\":\"New Book\",\"isbn\":\"1234567890\"}"))
145
+ .andExpect(status().isCreated())
146
+ .andExpect(header().string("Location", "/api/books/1"));
147
+ }
148
+ }
149
+ ```
150
+
151
+ ```properties
152
+ # application.properties — base config, all env-specific values externalized (Ch 3)
153
+ spring.datasource.url=${DB_URL:jdbc:h2:mem:library}
154
+ spring.datasource.username=${DB_USER:sa}
155
+ spring.datasource.password=${DB_PASS:}
156
+ spring.jpa.hibernate.ddl-auto=validate
157
+
158
+ # Actuator — health and info only exposed publicly (Ch 7)
159
+ management.endpoints.web.exposure.include=health,info
160
+ management.endpoint.health.show-details=when-authorized
161
+
162
+ # application-dev.properties — dev overrides (Ch 3)
163
+ # spring.datasource.url=jdbc:h2:mem:library
164
+ # spring.jpa.hibernate.ddl-auto=create-drop
165
+ # logging.level.com.example=DEBUG
166
+ # management.endpoints.web.exposure.include=*
167
+
168
+ # application-production.properties — production hardening (Ch 8)
169
+ # spring.jpa.hibernate.ddl-auto=validate
170
+ # logging.level.root=WARN
171
+ # management.endpoints.web.exposure.include=health,info
172
+ ```
173
+
174
+ **Key improvements:**
175
+ - `@SpringBootApplication` enables auto-configuration — no manual `DataSource` bean (Ch 2)
176
+ - Credentials externalized to env vars via `${DB_URL}` — never hardcoded (Ch 3)
177
+ - Constructor injection throughout — testable without Spring context (Ch 2)
178
+ - `ResponseEntity` with correct status codes: 200, 201, 404 (Ch 2)
179
+ - `Optional` → `orElseThrow` → `ResponseStatusException` — clean 404 (Ch 2)
180
+ - `@ConfigurationProperties` for grouped app config (Ch 3)
181
+ - `@WebMvcTest` + `@MockBean` — fast, isolated controller tests (Ch 4)
182
+ - `@WithMockUser` — security tested explicitly (Ch 4)
183
+ - Custom `HealthIndicator` — DB health visible in Actuator (Ch 7)
184
+ - Actuator locked down — only `health` and `info` public (Ch 7)
185
+ - Profile-based properties — no env checks in code (Ch 3, 8)
@@ -0,0 +1,84 @@
1
+ # Before: Spring Boot in Action
2
+
3
+ A book library REST API with common Spring Boot anti-patterns — manual configuration fighting auto-config, field injection, hardcoded values, missing tests, and no Actuator.
4
+
5
+ ```java
6
+ // Main class missing @SpringBootApplication — won't auto-configure anything
7
+ @Configuration
8
+ @ComponentScan
9
+ public class LibraryApp {
10
+ public static void main(String[] args) {
11
+ SpringApplication.run(LibraryApp.class, args);
12
+ }
13
+ }
14
+
15
+ // Manual DataSource bean — fights auto-configuration (Ch 2, 3)
16
+ @Configuration
17
+ public class DatabaseConfig {
18
+ @Bean
19
+ public DataSource dataSource() {
20
+ DriverManagerDataSource ds = new DriverManagerDataSource();
21
+ ds.setDriverClassName("org.postgresql.Driver");
22
+ ds.setUrl("jdbc:postgresql://localhost/library"); // hardcoded (Ch 3)
23
+ ds.setUsername("admin"); // hardcoded credential!
24
+ ds.setPassword("secret123"); // hardcoded credential!
25
+ return ds;
26
+ }
27
+ }
28
+
29
+ // Field injection — untestable without Spring context (Ch 2)
30
+ @RestController
31
+ public class BookController {
32
+ @Autowired
33
+ private BookRepository bookRepository;
34
+
35
+ @Autowired
36
+ private BookService bookService;
37
+
38
+ // Returns null instead of 404 when book not found (Ch 2)
39
+ @GetMapping("/books/{id}")
40
+ public Book getBook(@PathVariable Long id) {
41
+ return bookRepository.findById(id).orElse(null); // null slips to client
42
+ }
43
+
44
+ // No status code — always returns 200 even on create (Ch 2)
45
+ @PostMapping("/books")
46
+ @ResponseBody
47
+ public Book createBook(@RequestBody Book book) {
48
+ return bookRepository.save(book);
49
+ }
50
+ }
51
+
52
+ // Service with field injection and no error handling
53
+ @Service
54
+ public class BookService {
55
+ @Autowired
56
+ private BookRepository bookRepository;
57
+
58
+ public List<Book> search(String query) {
59
+ // Environment check in code instead of using profiles (Ch 3)
60
+ if (System.getProperty("env").equals("dev")) {
61
+ System.out.println("Searching for: " + query); // println not logger
62
+ }
63
+ return bookRepository.findAll(); // returns everything, ignores query
64
+ }
65
+ }
66
+
67
+ // Test that boots full context just to test one controller method (Ch 4)
68
+ @SpringBootTest
69
+ public class BookControllerTest {
70
+ @Autowired
71
+ private BookController controller;
72
+
73
+ @Test
74
+ public void testGetBook() {
75
+ // Direct controller call — no HTTP semantics, no status code testing
76
+ Book result = controller.getBook(1L);
77
+ assertNotNull(result);
78
+ }
79
+ }
80
+
81
+ // application.properties — missing externalized config
82
+ // (no datasource url, credentials baked into Java code above)
83
+ // spring.jpa.hibernate.ddl-auto=create // destroys data on restart!
84
+ ```