Skip to content

Commit c6f7266

Browse files
aksOpsclaude
andcommitted
Add 277 tests to reach 85% code coverage
- 29 REST API integration tests (routes.py 12% → 99%) - 41 KuzuDB backend tests (kuzu.py 0% → 85%+) - 32 SQLite backend tests - 24 Java detector tests (GraphQL, JMS, RabbitMQ, RMI) - 18 imports detector tests (Ruby, Swift, Perl, Lua, Dart, R) - 14 Kubernetes detector tests - 12 pyproject.toml detector tests - 21 detector utils tests - 15 graph views tests - 14 DOT output tests - 10 cache store tests - 9 change detector tests - 24 structured parser tests (Gradle, Properties, SQL, XML) - Fix: routes.py neighbor endpoint ordering (path collision) - Total: 1965 tests, 85% coverage (was 74%) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6e7b669 commit c6f7266

14 files changed

Lines changed: 3128 additions & 10 deletions

src/osscodeiq/server/routes.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ async def list_nodes(
3838
):
3939
return service.list_nodes(kind=kind, limit=limit, offset=offset)
4040

41+
# NOTE: /neighbors must be registered before the catch-all {node_id:path}
42+
# route, otherwise Starlette's greedy path matching swallows "/neighbors".
43+
44+
@router.get("/nodes/{node_id:path}/neighbors")
45+
async def get_neighbors(
46+
node_id: str,
47+
direction: str = "both",
48+
edge_kinds: str | None = None,
49+
):
50+
kinds = edge_kinds.split(",") if edge_kinds else None
51+
return service.get_neighbors(node_id, direction=direction, edge_kinds=kinds)
52+
4153
@router.get(
4254
"/nodes/{node_id:path}",
4355
responses={404: {"description": "Node not found"}},
@@ -56,16 +68,7 @@ async def list_edges(
5668
):
5769
return service.list_edges(kind=kind, limit=limit, offset=offset)
5870

59-
# ── Neighbors & Ego ──────────────────────────────────────────────────
60-
61-
@router.get("/nodes/{node_id:path}/neighbors")
62-
async def get_neighbors(
63-
node_id: str,
64-
direction: str = "both",
65-
edge_kinds: str | None = None,
66-
):
67-
kinds = edge_kinds.split(",") if edge_kinds else None
68-
return service.get_neighbors(node_id, direction=direction, edge_kinds=kinds)
71+
# ── Ego ──────────────────────────────────────────────────────────────
6972

7073
@router.get("/ego/{center:path}")
7174
async def get_ego(
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
"""Tests for Kubernetes manifest detector."""
2+
3+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
4+
from osscodeiq.detectors.config.kubernetes import KubernetesDetector
5+
from osscodeiq.models.graph import NodeKind, EdgeKind
6+
7+
8+
def _ctx(parsed_data, path="k8s/deploy.yaml"):
9+
return DetectorContext(
10+
file_path=path, language="yaml", content=b"",
11+
parsed_data=parsed_data, module_name="test",
12+
)
13+
14+
15+
def _yaml_single(doc):
16+
return {"type": "yaml", "data": doc}
17+
18+
19+
def _yaml_multi(docs):
20+
return {"type": "yaml_multi", "documents": docs}
21+
22+
23+
class TestKubernetesDetector:
24+
def setup_method(self):
25+
self.detector = KubernetesDetector()
26+
27+
def test_no_parsed_data(self):
28+
ctx = _ctx(None)
29+
result = self.detector.detect(ctx)
30+
assert len(result.nodes) == 0
31+
32+
def test_non_k8s_yaml(self):
33+
ctx = _ctx(_yaml_single({"kind": "NotKubernetes", "metadata": {"name": "test"}}))
34+
result = self.detector.detect(ctx)
35+
assert len(result.nodes) == 0
36+
37+
def test_deployment(self):
38+
doc = {
39+
"kind": "Deployment",
40+
"metadata": {"name": "web-app", "namespace": "prod", "labels": {"app": "web"}},
41+
"spec": {
42+
"selector": {"matchLabels": {"app": "web"}},
43+
"template": {
44+
"metadata": {"labels": {"app": "web"}},
45+
"spec": {
46+
"containers": [
47+
{
48+
"name": "web",
49+
"image": "nginx:1.21",
50+
"ports": [{"containerPort": 80, "protocol": "TCP"}],
51+
"env": [{"name": "ENV_VAR", "value": "val"}],
52+
}
53+
]
54+
},
55+
},
56+
},
57+
}
58+
result = self.detector.detect(_ctx(_yaml_single(doc)))
59+
infra = [n for n in result.nodes if n.kind == NodeKind.INFRA_RESOURCE]
60+
assert len(infra) == 1
61+
assert infra[0].label == "Deployment/web-app"
62+
assert infra[0].properties["namespace"] == "prod"
63+
64+
containers = [n for n in result.nodes if n.kind == NodeKind.CONFIG_KEY]
65+
assert len(containers) == 1
66+
assert containers[0].properties["image"] == "nginx:1.21"
67+
assert "80/TCP" in containers[0].properties["ports"]
68+
assert "ENV_VAR" in containers[0].properties["env_vars"]
69+
70+
def test_service_with_selector(self):
71+
docs = [
72+
{
73+
"kind": "Deployment",
74+
"metadata": {"name": "api"},
75+
"spec": {
76+
"selector": {"matchLabels": {"app": "api"}},
77+
"template": {"metadata": {"labels": {"app": "api"}}, "spec": {"containers": [{"name": "api", "image": "api:1"}]}},
78+
},
79+
},
80+
{
81+
"kind": "Service",
82+
"metadata": {"name": "api-svc"},
83+
"spec": {"selector": {"app": "api"}, "ports": [{"port": 80}]},
84+
},
85+
]
86+
result = self.detector.detect(_ctx(_yaml_multi(docs)))
87+
infra = [n for n in result.nodes if n.kind == NodeKind.INFRA_RESOURCE]
88+
assert len(infra) == 2
89+
depends = [e for e in result.edges if e.kind == EdgeKind.DEPENDS_ON]
90+
assert len(depends) == 1
91+
assert "app=api" in depends[0].label
92+
93+
def test_configmap(self):
94+
doc = {
95+
"kind": "ConfigMap",
96+
"metadata": {"name": "app-config", "namespace": "default"},
97+
}
98+
result = self.detector.detect(_ctx(_yaml_single(doc)))
99+
assert len(result.nodes) == 1
100+
assert result.nodes[0].label == "ConfigMap/app-config"
101+
102+
def test_pvc(self):
103+
doc = {
104+
"kind": "PersistentVolumeClaim",
105+
"metadata": {"name": "data-pvc"},
106+
"spec": {"accessModes": ["ReadWriteOnce"]},
107+
}
108+
result = self.detector.detect(_ctx(_yaml_single(doc)))
109+
assert len(result.nodes) == 1
110+
assert "PersistentVolumeClaim" in result.nodes[0].label
111+
112+
def test_cronjob(self):
113+
doc = {
114+
"kind": "CronJob",
115+
"metadata": {"name": "cleanup"},
116+
"spec": {
117+
"schedule": "0 2 * * *",
118+
"jobTemplate": {
119+
"spec": {
120+
"template": {
121+
"spec": {
122+
"containers": [{"name": "cleanup", "image": "busybox"}]
123+
}
124+
}
125+
}
126+
},
127+
},
128+
}
129+
result = self.detector.detect(_ctx(_yaml_single(doc)))
130+
infra = [n for n in result.nodes if n.kind == NodeKind.INFRA_RESOURCE]
131+
assert len(infra) == 1
132+
containers = [n for n in result.nodes if n.kind == NodeKind.CONFIG_KEY]
133+
assert len(containers) == 1
134+
assert containers[0].properties["image"] == "busybox"
135+
136+
def test_statefulset(self):
137+
doc = {
138+
"kind": "StatefulSet",
139+
"metadata": {"name": "db"},
140+
"spec": {
141+
"selector": {"matchLabels": {"app": "db"}},
142+
"template": {
143+
"metadata": {"labels": {"app": "db"}},
144+
"spec": {"containers": [{"name": "postgres", "image": "postgres:14"}]},
145+
},
146+
},
147+
}
148+
result = self.detector.detect(_ctx(_yaml_single(doc)))
149+
assert len([n for n in result.nodes if n.kind == NodeKind.INFRA_RESOURCE]) == 1
150+
assert len([n for n in result.nodes if n.kind == NodeKind.CONFIG_KEY]) == 1
151+
152+
def test_ingress_routes_to_service(self):
153+
docs = [
154+
{
155+
"kind": "Service",
156+
"metadata": {"name": "web-svc"},
157+
"spec": {"selector": {"app": "web"}},
158+
},
159+
{
160+
"kind": "Ingress",
161+
"metadata": {"name": "web-ingress"},
162+
"spec": {
163+
"rules": [
164+
{
165+
"http": {
166+
"paths": [
167+
{
168+
"path": "/",
169+
"backend": {"service": {"name": "web-svc", "port": {"number": 80}}},
170+
}
171+
]
172+
}
173+
}
174+
]
175+
},
176+
},
177+
]
178+
result = self.detector.detect(_ctx(_yaml_multi(docs)))
179+
connects = [e for e in result.edges if e.kind == EdgeKind.CONNECTS_TO]
180+
assert len(connects) == 1
181+
assert "web-svc" in connects[0].label
182+
183+
def test_pod(self):
184+
doc = {
185+
"kind": "Pod",
186+
"metadata": {"name": "debug-pod"},
187+
"spec": {"containers": [{"name": "debug", "image": "busybox"}]},
188+
}
189+
result = self.detector.detect(_ctx(_yaml_single(doc)))
190+
assert len([n for n in result.nodes if n.kind == NodeKind.INFRA_RESOURCE]) == 1
191+
assert len([n for n in result.nodes if n.kind == NodeKind.CONFIG_KEY]) == 1
192+
193+
def test_multi_doc_filters_non_k8s(self):
194+
docs = [
195+
{"kind": "Deployment", "metadata": {"name": "app"}, "spec": {"template": {"spec": {"containers": [{"name": "c", "image": "i"}]}}}},
196+
{"kind": "NotK8s", "metadata": {"name": "foo"}},
197+
{"something": "else"},
198+
]
199+
result = self.detector.detect(_ctx(_yaml_multi(docs)))
200+
infra = [n for n in result.nodes if n.kind == NodeKind.INFRA_RESOURCE]
201+
assert len(infra) == 1
202+
203+
def test_determinism(self):
204+
doc = {
205+
"kind": "Deployment",
206+
"metadata": {"name": "app"},
207+
"spec": {"template": {"spec": {"containers": [{"name": "c", "image": "img"}]}}},
208+
}
209+
r1 = self.detector.detect(_ctx(_yaml_single(doc)))
210+
r2 = self.detector.detect(_ctx(_yaml_single(doc)))
211+
assert [n.id for n in r1.nodes] == [n.id for n in r2.nodes]
212+
assert [(e.source, e.target) for e in r1.edges] == [(e.source, e.target) for e in r2.edges]
213+
214+
def test_ingress_default_backend(self):
215+
docs = [
216+
{"kind": "Service", "metadata": {"name": "default-svc"}, "spec": {}},
217+
{
218+
"kind": "Ingress",
219+
"metadata": {"name": "default-ingress"},
220+
"spec": {"defaultBackend": {"service": {"name": "default-svc", "port": {"number": 80}}}},
221+
},
222+
]
223+
result = self.detector.detect(_ctx(_yaml_multi(docs)))
224+
connects = [e for e in result.edges if e.kind == EdgeKind.CONNECTS_TO]
225+
assert len(connects) == 1
226+
227+
def test_init_containers(self):
228+
doc = {
229+
"kind": "Deployment",
230+
"metadata": {"name": "app"},
231+
"spec": {
232+
"template": {
233+
"spec": {
234+
"containers": [{"name": "main", "image": "app:1"}],
235+
"initContainers": [{"name": "init", "image": "init:1"}],
236+
}
237+
}
238+
},
239+
}
240+
result = self.detector.detect(_ctx(_yaml_single(doc)))
241+
containers = [n for n in result.nodes if n.kind == NodeKind.CONFIG_KEY]
242+
assert len(containers) == 2

0 commit comments

Comments
 (0)