Skip to content

Commit 23cbd3a

Browse files
committed
Merge branch 'fix/api-hardening-validation-cleanup'
# Conflicts: # src/main/java/io/github/randomcodespace/iq/api/GraphController.java
2 parents 90401ee + b26b963 commit 23cbd3a

5 files changed

Lines changed: 124 additions & 13 deletions

File tree

src/main/java/io/github/randomcodespace/iq/api/GraphController.java

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
import org.springframework.web.bind.annotation.RestController;
1515
import org.springframework.web.server.ResponseStatusException;
1616

17+
import io.github.randomcodespace.iq.model.NodeKind;
18+
1719
import java.io.IOException;
1820
import java.nio.charset.StandardCharsets;
1921
import java.nio.file.Files;
2022
import java.nio.file.Path;
23+
import java.util.Arrays;
2124
import java.util.List;
2225
import java.util.Map;
26+
import java.util.stream.Collectors;
2327

2428
/**
2529
* REST API controller matching the Python OSSCodeIQ API paths.
@@ -63,7 +67,8 @@ public Map<String, Object> nodesByKind(
6367
@RequestParam(defaultValue = "50") int limit,
6468
@RequestParam(defaultValue = "0") int offset) {
6569
requireQueryService();
66-
return queryService.nodesByKind(kind, Math.min(limit, 1000), offset);
70+
validateNodeKind(kind);
71+
return queryService.nodesByKind(kind, Math.min(limit, 1000), Math.max(0, offset));
6772
}
6873

6974
@GetMapping("/nodes")
@@ -72,13 +77,10 @@ public Map<String, Object> listNodes(
7277
@RequestParam(defaultValue = "100") int limit,
7378
@RequestParam(defaultValue = "0") int offset) {
7479
requireQueryService();
75-
return queryService.listNodes(kind, Math.min(limit, 1000), offset);
76-
}
77-
78-
@GetMapping("/nodes/find")
79-
public List<Map<String, Object>> findNode(@RequestParam String q) {
80-
requireQueryService();
81-
return queryService.searchGraph(q, 50);
80+
if (kind != null) {
81+
validateNodeKind(kind);
82+
}
83+
return queryService.listNodes(kind, Math.min(limit, 1000), Math.max(0, offset));
8284
}
8385

8486
@GetMapping("/nodes/{nodeId}/detail")
@@ -105,7 +107,7 @@ public Map<String, Object> listEdges(
105107
@RequestParam(defaultValue = "100") int limit,
106108
@RequestParam(defaultValue = "0") int offset) {
107109
requireQueryService();
108-
return queryService.listEdges(kind, Math.min(limit, 1000), offset);
110+
return queryService.listEdges(kind, Math.min(limit, 1000), Math.max(0, offset));
109111
}
110112

111113
@GetMapping("/ego/{center}")
@@ -197,6 +199,18 @@ public List<Map<String, Object>> searchGraph(
197199
return queryService.searchGraph(q, Math.min(limit, 1000));
198200
}
199201

202+
private void validateNodeKind(String kind) {
203+
try {
204+
NodeKind.fromValue(kind);
205+
} catch (IllegalArgumentException e) {
206+
String valid = Arrays.stream(NodeKind.values())
207+
.map(NodeKind::getValue)
208+
.collect(Collectors.joining(", "));
209+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
210+
"Invalid node kind: '" + kind + "'. Valid values: " + valid);
211+
}
212+
}
213+
200214
private void requireQueryService() {
201215
if (queryService == null) {
202216
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,

src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ public class CodeIqConfig {
2525
/** Batch size for file processing during indexing (files per H2 flush). */
2626
private int batchSize = 500;
2727

28+
/** Graph configuration sub-properties. */
29+
private Graph graph = new Graph();
30+
31+
public static class Graph {
32+
private String path = ".osscodeiq/graph.db";
33+
34+
public String getPath() { return path; }
35+
public void setPath(String path) { this.path = path; }
36+
}
37+
2838
/** Service name tag for multi-repo graph mode. */
2939
private String serviceName;
3040

@@ -77,4 +87,12 @@ public String getServiceName() {
7787
public void setServiceName(String serviceName) {
7888
this.serviceName = serviceName;
7989
}
90+
91+
public Graph getGraph() {
92+
return graph;
93+
}
94+
95+
public void setGraph(Graph graph) {
96+
this.graph = graph;
97+
}
8098
}

