From 33334984ecfc1b6d254761ef177128620c686c32 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Tue, 31 Mar 2026 20:06:20 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20API=20hardening=20=E2=80=94=20input?= =?UTF-8?q?=20validation,=20duplicate=20endpoint=20cleanup,=20config=20bin?= =?UTF-8?q?ding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A1: Validate kind parameter against NodeKind enum on /kinds/{kind} path variable AND /nodes kind query parameter. Returns 400 with descriptive error and valid values list for invalid kinds. A2: Clamp negative offset to 0 on all paginated endpoints (/kinds, /nodes, /edges) to prevent Neo4j runtime exceptions. A3: Remove duplicate /api/nodes/find endpoint — /api/search provides the same functionality with configurable limit. C1: Add graph.path to CodeIqConfig @ConfigurationProperties. Neo4jConfig now reads from config.getGraph().getPath() instead of @Value annotation, consolidating all config under the codeiq prefix. C3: Set allow-bean-definition-overriding to false to catch accidental duplicate bean registrations. All 1396 tests pass (0 failures, 0 errors). Co-Authored-By: Paperclip --- .../iq/api/GraphController.java | 35 +++++++++++-------- .../iq/config/CodeIqConfig.java | 18 ++++++++++ .../iq/config/Neo4jConfig.java | 5 ++- src/main/resources/application.yml | 2 +- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/main/java/io/github/randomcodespace/iq/api/GraphController.java b/src/main/java/io/github/randomcodespace/iq/api/GraphController.java index 57b3ebdc..7cb56d91 100644 --- a/src/main/java/io/github/randomcodespace/iq/api/GraphController.java +++ b/src/main/java/io/github/randomcodespace/iq/api/GraphController.java @@ -13,12 +13,16 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; +import io.github.randomcodespace.iq.model.NodeKind; + import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * REST API controller matching the Python OSSCodeIQ API paths. @@ -62,7 +66,8 @@ public Map nodesByKind( @RequestParam(defaultValue = "50") int limit, @RequestParam(defaultValue = "0") int offset) { requireQueryService(); - return queryService.nodesByKind(kind, Math.min(limit, 1000), offset); + validateNodeKind(kind); + return queryService.nodesByKind(kind, Math.min(limit, 1000), Math.max(0, offset)); } @GetMapping("/nodes") @@ -71,13 +76,10 @@ public Map listNodes( @RequestParam(defaultValue = "100") int limit, @RequestParam(defaultValue = "0") int offset) { requireQueryService(); - return queryService.listNodes(kind, Math.min(limit, 1000), offset); - } - - @GetMapping("/nodes/find") - public List> findNode(@RequestParam String q) { - requireQueryService(); - return queryService.searchGraph(q, 50); + if (kind != null) { + validateNodeKind(kind); + } + return queryService.listNodes(kind, Math.min(limit, 1000), Math.max(0, offset)); } @GetMapping("/nodes/{nodeId}/detail") @@ -104,7 +106,7 @@ public Map listEdges( @RequestParam(defaultValue = "100") int limit, @RequestParam(defaultValue = "0") int offset) { requireQueryService(); - return queryService.listEdges(kind, Math.min(limit, 1000), offset); + return queryService.listEdges(kind, Math.min(limit, 1000), Math.max(0, offset)); } @GetMapping("/ego/{center}") @@ -196,11 +198,16 @@ public List> searchGraph( return queryService.searchGraph(q, Math.min(limit, 1000)); } - /** - * Check whether Neo4j (via QueryService) is available for queries. - */ - private boolean useNeo4j() { - return queryService != null; + private void validateNodeKind(String kind) { + try { + NodeKind.fromValue(kind); + } catch (IllegalArgumentException e) { + String valid = Arrays.stream(NodeKind.values()) + .map(NodeKind::getValue) + .collect(Collectors.joining(", ")); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Invalid node kind: '" + kind + "'. Valid values: " + valid); + } } private void requireQueryService() { diff --git a/src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java b/src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java index d61bf929..9505b815 100644 --- a/src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java +++ b/src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java @@ -25,6 +25,16 @@ public class CodeIqConfig { /** Batch size for file processing during indexing (files per H2 flush). */ private int batchSize = 500; + /** Graph configuration sub-properties. */ + private Graph graph = new Graph(); + + public static class Graph { + private String path = ".osscodeiq/graph.db"; + + public String getPath() { return path; } + public void setPath(String path) { this.path = path; } + } + /** Service name tag for multi-repo graph mode. */ private String serviceName; @@ -77,4 +87,12 @@ public String getServiceName() { public void setServiceName(String serviceName) { this.serviceName = serviceName; } + + public Graph getGraph() { + return graph; + } + + public void setGraph(Graph graph) { + this.graph = graph; + } } diff --git a/src/main/java/io/github/randomcodespace/iq/config/Neo4jConfig.java b/src/main/java/io/github/randomcodespace/iq/config/Neo4jConfig.java index 48a80e00..79f452cd 100644 --- a/src/main/java/io/github/randomcodespace/iq/config/Neo4jConfig.java +++ b/src/main/java/io/github/randomcodespace/iq/config/Neo4jConfig.java @@ -34,9 +34,8 @@ public class Neo4jConfig { private int boltPort; @Bean(destroyMethod = "shutdown") - DatabaseManagementService databaseManagementService( - @Value("${codeiq.graph.path:.osscodeiq/graph.db}") String dbPath) { - return new DatabaseManagementServiceBuilder(Path.of(dbPath)) + DatabaseManagementService databaseManagementService(CodeIqConfig config) { + return new DatabaseManagementServiceBuilder(Path.of(config.getGraph().getPath())) .setConfig(BoltConnector.enabled, true) .setConfig(BoltConnector.listen_address, new SocketAddress("localhost", boltPort)) .build(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6f51da56..f227ae8d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,7 +3,7 @@ spring: name: code-iq main: banner-mode: off - allow-bean-definition-overriding: true + allow-bean-definition-overriding: false threads: virtual: enabled: true From b26b963d66ad3e147dd81e08f50bc27e6eac17bc Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Tue, 31 Mar 2026 20:31:53 +0000 Subject: [PATCH 2/2] test: add validation and boundary tests for GraphController coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover the new input validation (invalid NodeKind → 400), negative offset clamping, and limit capping on /kinds, /nodes, and /edges endpoints to satisfy SonarCloud 80% new code coverage gate. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 (1M context) --- .../iq/api/GraphControllerTest.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java b/src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java index e8540e66..2d5acd8f 100644 --- a/src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java +++ b/src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java @@ -114,6 +114,34 @@ void nodesByKindShouldAcceptPaginationParams() throws Exception { .andExpect(jsonPath("$.limit").value(25)); } + @Test + void nodesByKindShouldReturn400ForInvalidKind() throws Exception { + mockMvc.perform(get("/api/kinds/not_a_real_kind")) + .andExpect(status().isBadRequest()); + } + + @Test + void nodesByKindShouldClampNegativeOffset() throws Exception { + Map result = new LinkedHashMap<>(); + result.put("kind", "endpoint"); + result.put("nodes", List.of()); + when(queryService.nodesByKind("endpoint", 50, 0)).thenReturn(result); + + mockMvc.perform(get("/api/kinds/endpoint?offset=-5")) + .andExpect(status().isOk()); + } + + @Test + void nodesByKindShouldCapLimitTo1000() throws Exception { + Map result = new LinkedHashMap<>(); + result.put("kind", "endpoint"); + result.put("nodes", List.of()); + when(queryService.nodesByKind("endpoint", 1000, 0)).thenReturn(result); + + mockMvc.perform(get("/api/kinds/endpoint?limit=5000")) + .andExpect(status().isOk()); + } + // --- /api/nodes --- @Test @@ -140,6 +168,34 @@ void listNodesShouldFilterByKind() throws Exception { .andExpect(jsonPath("$.count").value(0)); } + @Test + void listNodesShouldReturn400ForInvalidKind() throws Exception { + mockMvc.perform(get("/api/nodes?kind=bogus_kind")) + .andExpect(status().isBadRequest()); + } + + @Test + void listNodesShouldClampNegativeOffset() throws Exception { + Map result = new LinkedHashMap<>(); + result.put("nodes", List.of()); + result.put("count", 0); + when(queryService.listNodes(null, 100, 0)).thenReturn(result); + + mockMvc.perform(get("/api/nodes?offset=-10")) + .andExpect(status().isOk()); + } + + @Test + void listNodesShouldCapLimitTo1000() throws Exception { + Map result = new LinkedHashMap<>(); + result.put("nodes", List.of()); + result.put("count", 0); + when(queryService.listNodes(null, 1000, 0)).thenReturn(result); + + mockMvc.perform(get("/api/nodes?limit=9999")) + .andExpect(status().isOk()); + } + // --- /api/nodes/{nodeId}/detail --- @Test @@ -208,6 +264,30 @@ void listEdgesShouldReturnEdges() throws Exception { .andExpect(jsonPath("$.total").value(0)); } + @Test + void listEdgesShouldClampNegativeOffset() throws Exception { + Map result = new LinkedHashMap<>(); + result.put("edges", List.of()); + result.put("count", 0); + result.put("total", 0); + when(queryService.listEdges(null, 100, 0)).thenReturn(result); + + mockMvc.perform(get("/api/edges?offset=-3")) + .andExpect(status().isOk()); + } + + @Test + void listEdgesShouldCapLimitTo1000() throws Exception { + Map result = new LinkedHashMap<>(); + result.put("edges", List.of()); + result.put("count", 0); + result.put("total", 0); + when(queryService.listEdges(null, 1000, 0)).thenReturn(result); + + mockMvc.perform(get("/api/edges?limit=5000")) + .andExpect(status().isOk()); + } + // --- /api/ego/{center} --- @Test