diff --git a/pom.xml b/pom.xml index 10b26e9a..16b0fa67 100644 --- a/pom.xml +++ b/pom.xml @@ -266,6 +266,12 @@ org.jacoco jacoco-maven-plugin ${jacoco.version} + + + + io/github/randomcodespace/iq/grammar/** + + prepare-agent diff --git a/src/test/java/io/github/randomcodespace/iq/analyzer/linker/LinkersCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/analyzer/linker/LinkersCoverageTest.java new file mode 100644 index 00000000..07e8a171 --- /dev/null +++ b/src/test/java/io/github/randomcodespace/iq/analyzer/linker/LinkersCoverageTest.java @@ -0,0 +1,377 @@ +package io.github.randomcodespace.iq.analyzer.linker; + +import io.github.randomcodespace.iq.model.CodeEdge; +import io.github.randomcodespace.iq.model.CodeNode; +import io.github.randomcodespace.iq.model.EdgeKind; +import io.github.randomcodespace.iq.model.NodeKind; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Additional coverage tests for linker classes — branches not hit by + * existing tests. + */ +class LinkersCoverageTest { + + // ===================================================================== + // GuardLinker + // ===================================================================== + @Nested + class GuardLinkerCoverage { + private final GuardLinker linker = new GuardLinker(); + + @Test + void linksGuardToEndpointInSameFile() { + var guard = new CodeNode("guard:auth1", NodeKind.GUARD, "AuthGuard"); + guard.setFilePath("src/UserController.java"); + + var endpoint = new CodeNode("ep:getUser", NodeKind.ENDPOINT, "GET /users/{id}"); + endpoint.setFilePath("src/UserController.java"); + + LinkResult result = linker.link(List.of(guard, endpoint), List.of()); + + assertEquals(1, result.edges().size()); + CodeEdge edge = result.edges().getFirst(); + assertEquals(EdgeKind.PROTECTS, edge.getKind()); + assertEquals("guard:auth1", edge.getSourceId()); + assertEquals("ep:getUser", edge.getTarget().getId()); + assertEquals(true, edge.getProperties().get("inferred")); + } + + @Test + void linksMiddlewareToEndpoint() { + var middleware = new CodeNode("mw:jwt", NodeKind.MIDDLEWARE, "JwtMiddleware"); + middleware.setFilePath("src/SecureController.java"); + + var endpoint = new CodeNode("ep:secure", NodeKind.ENDPOINT, "POST /secure"); + endpoint.setFilePath("src/SecureController.java"); + + LinkResult result = linker.link(List.of(middleware, endpoint), List.of()); + + assertEquals(1, result.edges().size()); + assertEquals(EdgeKind.PROTECTS, result.edges().getFirst().getKind()); + } + + @Test + void noLinkBetweenDifferentFiles() { + var guard = new CodeNode("guard:g1", NodeKind.GUARD, "Guard"); + guard.setFilePath("src/GuardConfig.java"); + + var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /data"); + endpoint.setFilePath("src/DataController.java"); + + LinkResult result = linker.link(List.of(guard, endpoint), List.of()); + + assertTrue(result.edges().isEmpty()); + } + + @Test + void noGuardsReturnsEmpty() { + var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /x"); + endpoint.setFilePath("src/Ctrl.java"); + + LinkResult result = linker.link(List.of(endpoint), List.of()); + assertTrue(result.edges().isEmpty()); + } + + @Test + void noEndpointsReturnsEmpty() { + var guard = new CodeNode("g:g1", NodeKind.GUARD, "G"); + guard.setFilePath("src/Ctrl.java"); + + LinkResult result = linker.link(List.of(guard), List.of()); + assertTrue(result.edges().isEmpty()); + } + + @Test + void nodeWithNullFilePathSkipped() { + var guard = new CodeNode("g:g1", NodeKind.GUARD, "G"); + // no filePath set + + var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /x"); + // no filePath set + + LinkResult result = linker.link(List.of(guard, endpoint), List.of()); + assertTrue(result.edges().isEmpty()); + } + + @Test + void nodeWithBlankFilePathSkipped() { + var guard = new CodeNode("g:g1", NodeKind.GUARD, "G"); + guard.setFilePath(" "); + + var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /x"); + endpoint.setFilePath(" "); + + LinkResult result = linker.link(List.of(guard, endpoint), List.of()); + assertTrue(result.edges().isEmpty()); + } + + @Test + void avoidsDuplicateProtectsEdges() { + var guard = new CodeNode("g:g1", NodeKind.GUARD, "G"); + guard.setFilePath("src/Ctrl.java"); + + var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /x"); + endpoint.setFilePath("src/Ctrl.java"); + + // Pre-existing PROTECTS edge + var existing = new CodeEdge(); + existing.setId("existing"); + existing.setKind(EdgeKind.PROTECTS); + existing.setSourceId("g:g1"); + existing.setTarget(endpoint); + + LinkResult result = linker.link(List.of(guard, endpoint), List.of(existing)); + assertTrue(result.edges().isEmpty()); + } + + @Test + void multipleGuardsAndEndpointsCrossLinked() { + var guard1 = new CodeNode("g:g1", NodeKind.GUARD, "Auth"); + guard1.setFilePath("src/Ctrl.java"); + var guard2 = new CodeNode("g:g2", NodeKind.MIDDLEWARE, "Logging"); + guard2.setFilePath("src/Ctrl.java"); + var ep1 = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /a"); + ep1.setFilePath("src/Ctrl.java"); + var ep2 = new CodeNode("ep:e2", NodeKind.ENDPOINT, "POST /b"); + ep2.setFilePath("src/Ctrl.java"); + + LinkResult result = linker.link(List.of(guard1, guard2, ep1, ep2), List.of()); + // 2 guards x 2 endpoints = 4 edges + assertEquals(4, result.edges().size()); + } + + @Test + void deterministic() { + var guard = new CodeNode("g:g1", NodeKind.GUARD, "G"); + guard.setFilePath("f.java"); + var ep1 = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /a"); + ep1.setFilePath("f.java"); + var ep2 = new CodeNode("ep:e2", NodeKind.ENDPOINT, "GET /b"); + ep2.setFilePath("f.java"); + + LinkResult r1 = linker.link(List.of(guard, ep1, ep2), List.of()); + LinkResult r2 = linker.link(List.of(guard, ep1, ep2), List.of()); + + assertEquals(r1.edges().size(), r2.edges().size()); + for (int i = 0; i < r1.edges().size(); i++) { + assertEquals(r1.edges().get(i).getId(), r2.edges().get(i).getId()); + } + } + } + + // ===================================================================== + // EntityLinker — additional branches + // ===================================================================== + @Nested + class EntityLinkerCoverage { + private final EntityLinker linker = new EntityLinker(); + + @Test + void matchesFqnSimpleNameForEntity() { + // Entity has fqn "com.example.User" — repo uses label "User" + var entity = new CodeNode("entity:com.example.User", NodeKind.ENTITY, "UserEntity"); + entity.setFqn("com.example.User"); + var repo = new CodeNode("repo:UserRepository", NodeKind.REPOSITORY, "UserRepository"); + + LinkResult result = linker.link(List.of(entity, repo), List.of()); + + // "user" (from fqn "com.example.User" -> "user") should match "user" (from "UserRepository" - "Repository") + assertEquals(1, result.edges().size()); + } + + @Test + void matchesByLabelLowercase() { + var entity = new CodeNode("entity:Product", NodeKind.ENTITY, "Product"); + var repo = new CodeNode("repo:ProductRepository", NodeKind.REPOSITORY, "ProductRepository"); + + LinkResult result = linker.link(List.of(entity, repo), List.of()); + assertEquals(1, result.edges().size()); + assertEquals("entity:Product", result.edges().getFirst().getTarget().getId()); + } + + @Test + void multipleReposForSameEntity() { + var entity = new CodeNode("entity:Order", NodeKind.ENTITY, "Order"); + var repo1 = new CodeNode("repo:OrderRepository", NodeKind.REPOSITORY, "OrderRepository"); + var repo2 = new CodeNode("repo:OrderRepo", NodeKind.REPOSITORY, "OrderRepo"); + + LinkResult result = linker.link(List.of(entity, repo1, repo2), List.of()); + assertEquals(2, result.edges().size()); + } + + @Test + void entityWithNullFqnUsesLabel() { + var entity = new CodeNode("entity:Item", NodeKind.ENTITY, "Item"); + // fqn is null + var repo = new CodeNode("repo:ItemDao", NodeKind.REPOSITORY, "ItemDao"); + + LinkResult result = linker.link(List.of(entity, repo), List.of()); + assertEquals(1, result.edges().size()); + } + + @Test + void noPrefixMatchSkips() { + // "SalesDAO" doesn't match "Product" entity + var entity = new CodeNode("entity:Product", NodeKind.ENTITY, "Product"); + var repo = new CodeNode("repo:SalesDAO", NodeKind.REPOSITORY, "SalesDAO"); + + LinkResult result = linker.link(List.of(entity, repo), List.of()); + assertTrue(result.edges().isEmpty()); + } + + @Test + void deterministic() { + var entity1 = new CodeNode("entity:Alpha", NodeKind.ENTITY, "Alpha"); + var entity2 = new CodeNode("entity:Beta", NodeKind.ENTITY, "Beta"); + var repo1 = new CodeNode("repo:AlphaRepository", NodeKind.REPOSITORY, "AlphaRepository"); + var repo2 = new CodeNode("repo:BetaRepo", NodeKind.REPOSITORY, "BetaRepo"); + + LinkResult r1 = linker.link(List.of(entity1, entity2, repo1, repo2), List.of()); + LinkResult r2 = linker.link(List.of(entity1, entity2, repo1, repo2), List.of()); + + assertEquals(r1.edges().size(), r2.edges().size()); + for (int i = 0; i < r1.edges().size(); i++) { + assertEquals(r1.edges().get(i).getId(), r2.edges().get(i).getId()); + } + } + } + + // ===================================================================== + // ModuleContainmentLinker — additional branches + // ===================================================================== + @Nested + class ModuleContainmentCoverage { + private final ModuleContainmentLinker linker = new ModuleContainmentLinker(); + + @Test + void multipleKindsInSameModule() { + var cls = new CodeNode("cls:A", NodeKind.CLASS, "A"); + cls.setModule("org.example"); + var iface = new CodeNode("iface:B", NodeKind.INTERFACE, "B"); + iface.setModule("org.example"); + var enm = new CodeNode("enum:C", NodeKind.ENUM, "C"); + enm.setModule("org.example"); + + LinkResult result = linker.link(List.of(cls, iface, enm), List.of()); + + assertEquals(1, result.nodes().size()); // one module node + assertEquals(3, result.edges().size()); // 3 CONTAINS edges + } + + @Test + void nullModuleSkipped() { + var node = new CodeNode("cls:A", NodeKind.CLASS, "A"); + // module not set — should be null + + LinkResult result = linker.link(List.of(node), List.of()); + assertTrue(result.nodes().isEmpty()); + assertTrue(result.edges().isEmpty()); + } + + @Test + void deterministic() { + var n1 = new CodeNode("cls:X", NodeKind.CLASS, "X"); + n1.setModule("com.mod"); + var n2 = new CodeNode("cls:Y", NodeKind.CLASS, "Y"); + n2.setModule("com.mod"); + + LinkResult r1 = linker.link(List.of(n1, n2), List.of()); + LinkResult r2 = linker.link(List.of(n1, n2), List.of()); + + assertEquals(r1.nodes().size(), r2.nodes().size()); + assertEquals(r1.edges().size(), r2.edges().size()); + } + } + + // ===================================================================== + // TopicLinker — additional branches + // ===================================================================== + @Nested + class TopicLinkerCoverage { + private final TopicLinker linker = new TopicLinker(); + + @Test + void emptyNodesAndEdgesReturnsEmpty() { + LinkResult result = linker.link(List.of(), List.of()); + assertTrue(result.edges().isEmpty()); + assertTrue(result.nodes().isEmpty()); + } + + @Test + void topicWithNoProducersOrConsumersReturnsEmpty() { + var topic = new CodeNode("topic:orphan", NodeKind.TOPIC, "orphan"); + LinkResult result = linker.link(List.of(topic), List.of()); + assertTrue(result.edges().isEmpty()); + } + + @Test + void multipleConsumersForOneTopic() { + var topic = new CodeNode("topic:updates", NodeKind.TOPIC, "updates"); + var producer = new CodeNode("svc:Prod", NodeKind.CLASS, "Prod"); + var consumer1 = new CodeNode("svc:Con1", NodeKind.CLASS, "Con1"); + var consumer2 = new CodeNode("svc:Con2", NodeKind.CLASS, "Con2"); + + var producesEdge = new CodeEdge(); + producesEdge.setId("e1"); + producesEdge.setKind(EdgeKind.PRODUCES); + producesEdge.setSourceId("svc:Prod"); + producesEdge.setTarget(topic); + + var consumesEdge1 = new CodeEdge(); + consumesEdge1.setId("e2"); + consumesEdge1.setKind(EdgeKind.CONSUMES); + consumesEdge1.setSourceId("svc:Con1"); + consumesEdge1.setTarget(topic); + + var consumesEdge2 = new CodeEdge(); + consumesEdge2.setId("e3"); + consumesEdge2.setKind(EdgeKind.CONSUMES); + consumesEdge2.setSourceId("svc:Con2"); + consumesEdge2.setTarget(topic); + + LinkResult result = linker.link( + List.of(topic, producer, consumer1, consumer2), + List.of(producesEdge, consumesEdge1, consumesEdge2)); + + assertEquals(2, result.edges().size()); + } + + @Test + void multipleProducersForOneTopic() { + var topic = new CodeNode("topic:orders", NodeKind.TOPIC, "orders"); + var prod1 = new CodeNode("svc:Prod1", NodeKind.CLASS, "Prod1"); + var prod2 = new CodeNode("svc:Prod2", NodeKind.CLASS, "Prod2"); + var consumer = new CodeNode("svc:Con", NodeKind.CLASS, "Con"); + + var e1 = new CodeEdge(); + e1.setId("e1"); + e1.setKind(EdgeKind.PRODUCES); + e1.setSourceId("svc:Prod1"); + e1.setTarget(topic); + + var e2 = new CodeEdge(); + e2.setId("e2"); + e2.setKind(EdgeKind.PRODUCES); + e2.setSourceId("svc:Prod2"); + e2.setTarget(topic); + + var e3 = new CodeEdge(); + e3.setId("e3"); + e3.setKind(EdgeKind.CONSUMES); + e3.setSourceId("svc:Con"); + e3.setTarget(topic); + + LinkResult result = linker.link( + List.of(topic, prod1, prod2, consumer), + List.of(e1, e2, e3)); + + assertEquals(2, result.edges().size()); + } + } +} diff --git a/src/test/java/io/github/randomcodespace/iq/cache/CacheCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/cache/CacheCoverageTest.java new file mode 100644 index 00000000..a34839da --- /dev/null +++ b/src/test/java/io/github/randomcodespace/iq/cache/CacheCoverageTest.java @@ -0,0 +1,245 @@ +package io.github.randomcodespace.iq.cache; + +import io.github.randomcodespace.iq.model.CodeEdge; +import io.github.randomcodespace.iq.model.CodeNode; +import io.github.randomcodespace.iq.model.EdgeKind; +import io.github.randomcodespace.iq.model.NodeKind; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Additional coverage tests for the cache package — branches not hit by + * existing tests. + */ +class CacheCoverageTest { + + // ===================================================================== + // FileHasher + // ===================================================================== + @Nested + class FileHasherCoverage { + + @Test + void hashEmptyFile(@TempDir Path tempDir) throws IOException { + Path empty = tempDir.resolve("empty.txt"); + Files.writeString(empty, "", StandardCharsets.UTF_8); + + String hash = FileHasher.hash(empty); + assertNotNull(hash); + assertEquals(32, hash.length()); + assertTrue(hash.matches("[0-9a-f]+")); + } + + @Test + void hashEmptyString() { + String hash = FileHasher.hashString(""); + assertNotNull(hash); + assertEquals(32, hash.length()); + } + + @Test + void hashOfSameStringIsAlwaysSame() { + String s = "deterministic content"; + assertEquals(FileHasher.hashString(s), FileHasher.hashString(s)); + } + + @Test + void hashDiffersForUnicode() { + String a = "hello"; + String b = "héllo"; // accented é + assertNotEquals(FileHasher.hashString(a), FileHasher.hashString(b)); + } + + @Test + void hashLargeContent() { + // 1 MB string + String large = "x".repeat(1_000_000); + String hash = FileHasher.hashString(large); + assertEquals(32, hash.length()); + } + + @Test + void hashFileAndStringProduceSameResultForSameContent(@TempDir Path tempDir) throws IOException { + String content = "match me"; + Path file = tempDir.resolve("match.txt"); + Files.writeString(file, content, StandardCharsets.UTF_8); + + String fileHash = FileHasher.hash(file); + String stringHash = FileHasher.hashString(content); + assertEquals(fileHash, stringHash); + } + } + + // ===================================================================== + // AnalysisCache — additional branches + // ===================================================================== + @Nested + class AnalysisCacheCoverage { + + private AnalysisCache cache; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + cache = new AnalysisCache(tempDir.resolve("cov-test.db")); + } + + @AfterEach + void tearDown() { + if (cache != null) cache.close(); + } + + @Test + void storeAndLoadNodeWithAllProperties() { + CodeNode node = new CodeNode("cls:X", NodeKind.CLASS, "X"); + node.setFqn("com.example.X"); + node.setFilePath("src/X.java"); + node.setModule("com.example"); + node.setLayer("backend"); + node.setLineStart(10); + node.setLineEnd(50); + node.setAnnotations(List.of("@Service", "@Transactional")); + node.setProperties(Map.of("framework", "spring_boot", "layer", "backend")); + + cache.storeResults("hash-full", "src/X.java", "java", List.of(node), List.of()); + var result = cache.loadCachedResults("hash-full"); + + assertNotNull(result); + CodeNode loaded = result.nodes().getFirst(); + assertEquals("com.example.X", loaded.getFqn()); + assertEquals("com.example", loaded.getModule()); + assertEquals("backend", loaded.getLayer()); + assertEquals(10, loaded.getLineStart()); + assertEquals(50, loaded.getLineEnd()); + assertTrue(loaded.getAnnotations().contains("@Service")); + assertEquals("spring_boot", loaded.getProperties().get("framework")); + } + + @Test + void storeEdgeAndLoadBackWithProperties() { + CodeNode src = new CodeNode("cls:A", NodeKind.CLASS, "A"); + CodeNode tgt = new CodeNode("cls:B", NodeKind.CLASS, "B"); + CodeEdge edge = new CodeEdge("e:A->B", EdgeKind.DEPENDS_ON, "cls:A", tgt); + edge.setProperties(Map.of("inferred", true, "reason", "naming_convention")); + + cache.storeResults("hash-edge", "src/A.java", "java", + List.of(src, tgt), List.of(edge)); + var result = cache.loadCachedResults("hash-edge"); + + assertNotNull(result); + assertEquals(1, result.edges().size()); + CodeEdge loaded = result.edges().getFirst(); + assertEquals(EdgeKind.DEPENDS_ON, loaded.getKind()); + assertEquals("cls:A", loaded.getSourceId()); + assertEquals("cls:B", loaded.getTarget().getId()); + } + + @Test + void multipleStoresAndLoadsSeparateHashes() { + var nodeA = new CodeNode("n:A", NodeKind.CLASS, "A"); + var nodeB = new CodeNode("n:B", NodeKind.CLASS, "B"); + + cache.storeResults("hash-a", "A.java", "java", List.of(nodeA), List.of()); + cache.storeResults("hash-b", "B.java", "java", List.of(nodeB), List.of()); + + var rA = cache.loadCachedResults("hash-a"); + var rB = cache.loadCachedResults("hash-b"); + + assertNotNull(rA); + assertNotNull(rB); + assertEquals("n:A", rA.nodes().getFirst().getId()); + assertEquals("n:B", rB.nodes().getFirst().getId()); + } + + @Test + void clearResetsAllCounters() { + var n = new CodeNode("n:X", NodeKind.CLASS, "X"); + cache.storeResults("h1", "X.java", "java", List.of(n), List.of()); + cache.storeResults("h2", "Y.java", "java", List.of(n), List.of()); + cache.recordRun("sha1", 5); + + cache.clear(); + + var stats = cache.getStats(); + assertEquals(0L, stats.get("cached_files")); + assertEquals(0L, stats.get("cached_nodes")); + assertEquals(0L, stats.get("total_runs")); + assertNull(cache.getLastCommit()); + } + + @Test + void multipleRunsGetLastCommitReturnsLatest() { + cache.recordRun("sha-first", 1); + cache.recordRun("sha-second", 2); + cache.recordRun("sha-third", 3); + + assertEquals("sha-third", cache.getLastCommit()); + } + + @Test + void isCachedReturnsTrueAfterStore() { + var n = new CodeNode("n:A", NodeKind.CLASS, "A"); + cache.storeResults("tracked-hash", "A.java", "java", List.of(n), List.of()); + assertTrue(cache.isCached("tracked-hash")); + } + + @Test + void isCachedReturnsFalseForRemovedFile() { + var n = new CodeNode("n:A", NodeKind.CLASS, "A"); + cache.storeResults("remove-hash", "A.java", "java", List.of(n), List.of()); + cache.removeFile("remove-hash"); + assertFalse(cache.isCached("remove-hash")); + } + + @Test + void storeEmptyNodeListIsCachedButLoadReturnsNull() { + // Store with empty nodes and edges — isCached checks the files table + cache.storeResults("empty-nodes-hash", "Empty.java", "java", List.of(), List.of()); + // The file entry is recorded + assertTrue(cache.isCached("empty-nodes-hash")); + // But loadCachedResults returns null when nodes AND edges are both empty + var result = cache.loadCachedResults("empty-nodes-hash"); + assertNull(result); + } + + @Test + void statsTotalNodesAcrossMultipleFiles() { + var n1 = new CodeNode("n:1", NodeKind.CLASS, "A"); + var n2 = new CodeNode("n:2", NodeKind.CLASS, "B"); + var n3 = new CodeNode("n:3", NodeKind.METHOD, "m"); + + cache.storeResults("file1", "A.java", "java", List.of(n1, n2), List.of()); + cache.storeResults("file2", "B.java", "java", List.of(n3), List.of()); + + var stats = cache.getStats(); + assertEquals(2L, stats.get("cached_files")); + assertEquals(3L, stats.get("cached_nodes")); + } + + @Test + void deterministic() { + var n = new CodeNode("n:D", NodeKind.CLASS, "D"); + cache.storeResults("det-hash", "D.java", "java", List.of(n), List.of()); + + var r1 = cache.loadCachedResults("det-hash"); + var r2 = cache.loadCachedResults("det-hash"); + + assertNotNull(r1); + assertNotNull(r2); + assertEquals(r1.nodes().size(), r2.nodes().size()); + assertEquals(r1.nodes().getFirst().getId(), r2.nodes().getFirst().getId()); + } + } +} diff --git a/src/test/java/io/github/randomcodespace/iq/detector/auth/AuthDetectorsCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/detector/auth/AuthDetectorsCoverageTest.java new file mode 100644 index 00000000..73e8413b --- /dev/null +++ b/src/test/java/io/github/randomcodespace/iq/detector/auth/AuthDetectorsCoverageTest.java @@ -0,0 +1,471 @@ +package io.github.randomcodespace.iq.detector.auth; + +import io.github.randomcodespace.iq.detector.DetectorContext; +import io.github.randomcodespace.iq.detector.DetectorResult; +import io.github.randomcodespace.iq.detector.DetectorTestUtils; +import io.github.randomcodespace.iq.model.NodeKind; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Additional coverage for auth detectors — branches not hit by existing tests. + */ +class AuthDetectorsCoverageTest { + + // ===================================================================== + // CertificateAuthDetector + // ===================================================================== + @Nested + class CertificateCoverage { + private final CertificateAuthDetector d = new CertificateAuthDetector(); + + @Test + void detectsRequestCertMtls() { + DetectorResult r = d.detect(ctx("typescript", "const opts = { requestCert: true, rejectUnauthorized: true };")); + assertFalse(r.nodes().isEmpty()); + assertEquals("mtls", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsClientAuthEqualsTrueMtls() { + // Pattern is: clientAuth = "true" (literal double-quote around true) + DetectorResult r = d.detect(ctx("yaml", "clientAuth = \"true\"")); + assertFalse(r.nodes().isEmpty()); + assertEquals("mtls", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsX509AuthenticationFilter() { + DetectorResult r = d.detect(ctx("java", + "http.addFilter(new X509AuthenticationFilter());")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsAddCertificateForwarding() { + DetectorResult r = d.detect(ctx("csharp", + "services.AddCertificateForwarding(opts => {});")); + assertFalse(r.nodes().isEmpty()); + assertEquals("mtls", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsCertificateAuthenticationDefaults() { + DetectorResult r = d.detect(ctx("csharp", + "services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme);")); + assertFalse(r.nodes().isEmpty()); + assertEquals("x509", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsX509Fluent() { + DetectorResult r = d.detect(ctx("java", + "auth.x509().subjectPrincipalRegex(\"CN=(.*?)(?:,|$)\");")); + assertFalse(r.nodes().isEmpty()); + assertEquals("x509", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsJavaxKeyStore() { + DetectorResult r = d.detect(ctx("java", + "System.setProperty(\"javax.net.ssl.keyStore\", \"/certs/server.jks\");")); + assertFalse(r.nodes().isEmpty()); + assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsSslSSLContext() { + DetectorResult r = d.detect(ctx("python", + "ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)")); + assertFalse(r.nodes().isEmpty()); + assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsTlsCreateServer() { + DetectorResult r = d.detect(ctx("typescript", + "const server = tls.createServer({ key, cert });")); + assertFalse(r.nodes().isEmpty()); + assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsCertPathInTlsConfig() { + DetectorResult r = d.detect(ctx("typescript", + "const cert = fs.readFileSync('/etc/certs/server.pem');")); + assertFalse(r.nodes().isEmpty()); + assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type")); + assertNotNull(r.nodes().get(0).getProperties().get("cert_path")); + } + + @Test + void detectsTrustStore() { + DetectorResult r = d.detect(ctx("java", "System.setProperty(\"trustStore\", \"/path/store.jks\");")); + assertFalse(r.nodes().isEmpty()); + assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsAzureAd() { + DetectorResult r = d.detect(ctx("csharp", + "// AzureAd section configured in appsettings.json")); + assertFalse(r.nodes().isEmpty()); + assertEquals("azure_ad", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsAzureTenantId() { + DetectorResult r = d.detect(ctx("yaml", + "AZURE_TENANT_ID: abc-def-123")); + assertFalse(r.nodes().isEmpty()); + assertEquals("azure_ad", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsClientCertificateCredentialWithAuthFlow() { + DetectorResult r = d.detect(ctx("csharp", + "var cred = new ClientCertificateCredential(tenantId, clientId, cert);")); + assertFalse(r.nodes().isEmpty()); + assertEquals("client_certificate", r.nodes().get(0).getProperties().get("auth_flow")); + } + + @Test + void detectsMsalWithMsalAuthFlow() { + DetectorResult r = d.detect(ctx("typescript", + "import * as msal from '@azure/msal-browser';")); + assertFalse(r.nodes().isEmpty()); + assertEquals("msal", r.nodes().get(0).getProperties().get("auth_flow")); + } + + @Test + void detectsAddMicrosoftIdentityWebApi() { + DetectorResult r = d.detect(ctx("csharp", + "services.AddMicrosoftIdentityWebApi(config);")); + assertFalse(r.nodes().isEmpty()); + assertEquals("azure_ad", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void multipleMatchesOnSameLineOnlyOneNode() { + // A line matching two patterns — only one node per line + DetectorResult r = d.detect(ctx("yaml", " AZURE_TENANT_ID: abc\n AZURE_CLIENT_ID: xyz")); + assertEquals(2, r.nodes().size()); // two lines, one match each + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("java", "")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorResult r = d.detect(new DetectorContext("test.java", "java", null)); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void deterministic() { + DetectorTestUtils.assertDeterministic(d, ctx("java", + "X509AuthenticationFilter f;\nssl_verify_client on;\ntrustStore=/path/store.jks")); + } + } + + // ===================================================================== + // LdapAuthDetector + // ===================================================================== + @Nested + class LdapCoverage { + private final LdapAuthDetector d = new LdapAuthDetector(); + + @Test + void detectsLdapTemplateInJava() { + DetectorResult r = d.detect(ctx("java", "@Autowired LdapTemplate ldapTemplate;")); + assertFalse(r.nodes().isEmpty()); + assertEquals("ldap", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsActiveDirectoryLdapAuthProviderInJava() { + DetectorResult r = d.detect(ctx("java", + "new ActiveDirectoryLdapAuthenticationProvider(domain, url)")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsEnableLdapRepositoriesInJava() { + DetectorResult r = d.detect(ctx("java", + "@EnableLdapRepositories\n@SpringBootApplication\npublic class App {}")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsLdap3ConnectionInPython() { + DetectorResult r = d.detect(ctx("python", + "conn = ldap3.Connection(server, user=dn, password=pw)")); + assertFalse(r.nodes().isEmpty()); + assertEquals("ldap", r.nodes().get(0).getProperties().get("auth_type")); + assertEquals("python", r.nodes().get(0).getProperties().get("language")); + } + + @Test + void detectsLdap3ServerInPython() { + DetectorResult r = d.detect(ctx("python", + "server = ldap3.Server('ldap.example.com', port=389)")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsAuthLdapBindDnInPython() { + DetectorResult r = d.detect(ctx("python", + "AUTH_LDAP_BIND_DN = 'cn=django-agent,dc=example,dc=com'")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsLdapjsRequireInTypeScript() { + DetectorResult r = d.detect(ctx("typescript", + "const ldap = require('ldapjs');")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsLdapjsImportInTypeScript() { + DetectorResult r = d.detect(ctx("typescript", + "import ldapjs from 'ldapjs';")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsPassportLdapauthInTypeScript() { + DetectorResult r = d.detect(ctx("typescript", + "const LdapStrategy = require('passport-ldapauth');")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsSystemDirectoryServicesInCSharp() { + DetectorResult r = d.detect(ctx("csharp", + "using System.DirectoryServices;")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsLdapConnectionInCSharp() { + DetectorResult r = d.detect(ctx("csharp", + "LdapConnection conn = new LdapConnection(new LdapDirectoryIdentifier(server));")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsDirectoryEntryInCSharp() { + DetectorResult r = d.detect(ctx("csharp", + "DirectoryEntry entry = new DirectoryEntry(path, user, password);")); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void unsupportedLanguageReturnsEmpty() { + DetectorResult r = d.detect(ctx("go", "ldap.Connect(\"server\", 389)")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("java", "")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorResult r = d.detect(new DetectorContext("f.java", "java", null)); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void deterministic() { + DetectorTestUtils.assertDeterministic(d, ctx("java", + "LdapContextSource src;\nLdapTemplate tmpl;\nActiveDirectoryLdapAuthenticationProvider prov;")); + } + } + + // ===================================================================== + // SessionHeaderAuthDetector + // ===================================================================== + @Nested + class SessionHeaderCoverage { + private final SessionHeaderAuthDetector d = new SessionHeaderAuthDetector(); + + @Test + void detectsCookieSession() { + DetectorResult r = d.detect(ctx("typescript", + "const session = require('cookie-session');")); + assertFalse(r.nodes().isEmpty()); + assertEquals("session", r.nodes().get(0).getProperties().get("auth_type")); + assertEquals(NodeKind.MIDDLEWARE, r.nodes().get(0).getKind()); + } + + @Test + void detectsSessionAttributesInJava() { + DetectorResult r = d.detect(ctx("java", + "@SessionAttributes(\"user\")\npublic class UserController {}")); + assertFalse(r.nodes().isEmpty()); + assertEquals("session", r.nodes().get(0).getProperties().get("auth_type")); + assertEquals(NodeKind.GUARD, r.nodes().get(0).getKind()); + } + + @Test + void detectsSessionMiddlewareInTypeScript() { + DetectorResult r = d.detect(ctx("typescript", + "app.use(SessionMiddleware({ secret: 'key' }));")); + assertFalse(r.nodes().isEmpty()); + assertEquals("session", r.nodes().get(0).getProperties().get("auth_type")); + assertEquals(NodeKind.MIDDLEWARE, r.nodes().get(0).getKind()); + } + + @Test + void detectsHttpSessionInJava() { + DetectorResult r = d.detect(ctx("java", + "HttpSession session = request.getSession();")); + assertFalse(r.nodes().isEmpty()); + assertEquals("session", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsSessionEngineInPython() { + DetectorResult r = d.detect(ctx("python", + "SESSION_ENGINE = 'django.contrib.sessions.backends.db'")); + assertFalse(r.nodes().isEmpty()); + assertEquals("session", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsXApiKeyHeader() { + DetectorResult r = d.detect(ctx("typescript", + "const key = req.headers['X-API-Key'];")); + assertFalse(r.nodes().isEmpty()); + assertEquals("header", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsAuthorizationHeaderAccess() { + DetectorResult r = d.detect(ctx("typescript", + "const token = req.headers['authorization'];")); + assertFalse(r.nodes().isEmpty()); + assertEquals("header", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsGetHeaderAuthorization() { + DetectorResult r = d.detect(ctx("java", + "String token = request.getHeader(\"Authorization\");")); + assertFalse(r.nodes().isEmpty()); + assertEquals("header", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsXApiKeyLowercase() { + // The HEADER pattern '['"X-API-Key'"]' (CASE_INSENSITIVE) fires before the API_KEY + // req.headers pattern, so 'x-api-key' is classified as "header" auth_type + DetectorResult r = d.detect(ctx("typescript", + "const key = req.headers['x-api-key'];")); + assertFalse(r.nodes().isEmpty()); + // Either header or api_key is valid — just verify detection occurred + String authType = (String) r.nodes().get(0).getProperties().get("auth_type"); + assertTrue("header".equals(authType) || "api_key".equals(authType)); + } + + @Test + void detectsApiKeyAssignment() { + // "api_key = " pattern (API_KEY) fires for this content + DetectorResult r = d.detect(ctx("python", + "api_key = os.getenv('SERVICE_KEY')")); + assertFalse(r.nodes().isEmpty()); + assertEquals("api_key", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsValidateApiKey() { + DetectorResult r = d.detect(ctx("python", + "if not validate_api_key(request):\n raise Unauthorized")); + assertFalse(r.nodes().isEmpty()); + assertEquals("api_key", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsCsrfProtectDecorator() { + DetectorResult r = d.detect(ctx("python", + "@csrf_protect\ndef my_view(request): pass")); + assertFalse(r.nodes().isEmpty()); + assertEquals("csrf", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsCsrfExempt() { + DetectorResult r = d.detect(ctx("python", + "@csrf_exempt\ndef public_api(request): pass")); + assertFalse(r.nodes().isEmpty()); + assertEquals("csrf", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsCsrfViewMiddleware() { + DetectorResult r = d.detect(ctx("python", + "MIDDLEWARE = ['django.middleware.csrf.CsrfViewMiddleware']")); + assertFalse(r.nodes().isEmpty()); + assertEquals("csrf", r.nodes().get(0).getProperties().get("auth_type")); + assertEquals(NodeKind.MIDDLEWARE, r.nodes().get(0).getKind()); + } + + @Test + void detectsCsurfMiddleware() { + DetectorResult r = d.detect(ctx("typescript", + "const csrf = require('csurf');")); + assertFalse(r.nodes().isEmpty()); + assertEquals("csrf", r.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void unsupportedLanguageReturnsEmpty() { + // csharp not in supported languages list + DetectorResult r = d.detect(ctx("csharp", + "app.UseSession();")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("java", "")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorResult r = d.detect(new DetectorContext("f.ts", "typescript", null)); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void multipleMatchesOnMultipleLines() { + DetectorResult r = d.detect(ctx("java", + "HttpSession s = req.getSession();\nrequest.getHeader(\"Authorization\");")); + assertEquals(2, r.nodes().size()); + } + + @Test + void deterministic() { + DetectorTestUtils.assertDeterministic(d, ctx("typescript", + "require('express-session');\nreq.headers['authorization'];\nrequire('csurf');")); + } + } + + // ===================================================================== + // Helpers + // ===================================================================== + + private static DetectorContext ctx(String language, String content) { + return DetectorTestUtils.contextFor(language, content); + } +} diff --git a/src/test/java/io/github/randomcodespace/iq/detector/cpp/CppStructuresDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/cpp/CppStructuresDetectorTest.java index be315dbf..f5773797 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/cpp/CppStructuresDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/cpp/CppStructuresDetectorTest.java @@ -1,11 +1,128 @@ package io.github.randomcodespace.iq.detector.cpp; -import io.github.randomcodespace.iq.detector.*; import io.github.randomcodespace.iq.model.*; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; + +import io.github.randomcodespace.iq.detector.DetectorContext; +import io.github.randomcodespace.iq.detector.DetectorResult; +import io.github.randomcodespace.iq.detector.DetectorTestUtils; +import io.github.randomcodespace.iq.model.EdgeKind; +import io.github.randomcodespace.iq.model.NodeKind; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + class CppStructuresDetectorTest { + private final CppStructuresDetector d = new CppStructuresDetector(); - @Test void detectsClassAndNamespace() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("cpp", "#include \nnamespace app {\nclass User : public Entity {\n};\n}")); + + @Test + void detectsClass() { + String code = "#include \nclass User {\npublic:\n std::string name;\n};\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "User".equals(n.getLabel()))); + } + + @Test + void detectsClassWithBaseClass() { + String code = "class AdminUser : public User {\n};\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.EXTENDS)); + } + + @Test + void detectsStruct() { + String code = "struct Point {\n int x;\n int y;\n};\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "Point".equals(n.getLabel()))); + } + + @Test + void detectsStructWithBase() { + String code = "struct ColoredPoint : public Point {\n int color;\n};\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.EXTENDS)); + } + + @Test + void detectsNamespace() { + String code = "namespace app {\n class Service {};\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE && "app".equals(n.getLabel()))); + } + + @Test + void detectsEnum() { + String code = "enum Status {\n ACTIVE,\n INACTIVE\n};\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENUM)); + } + + @Test + void detectsEnumClass() { + String code = "enum class Color {\n RED,\n GREEN,\n BLUE\n};\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENUM)); + } + + @Test + void detectsInclude() { + String code = "#include \n#include \n#include \"myheader.hpp\"\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(3, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count()); + } + + @Test + void detectsClassAndNamespace() { + DetectorResult r = d.detect(ctx("#include \nnamespace app {\nclass User : public Entity {\n};\n}")); assertTrue(r.nodes().size() >= 2); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("cpp", "")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("cpp", "#include \nclass A {\n};\nstruct B {\n};")); } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.cpp", "cpp", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("cpp_structures", d.getName()); + } + + @Test + void supportedLanguagesContainsCpp() { + assertTrue(d.getSupportedLanguages().contains("cpp")); + } + + @Test + void deterministic() { + String code = """ + #include + #include + namespace myapp { + class Vehicle { + public: + std::string make; + }; + class Car : public Vehicle { + }; + struct Point { + int x; + int y; + }; + enum class Color { RED, GREEN, BLUE }; + } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("cpp", content); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/csharp/CSharpDetectorsCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/detector/csharp/CSharpDetectorsCoverageTest.java new file mode 100644 index 00000000..1187075e --- /dev/null +++ b/src/test/java/io/github/randomcodespace/iq/detector/csharp/CSharpDetectorsCoverageTest.java @@ -0,0 +1,413 @@ +package io.github.randomcodespace.iq.detector.csharp; + +import io.github.randomcodespace.iq.detector.DetectorContext; +import io.github.randomcodespace.iq.detector.DetectorResult; +import io.github.randomcodespace.iq.detector.DetectorTestUtils; +import io.github.randomcodespace.iq.model.EdgeKind; +import io.github.randomcodespace.iq.model.NodeKind; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Additional coverage tests for C# detectors — branches not hit by existing tests. + */ +class CSharpDetectorsCoverageTest { + + // ===================================================================== + // CSharpStructuresDetector + // ===================================================================== + @Nested + class StructuresCoverage { + private final CSharpStructuresDetector d = new CSharpStructuresDetector(); + + @Test + void detectsNamespaceNode() { + String code = """ + namespace MyApp.Controllers + { + public class HomeController {} + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE)); + } + + @Test + void detectsUsingStatements() { + String code = """ + using System; + using System.Collections.Generic; + using Microsoft.AspNetCore.Mvc; + + public class UserController {} + """; + DetectorResult r = d.detect(ctx("csharp", code)); + // Edges for using statements + List importEdges = r.edges().stream() + .filter(e -> e.getKind() == EdgeKind.IMPORTS) + .toList(); + assertFalse(importEdges.isEmpty()); + } + + @Test + void detectsAbstractClass() { + String code = """ + public abstract class Animal + { + public abstract string Sound(); + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ABSTRACT_CLASS)); + } + + @Test + void detectsClassWithBaseClassAndInterface() { + String code = """ + public class UserService : BaseService, IUserService, IDisposable + { + public void Dispose() {} + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + // Should have EXTENDS and IMPLEMENTS edges + List extendsEdges = r.edges().stream() + .filter(e -> e.getKind() == EdgeKind.EXTENDS) + .toList(); + List implementsEdges = r.edges().stream() + .filter(e -> e.getKind() == EdgeKind.IMPLEMENTS) + .toList(); + assertFalse(extendsEdges.isEmpty()); + assertFalse(implementsEdges.isEmpty()); + } + + @Test + void detectsApiControllerEndpoints() { + String code = """ + using Microsoft.AspNetCore.Mvc; + + [ApiController] + [Route("api/[controller]")] + public class ProductsController : ControllerBase + { + [HttpGet] + public IActionResult GetAll() => Ok(); + + [HttpPost] + public IActionResult Create(Product p) => CreatedAtAction(nameof(GetAll), p); + + [HttpGet("{id}")] + public IActionResult GetById(int id) => Ok(); + + [HttpDelete("{id}")] + public IActionResult Delete(int id) => NoContent(); + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + long endpoints = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count(); + assertTrue(endpoints >= 3, "Expected at least 3 endpoint nodes, got " + endpoints); + } + + @Test + void detectsInterfaceWithGenericTypeParam() { + String code = """ + public interface IRepository where T : class + { + T FindById(int id); + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.INTERFACE)); + } + + @Test + void detectsEnum() { + String code = """ + public enum OrderStatus + { + Pending, + Processing, + Shipped, + Delivered + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENUM)); + } + + @Test + void detectsMultipleClassesInSameFile() { + String code = """ + public class Request { } + public class Response { } + public class Handler { } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + long classes = r.nodes().stream().filter(n -> n.getKind() == NodeKind.CLASS).count(); + assertEquals(3, classes); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("csharp", "")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorResult r = d.detect(new DetectorContext("f.cs", "csharp", null)); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void deterministic() { + DetectorTestUtils.assertDeterministic(d, ctx("csharp", """ + namespace App; + using System; + public class Foo : Bar, IBaz {} + public interface IBaz { void Run(); } + public enum Status { A, B, C } + """)); + } + } + + // ===================================================================== + // CSharpEfcoreDetector + // ===================================================================== + @Nested + class EfcoreCoverage { + private final CSharpEfcoreDetector d = new CSharpEfcoreDetector(); + + @Test + void detectsDbContextWithNamespaceQualifiedBase() { + // "Microsoft.EntityFrameworkCore.DbContext" + String code = """ + public class OrderContext : Microsoft.EntityFrameworkCore.DbContext + { + public DbSet Orders { get; set; } + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + assertFalse(r.nodes().isEmpty()); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.REPOSITORY)); + } + + @Test + void detectsDbSetAndCreatesQueriesEdge() { + String code = """ + public class AppContext : DbContext + { + public DbSet Customers { get; set; } + public DbSet Invoices { get; set; } + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + // Should have QUERIES edges + List queriesEdges = r.edges().stream() + .filter(e -> e.getKind() == EdgeKind.QUERIES) + .toList(); + assertEquals(2, queriesEdges.size()); + } + + @Test + void detectsMigrationClass() { + String code = """ + public class AddUserTable : Migration + { + protected override void Up(MigrationBuilder mb) {} + protected override void Down(MigrationBuilder mb) {} + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MIGRATION)); + assertEquals("AddUserTable", r.nodes().stream() + .filter(n -> n.getKind() == NodeKind.MIGRATION) + .findFirst() + .orElseThrow() + .getLabel()); + } + + @Test + void detectsCreateTableInMigration() { + String code = """ + public class InitialCreate : Migration + { + protected override void Up(MigrationBuilder mb) + { + mb.CreateTable(name: "Products", columns: table => new {}); + mb.CreateTable(name: "Categories", columns: table => new {}); + } + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + long entities = r.nodes().stream() + .filter(n -> n.getKind() == NodeKind.ENTITY) + .count(); + assertEquals(2, entities); + } + + @Test + void createTableWithExistingEntityNotDuplicated() { + // If DbSet already creates an ENTITY node, CreateTable("Products") should not duplicate + String code = """ + public class ShopCtx : DbContext + { + public DbSet Products { get; set; } + } + public class CreateProductsMigration : Migration + { + protected override void Up(MigrationBuilder mb) + { + mb.CreateTable(name: "Products", columns: table => new {}); + } + } + """; + DetectorResult r = d.detect(ctx("csharp", code)); + long productEntities = r.nodes().stream() + .filter(n -> n.getKind() == NodeKind.ENTITY && "Products".equals(n.getLabel())) + .count(); + // Should be 1 (no duplicate) + assertEquals(1, productEntities); + } + + @Test + void noEfCorePatternReturnsEmpty() { + String code = "public class RegularService { public void Run() {} }"; + DetectorResult r = d.detect(ctx("csharp", code)); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorResult r = d.detect(new DetectorContext("f.cs", "csharp", null)); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void deterministic() { + DetectorTestUtils.assertDeterministic(d, ctx("csharp", """ + public class Ctx : DbContext { + public DbSet Foos { get; set; } + } + public class Init : Migration {} + """)); + } + } + + // ===================================================================== + // CSharpMinimalApisDetector + // ===================================================================== + @Nested + class MinimalApisCoverage { + private final CSharpMinimalApisDetector d = new CSharpMinimalApisDetector(); + + @Test + void detectsMapPatchEndpoint() { + String code = """ + var builder = WebApplication.CreateBuilder(args); + var app = builder.Build(); + app.MapPatch("/api/users/{id}", (int id, User u) => Results.Ok(u)); + """; + DetectorResult r = d.detect(ctx("csharp", code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENDPOINT + && "PATCH".equals(n.getProperties().get("http_method")))); + } + + @Test + void webApplicationCreateBuilderCreatesModuleNode() { + String code = """ + var builder = WebApplication.CreateBuilder(args); + var app = builder.Build(); + app.MapGet("/health", () => "ok"); + """; + DetectorResult r = d.detect(ctx("csharp", code)); + // Module node from WebApplication.CreateBuilder + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE)); + // Endpoint linked to module + assertFalse(r.edges().stream().filter(e -> e.getKind() == EdgeKind.EXPOSES).toList().isEmpty()); + } + + @Test + void detectsUseAuthenticationGuard() { + String code = """ + app.UseAuthentication(); + app.UseAuthorization(); + """; + DetectorResult r = d.detect(ctx("csharp", code)); + long guards = r.nodes().stream().filter(n -> n.getKind() == NodeKind.GUARD).count(); + assertEquals(2, guards); + } + + @Test + void detectsAddAuthenticationGuard() { + String code = """ + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme); + builder.Services.AddAuthorization(); + """; + DetectorResult r = d.detect(ctx("csharp", code)); + long guards = r.nodes().stream().filter(n -> n.getKind() == NodeKind.GUARD).count(); + assertEquals(2, guards); + } + + @Test + void endpointsWithoutBuilderHaveNoExposesEdge() { + // No WebApplication.CreateBuilder => no module node, no EXPOSES edges + String code = """ + app.MapGet("/ping", () => "pong"); + """; + DetectorResult r = d.detect(ctx("csharp", code)); + assertEquals(1, r.nodes().size()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void detectsAllHttpMethods() { + String code = """ + app.MapGet("/a", () => {}); + app.MapPost("/b", () => {}); + app.MapPut("/c", () => {}); + app.MapDelete("/d", () => {}); + app.MapPatch("/e", () => {}); + """; + DetectorResult r = d.detect(ctx("csharp", code)); + long endpoints = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count(); + assertEquals(5, endpoints); + } + + @Test + void noPatternReturnsEmpty() { + String code = "var x = 1;"; + DetectorResult r = d.detect(ctx("csharp", code)); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorResult r = d.detect(new DetectorContext("prog.cs", "csharp", null)); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void deterministic() { + DetectorTestUtils.assertDeterministic(d, ctx("csharp", """ + var builder = WebApplication.CreateBuilder(args); + var app = builder.Build(); + app.UseAuthentication(); + app.MapGet("/a", () => {}); + app.MapPost("/b", () => {}); + """)); + } + } + + // ===================================================================== + // Helpers + // ===================================================================== + + private static DetectorContext ctx(String language, String content) { + return DetectorTestUtils.contextFor(language, content); + } +} diff --git a/src/test/java/io/github/randomcodespace/iq/detector/docs/MarkdownStructureDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/docs/MarkdownStructureDetectorTest.java index 34bc4740..7bf35be8 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/docs/MarkdownStructureDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/docs/MarkdownStructureDetectorTest.java @@ -1,11 +1,144 @@ package io.github.randomcodespace.iq.detector.docs; -import io.github.randomcodespace.iq.detector.*; import io.github.randomcodespace.iq.model.*; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; + +import io.github.randomcodespace.iq.detector.DetectorContext; +import io.github.randomcodespace.iq.detector.DetectorResult; +import io.github.randomcodespace.iq.detector.DetectorTestUtils; +import io.github.randomcodespace.iq.model.EdgeKind; +import io.github.randomcodespace.iq.model.NodeKind; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + class MarkdownStructureDetectorTest { + private final MarkdownStructureDetector d = new MarkdownStructureDetector(); - @Test void detectsHeadings() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("markdown", "# My Doc\n## Section 1\nSome text\n## Section 2\n[link](other.md)")); + + @Test + void detectsModuleNodeForAnyContent() { + String code = "plain text without headings"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(1, r.nodes().stream().filter(n -> n.getKind() == NodeKind.MODULE).count()); + } + + @Test + void moduleNodeLabelIsFilenameWhenNoH1() { + DetectorResult r = d.detect(DetectorTestUtils.contextFor("test/README.md", "markdown", + "## Section without H1\nSome content")); + var module = r.nodes().stream().filter(n -> n.getKind() == NodeKind.MODULE).findFirst().orElseThrow(); + // label should be the filename since no H1 + assertNotNull(module.getLabel()); + } + + @Test + void detectsH1AsModuleLabel() { + String code = "# My Project\n## Overview\nSome text\n"; + DetectorResult r = d.detect(ctx(code)); + var module = r.nodes().stream().filter(n -> n.getKind() == NodeKind.MODULE).findFirst().orElseThrow(); + assertEquals("My Project", module.getLabel()); + } + + @Test + void detectsHeadings() { + String code = "# My Doc\n## Section 1\nSome text\n## Section 2\n[link](other.md)"; + DetectorResult r = d.detect(ctx(code)); assertTrue(r.nodes().size() >= 3); } - @Test void noMatch() { assertEquals(1, d.detect(DetectorTestUtils.contextFor("markdown", "plain text")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("markdown", "# Title\n## A\n## B")); } + + @Test + void detectsH1ToH3Headings() { + String code = """ + # Title + ## Chapter 1 + ### Section 1.1 + ## Chapter 2 + """; + DetectorResult r = d.detect(ctx(code)); + var headings = r.nodes().stream().filter(n -> n.getKind() == NodeKind.CONFIG_KEY).toList(); + assertEquals(4, headings.size()); + } + + @Test + void headingNodeHasLevelProperty() { + String code = "# Title\n## SubTitle\n"; + DetectorResult r = d.detect(ctx(code)); + var h1 = r.nodes().stream() + .filter(n -> n.getKind() == NodeKind.CONFIG_KEY && "Title".equals(n.getLabel())) + .findFirst().orElseThrow(); + assertEquals(1, h1.getProperties().get("level")); + var h2 = r.nodes().stream() + .filter(n -> n.getKind() == NodeKind.CONFIG_KEY && "SubTitle".equals(n.getLabel())) + .findFirst().orElseThrow(); + assertEquals(2, h2.getProperties().get("level")); + } + + @Test + void detectsContainsEdgeForHeadings() { + String code = "# Doc\n## Section\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.CONTAINS)); + } + + @Test + void detectsInternalLinks() { + String code = "# Doc\nSee [installation guide](installation.md) for details.\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.DEPENDS_ON)); + } + + @Test + void skipsExternalLinks() { + String code = "# Doc\nSee [external](https://example.com) for details.\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().noneMatch(e -> e.getKind() == EdgeKind.DEPENDS_ON)); + } + + @Test + void skipsAnchorOnlyLinks() { + String code = "# Doc\nJump to [section](#section)\n"; + DetectorResult r = d.detect(ctx(code)); + // link target is just #section, so linkPath="" -> skipped + assertTrue(r.edges().stream().noneMatch(e -> e.getKind() == EdgeKind.DEPENDS_ON)); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.md", "markdown", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("markdown_structure", d.getName()); + } + + @Test + void supportedLanguagesContainsMarkdown() { + assertTrue(d.getSupportedLanguages().contains("markdown")); + } + + @Test + void deterministic() { + String code = """ + # Project Documentation + ## Installation + Run `npm install` to install dependencies. + ## Usage + See [getting started](getting-started.md) guide. + ### Advanced Usage + More info at [docs](docs/advanced.md). + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("markdown", content); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorsCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorsCoverageTest.java new file mode 100644 index 00000000..978bc73f --- /dev/null +++ b/src/test/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorsCoverageTest.java @@ -0,0 +1,570 @@ +package io.github.randomcodespace.iq.detector.frontend; + +import io.github.randomcodespace.iq.detector.DetectorContext; +import io.github.randomcodespace.iq.detector.DetectorResult; +import io.github.randomcodespace.iq.detector.DetectorTestUtils; +import io.github.randomcodespace.iq.model.EdgeKind; +import io.github.randomcodespace.iq.model.NodeKind; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Additional coverage tests for frontend detectors — branches not hit by + * existing tests. + */ +class FrontendDetectorsCoverageTest { + + // ===================================================================== + // ReactComponentDetector + // ===================================================================== + @Nested + class ReactCoverage { + private final ReactComponentDetector d = new ReactComponentDetector(); + + @Test + void classExtendsReactComponentIsDetected() { + String code = """ + class Dashboard extends React.Component { + render() { return
; } + } + """; + DetectorResult r = d.detect(ctx("typescript", code)); + assertFalse(r.nodes().isEmpty()); + assertEquals(NodeKind.COMPONENT, r.nodes().get(0).getKind()); + assertEquals("class", r.nodes().get(0).getProperties().get("component_type")); + } + + @Test + void classExtendsComponentIsDetected() { + String code = """ + class Login extends Component { + render() { return
; } + } + """; + DetectorResult r = d.detect(ctx("typescript", code)); + assertFalse(r.nodes().isEmpty()); + assertEquals("Login", r.nodes().get(0).getLabel()); + } + + @Test + void multipleHooksExportedAsConst() { + String code = """ + export const useFetch = () => { return {}; }; + export const useDebounce = () => { return {}; }; + """; + DetectorResult r = d.detect(ctx("typescript", code)); + assertEquals(2, r.nodes().size()); + assertTrue(r.nodes().stream().allMatch(n -> n.getKind() == NodeKind.HOOK)); + } + + @Test + void duplicateComponentNameIsDeduped() { + String code = """ + export default function App() { return
; } + export default function App() { return ; } + """; + DetectorResult r = d.detect(ctx("typescript", code)); + // Should only appear once (deduplicated) + assertEquals(1, r.nodes().size()); + } + + @Test + void duplicateHookIsDeduped() { + String code = """ + export function useData() { return {}; } + export function useData() { return {}; } + """; + DetectorResult r = d.detect(ctx("typescript", code)); + assertEquals(1, r.nodes().size()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorResult r = d.detect(new DetectorContext("App.tsx", "typescript", null)); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("typescript", "")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void exportConstFCPattern() { + String code = "export const Nav: React.FC = () =>