src/main/java/io/github/randomcodespace/iq/config/Neo4jConfig.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ public class Neo4jConfig {
3434
private int boltPort;
3535

3636
@Bean(destroyMethod = "shutdown")
37-
DatabaseManagementService databaseManagementService(
38-
@Value("${codeiq.graph.path:.osscodeiq/graph.db}") String dbPath) {
39-
return new DatabaseManagementServiceBuilder(Path.of(dbPath))
37+
DatabaseManagementService databaseManagementService(CodeIqConfig config) {
38+
return new DatabaseManagementServiceBuilder(Path.of(config.getGraph().getPath()))
4039
.setConfig(BoltConnector.enabled, true)
4140
.setConfig(BoltConnector.listen_address, new SocketAddress("localhost", boltPort))
4241
.build();

src/main/resources/application.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ spring:
33
name: code-iq
44
main:
55
banner-mode: off
6-
allow-bean-definition-overriding: true
6+
allow-bean-definition-overriding: false
77
threads:
88
virtual:
99
enabled: true

src/test/java/io/github/randomcodespace/iq/api/GraphControllerTest.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,34 @@ void nodesByKindShouldAcceptPaginationParams() throws Exception {
114114
.andExpect(jsonPath("$.limit").value(25));
115115
}
116116

117+
@Test
118+
void nodesByKindShouldReturn400ForInvalidKind() throws Exception {
119+
mockMvc.perform(get("/api/kinds/not_a_real_kind"))
120+
.andExpect(status().isBadRequest());
121+
}
122+
123+
@Test
124+
void nodesByKindShouldClampNegativeOffset() throws Exception {
125+
Map<String, Object> result = new LinkedHashMap<>();
126+
result.put("kind", "endpoint");
127+
result.put("nodes", List.of());
128+
when(queryService.nodesByKind("endpoint", 50, 0)).thenReturn(result);
129+
130+
mockMvc.perform(get("/api/kinds/endpoint?offset=-5"))
131+
.andExpect(status().isOk());
132+
}
133+
134+
@Test
135+
void nodesByKindShouldCapLimitTo1000() throws Exception {
136+
Map<String, Object> result = new LinkedHashMap<>();
137+
result.put("kind", "endpoint");
138+
result.put("nodes", List.of());
139+
when(queryService.nodesByKind("endpoint", 1000, 0)).thenReturn(result);
140+
141+
mockMvc.perform(get("/api/kinds/endpoint?limit=5000"))
142+
.andExpect(status().isOk());
143+
}
144+
117145
// --- /api/nodes ---
118146

119147
@Test
@@ -140,6 +168,34 @@ void listNodesShouldFilterByKind() throws Exception {
140168
.andExpect(jsonPath("$.count").value(0));
141169
}
142170

171+
@Test
172+
void listNodesShouldReturn400ForInvalidKind() throws Exception {
173+
mockMvc.perform(get("/api/nodes?kind=bogus_kind"))
174+
.andExpect(status().isBadRequest());
175+
}
176+
177+
@Test
178+
void listNodesShouldClampNegativeOffset() throws Exception {
179+
Map<String, Object> result = new LinkedHashMap<>();
180+
result.put("nodes", List.of());
181+
result.put("count", 0);
182+
when(queryService.listNodes(null, 100, 0)).thenReturn(result);
183+
184+
mockMvc.perform(get("/api/nodes?offset=-10"))
185+
.andExpect(status().isOk());
186+
}
187+
188+
@Test
189+
void listNodesShouldCapLimitTo1000() throws Exception {
190+
Map<String, Object> result = new LinkedHashMap<>();
191+
result.put("nodes", List.of());
192+
result.put("count", 0);
193+
when(queryService.listNodes(null, 1000, 0)).thenReturn(result);
194+
195+
mockMvc.perform(get("/api/nodes?limit=9999"))
196+
.andExpect(status().isOk());
197+
}
198+
143199
// --- /api/nodes/{nodeId}/detail ---
144200

145201
@Test
@@ -208,6 +264,30 @@ void listEdgesShouldReturnEdges() throws Exception {
208264
.andExpect(jsonPath("$.total").value(0));
209265
}
210266

267+
@Test
268+
void listEdgesShouldClampNegativeOffset() throws Exception {
269+
Map<String, Object> result = new LinkedHashMap<>();
270+
result.put("edges", List.of());
271+
result.put("count", 0);
272+
result.put("total", 0);
273+
when(queryService.listEdges(null, 100, 0)).thenReturn(result);
274+
275+
mockMvc.perform(get("/api/edges?offset=-3"))
276+
.andExpect(status().isOk());
277+
}
278+
279+
@Test
280+
void listEdgesShouldCapLimitTo1000() throws Exception {
281+
Map<String, Object> result = new LinkedHashMap<>();
282+
result.put("edges", List.of());
283+
result.put("count", 0);
284+
result.put("total", 0);
285+
when(queryService.listEdges(null, 1000, 0)).thenReturn(result);
286+
287+
mockMvc.perform(get("/api/edges?limit=5000"))
288+
.andExpect(status().isOk());
289+
}
290+
211291
// --- /api/ego/{center} ---
212292

213293
@Test

0 commit comments

Comments
 (0)