Skip to content

Commit b198271

Browse files
aksOpsclaude
andcommitted
test(coverage): expand Python detector + FlowViews tests for SonarCloud coverage
Added comprehensive tests for 10 Python detectors and FlowViews, covering all major branches: AST/regex paths, node properties, edges, negative cases, and determinism. New FlowViewsTest covers all 5 view builders with a stub FlowDataSource. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d45153d commit b198271

11 files changed

Lines changed: 1948 additions & 7 deletions

src/test/java/io/github/randomcodespace/iq/detector/python/CeleryTaskDetectorTest.java

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,138 @@ def process_data(data):
8585
DetectorContext ctx = DetectorTestUtils.contextFor("tasks.py", "python", code);
8686
DetectorTestUtils.assertDeterministic(detector, ctx);
8787
}
88+
89+
@Test
90+
void detectsSharedTask() {
91+
String code = """
92+
@shared_task
93+
def cleanup():
94+
pass
95+
""";
96+
DetectorContext ctx = DetectorTestUtils.contextFor("tasks.py", "python", code);
97+
DetectorResult result = detector.detect(ctx);
98+
99+
assertEquals(2, result.nodes().size());
100+
var queueNode = result.nodes().stream()
101+
.filter(n -> n.getKind() == NodeKind.QUEUE).findFirst().orElseThrow();
102+
assertEquals("celery", queueNode.getProperties().get("broker"));
103+
}
104+
105+
@Test
106+
void taskQueueNodeHasTaskNameProperty() {
107+
String code = """
108+
@app.task
109+
def process(data):
110+
pass
111+
""";
112+
DetectorContext ctx = DetectorTestUtils.contextFor("tasks.py", "python", code);
113+
DetectorResult result = detector.detect(ctx);
114+
115+
var queueNode = result.nodes().stream()
116+
.filter(n -> n.getKind() == NodeKind.QUEUE).findFirst().orElseThrow();
117+
assertEquals("process", queueNode.getProperties().get("task_name"));
118+
assertEquals("process", queueNode.getProperties().get("function"));
119+
}
120+
121+
@Test
122+
void taskMethodNodeHasFqn() {
123+
String code = """
124+
@app.task
125+
def my_task():
126+
pass
127+
""";
128+
DetectorContext ctx = DetectorTestUtils.contextFor("tasks.py", "python", code);
129+
DetectorResult result = detector.detect(ctx);
130+
131+
var methodNode = result.nodes().stream()
132+
.filter(n -> n.getKind() == NodeKind.METHOD).findFirst().orElseThrow();
133+
assertNotNull(methodNode.getFqn());
134+
assertTrue(methodNode.getFqn().contains("my_task"));
135+
}
136+
137+
@Test
138+
void consumesEdgeGoesFromMethodToQueue() {
139+
String code = """
140+
@app.task
141+
def my_task():
142+
pass
143+
""";
144+
DetectorContext ctx = DetectorTestUtils.contextFor("tasks.py", "python", code);
145+
DetectorResult result = detector.detect(ctx);
146+
147+
var consumesEdge = result.edges().stream()
148+
.filter(e -> e.getKind() == EdgeKind.CONSUMES).findFirst().orElseThrow();
149+
assertNotNull(consumesEdge.getSourceId());
150+
assertTrue(consumesEdge.getSourceId().startsWith("method:"));
151+
}
152+
153+
@Test
154+
void detectsApplyAsync() {
155+
String code = """
156+
send_email.apply_async(args=["user@test.com"], countdown=60)
157+
""";
158+
DetectorContext ctx = DetectorTestUtils.contextFor("views.py", "python", code);
159+
DetectorResult result = detector.detect(ctx);
160+
161+
assertEquals(1, result.edges().size());
162+
assertEquals(EdgeKind.PRODUCES, result.edges().get(0).getKind());
163+
}
164+
165+
@Test
166+
void detectsSignatureCall() {
167+
String code = """
168+
task_sig = my_task.s(arg1)
169+
""";
170+
DetectorContext ctx = DetectorTestUtils.contextFor("views.py", "python", code);
171+
DetectorResult result = detector.detect(ctx);
172+
173+
assertEquals(1, result.edges().size());
174+
assertEquals(EdgeKind.PRODUCES, result.edges().get(0).getKind());
175+
}
176+
177+
@Test
178+
void multipleTaskDefinitions() {
179+
String code = """
180+
@app.task
181+
def task_a():
182+
pass
183+
184+
@shared_task
185+
def task_b():
186+
pass
187+
""";
188+
DetectorContext ctx = DetectorTestUtils.contextFor("tasks.py", "python", code);
189+
DetectorResult result = detector.detect(ctx);
190+
191+
long queueCount = result.nodes().stream()
192+
.filter(n -> n.getKind() == NodeKind.QUEUE).count();
193+
long methodCount = result.nodes().stream()
194+
.filter(n -> n.getKind() == NodeKind.METHOD).count();
195+
assertEquals(2, queueCount);
196+
assertEquals(2, methodCount);
197+
}
198+
199+
@Test
200+
void noMatchOnEmptyContent() {
201+
DetectorContext ctx = DetectorTestUtils.contextFor("python", "");
202+
DetectorResult result = detector.detect(ctx);
203+
204+
assertEquals(0, result.nodes().size());
205+
assertEquals(0, result.edges().size());
206+
}
207+
208+
@Test
209+
void explicitTaskNameOverridesFunctionName() {
210+
String code = """
211+
@app.task(name='myapp.tasks.send_notification')
212+
def notify_user(user_id):
213+
pass
214+
""";
215+
DetectorContext ctx = DetectorTestUtils.contextFor("tasks.py", "python", code);
216+
DetectorResult result = detector.detect(ctx);
217+
218+
var queueNode = result.nodes().stream()
219+
.filter(n -> n.getKind() == NodeKind.QUEUE).findFirst().orElseThrow();
220+
assertEquals("celery:myapp.tasks.send_notification", queueNode.getLabel());
221+
}
88222
}

src/test/java/io/github/randomcodespace/iq/detector/python/DjangoAuthDetectorTest.java

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,149 @@ def view2(request):
9999
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
100100
DetectorTestUtils.assertDeterministic(detector, ctx);
101101
}
102+
103+
@Test
104+
void detectsPermissionRequiredMixin() {
105+
String code = """
106+
class AdminView(PermissionRequiredMixin, View):
107+
permission_required = 'app.can_admin'
108+
""";
109+
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
110+
DetectorResult result = detector.detect(ctx);
111+
112+
assertEquals(1, result.nodes().size());
113+
assertEquals("PermissionRequiredMixin", result.nodes().get(0).getProperties().get("mixin"));
114+
assertEquals("AdminView", result.nodes().get(0).getProperties().get("class_name"));
115+
}
116+
117+
@Test
118+
void detectsUserPassesTestMixin() {
119+
String code = """
120+
class StaffView(UserPassesTestMixin, View):
121+
def test_func(self):
122+
return self.request.user.is_staff
123+
""";
124+
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
125+
DetectorResult result = detector.detect(ctx);
126+
127+
assertEquals(1, result.nodes().size());
128+
assertEquals("UserPassesTestMixin", result.nodes().get(0).getProperties().get("mixin"));
129+
}
130+
131+
@Test
132+
void loginRequiredHasAuthType() {
133+
String code = """
134+
@login_required
135+
def secure_view(request):
136+
pass
137+
""";
138+
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
139+
DetectorResult result = detector.detect(ctx);
140+
141+
assertEquals("django", result.nodes().get(0).getProperties().get("auth_type"));
142+
assertEquals(true, result.nodes().get(0).getProperties().get("auth_required"));
143+
}
144+
145+
@Test
146+
void loginRequiredHasAnnotations() {
147+
String code = """
148+
@login_required
149+
def secured(request):
150+
pass
151+
""";
152+
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
153+
DetectorResult result = detector.detect(ctx);
154+
155+
var node = result.nodes().get(0);
156+
assertTrue(node.getAnnotations().contains("@login_required"));
157+
}
158+
159+
@Test
160+
void permissionRequiredHasPermissionsProperty() {
161+
String code = """
162+
@permission_required("myapp.view_report")
163+
def report_view(request):
164+
pass
165+
""";
166+
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
167+
DetectorResult result = detector.detect(ctx);
168+
169+
@SuppressWarnings("unchecked")
170+
List<String> perms = (List<String>) result.nodes().get(0).getProperties().get("permissions");
171+
assertNotNull(perms);
172+
assertFalse(perms.isEmpty());
173+
assertEquals("myapp.view_report", perms.get(0));
174+
}
175+
176+
@Test
177+
void userPassesTestHasTestFunctionProperty() {
178+
String code = """
179+
@user_passes_test(lambda u: u.is_active)
180+
def restricted_view(request):
181+
pass
182+
""";
183+
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
184+
DetectorResult result = detector.detect(ctx);
185+
186+
// test_function should be set from arg
187+
assertNotNull(result.nodes().get(0).getProperties().get("test_function"));
188+
}
189+
190+
@Test
191+
void mixinAnnotationFormat() {
192+
String code = """
193+
class SecureList(LoginRequiredMixin, ListView):
194+
model = Item
195+
""";
196+
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
197+
DetectorResult result = detector.detect(ctx);
198+
199+
var node = result.nodes().get(0);
200+
assertTrue(node.getAnnotations().stream().anyMatch(a -> a.contains("LoginRequiredMixin")));
201+
}
202+
203+
@Test
204+
void noMatchOnEmptyContent() {
205+
DetectorContext ctx = DetectorTestUtils.contextFor("python", "");
206+
DetectorResult result = detector.detect(ctx);
207+
208+
assertEquals(0, result.nodes().size());
209+
}
210+
211+
@Test
212+
void multipleDecoratorsCapturedSeparately() {
213+
String code = """
214+
@login_required
215+
def view_a(request):
216+
pass
217+
218+
@login_required
219+
def view_b(request):
220+
pass
221+
""";
222+
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
223+
DetectorResult result = detector.detect(ctx);
224+
225+
assertEquals(2, result.nodes().size());
226+
}
227+
228+
@Test
229+
void allAuthTypeIsDjango() {
230+
String code = """
231+
@login_required
232+
def v1(request): pass
233+
234+
@permission_required("x.y")
235+
def v2(request): pass
236+
237+
@user_passes_test(lambda u: True)
238+
def v3(request): pass
239+
""";
240+
DetectorContext ctx = DetectorTestUtils.contextFor("python", code);
241+
DetectorResult result = detector.detect(ctx);
242+
243+
assertEquals(3, result.nodes().size());
244+
assertTrue(result.nodes().stream()
245+
.allMatch(n -> "django".equals(n.getProperties().get("auth_type"))));
246+
}
102247
}

0 commit comments

Comments
 (0)