From 997645874175761a6f46cfc2c5154ad67684420a Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 4 Apr 2026 08:11:46 +0000 Subject: [PATCH 1/5] test(coverage): expand Python/Go/Rust/Scala/C++/Kotlin/Proto/Shell detector tests Replace 11-line stub test files with comprehensive test suites across 13 detector test classes. Each class now has 12-18 tests covering positive match, edge/node properties, no-match/empty/null guard paths, supported-language/name meta checks, and determinism. Total test count grows from ~1620 to 1836 (+216 new tests). Co-Authored-By: Claude Sonnet 4.6 --- .../cpp/CppStructuresDetectorTest.java | 127 +++++++++++- .../docs/MarkdownStructureDetectorTest.java | 143 +++++++++++++- .../iq/detector/go/GoOrmDetectorTest.java | 165 +++++++++++++++- .../detector/go/GoStructuresDetectorTest.java | 182 +++++++++++++++++- .../iq/detector/go/GoWebDetectorTest.java | 159 ++++++++++++++- .../kotlin/KotlinStructuresDetectorTest.java | 140 +++++++++++++- .../kotlin/KtorRouteDetectorTest.java | 149 +++++++++++++- .../proto/ProtoStructureDetectorTest.java | 180 ++++++++++++++++- .../detector/rust/ActixWebDetectorTest.java | 154 ++++++++++++++- .../rust/RustStructuresDetectorTest.java | 143 +++++++++++++- .../scala/ScalaStructuresDetectorTest.java | 129 ++++++++++++- .../iq/detector/shell/BashDetectorTest.java | 169 +++++++++++++++- .../shell/PowerShellDetectorTest.java | 128 +++++++++++- 13 files changed, 1898 insertions(+), 70 deletions(-) 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/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/go/GoOrmDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/go/GoOrmDetectorTest.java index 0f9d0aad..9f245e56 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/go/GoOrmDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/go/GoOrmDetectorTest.java @@ -1,11 +1,164 @@ package io.github.randomcodespace.iq.detector.go; -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 GoOrmDetectorTest { + private final GoOrmDetector d = new GoOrmDetector(); - @Test void detectsGormModel() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("go", "import \"gorm.io/gorm\"\ntype User struct {\n gorm.Model\n}")); - assertTrue(r.nodes().size() >= 1); assertEquals(NodeKind.ENTITY, r.nodes().get(0).getKind()); + + @Test + void detectsGormModel() { + String code = "import \"gorm.io/gorm\"\ntype User struct {\n gorm.Model\n Name string\n}"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().size() >= 1); + assertEquals(NodeKind.ENTITY, r.nodes().get(0).getKind()); + } + + @Test + void gormModelHasFrameworkProperty() { + String code = "import \"gorm.io/gorm\"\ntype Product struct {\n gorm.Model\n}"; + DetectorResult r = d.detect(ctx(code)); + var entity = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENTITY).findFirst().orElseThrow(); + assertEquals("gorm", entity.getProperties().get("framework")); + } + + @Test + void detectsGormAutoMigrate() { + String code = "import \"gorm.io/gorm\"\ndb.AutoMigrate(&User{}, &Product{})"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MIGRATION)); + } + + @Test + void detectsGormQueryEdges() { + String code = """ + import "gorm.io/gorm" + func getUsers(db *gorm.DB) { + db.Find(&users) + db.Where("active = ?", true).First(&user) + db.Create(&newUser) + db.Save(&user) + db.Delete(&user) + } + """; + DetectorResult r = d.detect(ctx(code)); + assertFalse(r.edges().stream().filter(e -> e.getKind() == EdgeKind.QUERIES).findAny().isEmpty()); + } + + @Test + void detectsSqlxConnection() { + String code = """ + import "github.com/jmoiron/sqlx" + func connect() { + db := sqlx.Connect("postgres", dsn) + } + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.DATABASE_CONNECTION + && "sqlx".equals(n.getProperties().get("framework")))); + } + + @Test + void detectsSqlxQueryEdges() { + String code = """ + import "github.com/jmoiron/sqlx" + func query(db *sqlx.DB) { + db.Select(&users, "SELECT * FROM users") + db.Get(&user, "SELECT * FROM users WHERE id=$1", 1) + db.NamedExec("INSERT INTO users VALUES (:name)", user) + } + """; + DetectorResult r = d.detect(ctx(code)); + assertFalse(r.edges().stream().filter(e -> e.getKind() == EdgeKind.QUERIES).findAny().isEmpty()); + } + + @Test + void detectsDatabaseSqlConnection() { + String code = """ + import "database/sql" + func connect() { + db, _ := sql.Open("mysql", dsn) + } + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.DATABASE_CONNECTION + && "database_sql".equals(n.getProperties().get("framework")))); + } + + @Test + void detectsDatabaseSqlQueryEdges() { + String code = """ + import "database/sql" + func query(db *sql.DB) { + rows, _ := db.Query("SELECT * FROM items") + row := db.QueryRow("SELECT * FROM items WHERE id = ?", 1) + db.Exec("DELETE FROM items WHERE id = ?", 1) + } + """; + DetectorResult r = d.detect(ctx(code)); + assertFalse(r.edges().stream().filter(e -> e.getKind() == EdgeKind.QUERIES).findAny().isEmpty()); + } + + @Test + void noMatchOnPlainGoCode() { + DetectorResult r = d.detect(ctx("package main\nfunc main() {}")); + assertEquals(0, r.nodes().size()); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.go", "go", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("go_orm", d.getName()); + } + + @Test + void supportedLanguagesContainsGo() { + assertTrue(d.getSupportedLanguages().contains("go")); + } + + @Test + void deterministic() { + String code = """ + import "gorm.io/gorm" + type User struct { + gorm.Model + Name string + } + type Order struct { + gorm.Model + Total float64 + } + func setup(db *gorm.DB) { + db.AutoMigrate(&User{}, &Order{}) + db.Create(&User{Name: "test"}) + db.Find(&users) + db.Where("name = ?", "test").First(&user) + } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("go", content); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("go", "package main")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("go", "import \"gorm.io/gorm\"\ntype User struct {\n gorm.Model\n}\ndb.AutoMigrate(&User{})")); } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetectorTest.java index d614910d..1801ef50 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetectorTest.java @@ -1,11 +1,183 @@ package io.github.randomcodespace.iq.detector.go; -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 GoStructuresDetectorTest { + private final GoStructuresDetector d = new GoStructuresDetector(); - @Test void detectsStructAndInterface() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("go", "package main\ntype User struct {\n}\ntype Reader interface {\n}")); + + @Test + void detectsPackageNode() { + DetectorResult r = d.detect(ctx("package main\n")); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE)); + } + + @Test + void detectsStruct() { + String code = """ + package models + type User struct { + ID int + Name string + } + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "User".equals(n.getLabel()))); + } + + @Test + void detectsExportedStructFlagTrue() { + String code = "package p\ntype PublicStruct struct {}\n"; + DetectorResult r = d.detect(ctx(code)); + var node = r.nodes().stream().filter(n -> n.getKind() == NodeKind.CLASS).findFirst().orElseThrow(); + assertEquals(true, node.getProperties().get("exported")); + } + + @Test + void detectsUnexportedStructFlagFalse() { + String code = "package p\ntype privateStruct struct {}\n"; + DetectorResult r = d.detect(ctx(code)); + var node = r.nodes().stream().filter(n -> n.getKind() == NodeKind.CLASS).findFirst().orElseThrow(); + assertEquals(false, node.getProperties().get("exported")); + } + + @Test + void detectsInterface() { + String code = """ + package repository + type UserRepository interface { + FindAll() []User + FindByID(id int) User + } + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.INTERFACE && "UserRepository".equals(n.getLabel()))); + } + + @Test + void detectsMethodOnReceiver() { + String code = """ + package service + type UserService struct{} + func (s *UserService) GetUser(id int) User { + return User{} + } + func (s *UserService) CreateUser(u User) error { + return nil + } + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).count()); + // Methods have receiver_type property + assertTrue(r.nodes().stream() + .filter(n -> n.getKind() == NodeKind.METHOD) + .allMatch(n -> "UserService".equals(n.getProperties().get("receiver_type")))); + } + + @Test + void detectsMethodProducesDefinesEdge() { + String code = """ + package svc + type Svc struct{} + func (s *Svc) Do() {} + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.DEFINES)); + } + + @Test + void detectsTopLevelFunctions() { + String code = """ + package main + func main() {} + func helper() int { return 42 } + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).count()); + } + + @Test + void detectsBlockImports() { + String code = """ + package main + import ( + "fmt" + "net/http" + "github.com/gorilla/mux" + ) + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.IMPORTS)); + assertEquals(3, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count()); + } + + @Test + void detectsSingleImport() { + String code = "package main\nimport \"os\"\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.IMPORTS + && "os".equals(e.getTarget().getLabel()))); + } + + @Test + void detectsStructAndInterface() { + DetectorResult r = d.detect(ctx("package main\ntype User struct {\n}\ntype Reader interface {\n}")); assertTrue(r.nodes().size() >= 3); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("go", "")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("go", "package main\ntype Foo struct {\n}\nfunc Bar() {}")); } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.go", "go", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("go_structures", d.getName()); + } + + @Test + void supportedLanguagesContainsGo() { + assertTrue(d.getSupportedLanguages().contains("go")); + } + + @Test + void deterministic() { + String code = """ + package main + import ( + "fmt" + "os" + ) + type Config struct { + Name string + Port int + } + type Configurable interface { + Configure() error + } + func (c *Config) Apply() error { return nil } + func run() { fmt.Println("start") } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("go", content); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/go/GoWebDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/go/GoWebDetectorTest.java index 9cdbf984..a4bac39c 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/go/GoWebDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/go/GoWebDetectorTest.java @@ -1,11 +1,158 @@ package io.github.randomcodespace.iq.detector.go; -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.NodeKind; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + class GoWebDetectorTest { + private final GoWebDetector d = new GoWebDetector(); - @Test void detectsGinRoute() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("go", "r := gin.Default()\nr.GET(\"/users\", getUsers)")); - assertTrue(r.nodes().size() >= 1); assertEquals(NodeKind.ENDPOINT, r.nodes().get(0).getKind()); + + @Test + void detectsGinGetRoute() { + String code = "r := gin.Default()\nr.GET(\"/users\", getUsers)\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENDPOINT)); + assertEquals(NodeKind.ENDPOINT, r.nodes().get(0).getKind()); + } + + @Test + void detectsGinFramework() { + String code = "r := gin.Default()\nr.GET(\"/ping\", pingHandler)\n"; + DetectorResult r = d.detect(ctx(code)); + var ep = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).findFirst().orElseThrow(); + assertEquals("gin", ep.getProperties().get("framework")); + assertEquals("GET", ep.getProperties().get("http_method")); + assertEquals("/ping", ep.getProperties().get("path")); + } + + @Test + void detectsAllGinMethods() { + String code = """ + r := gin.New() + r.GET("/items", list) + r.POST("/items", create) + r.PUT("/items/:id", update) + r.DELETE("/items/:id", delete) + r.PATCH("/items/:id", patch) + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(5, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count()); + } + + @Test + void detectsEchoRoutes() { + String code = """ + e := echo.New() + e.GET("/items", getItems) + e.POST("/items", createItem) + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count()); + } + + @Test + void detectsEchoFramework() { + String code = "e := echo.New()\ne.GET(\"/health\", healthCheck)\n"; + DetectorResult r = d.detect(ctx(code)); + var ep = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).findFirst().orElseThrow(); + assertEquals("echo", ep.getProperties().get("framework")); + } + + @Test + void detectsChiLowercaseRoutes() { + String code = """ + r := chi.NewRouter() + r.Get("/health", healthCheck) + r.Post("/webhook", handleWebhook) + r.Delete("/data/{id}", deleteData) + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(3, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count()); + } + + @Test + void detectsNetHttpHandleFunc() { + String code = """ + func main() { + http.HandleFunc("/hello", helloHandler) + http.Handle("/static/", fs) + } + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count()); + } + + @Test + void detectsMuxHandleFuncWithMethods() { + String code = """ + r := mux.NewRouter() + r.HandleFunc("/api/users", getUsers).Methods("GET") + r.HandleFunc("/api/users", createUser).Methods("POST") + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count() >= 2); + } + + @Test + void detectsMiddlewareUse() { + String code = """ + r := gin.Default() + r.Use(cors) + r.Use(authMiddleware) + r.GET("/users", getUsers) + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.MIDDLEWARE).count()); + } + + @Test + void noMatchOnPlainGoCode() { + DetectorResult r = d.detect(ctx("package main\nfunc main() {}")); + assertEquals(0, r.nodes().size()); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.go", "go", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("go_web", d.getName()); + } + + @Test + void supportedLanguagesContainsGo() { + assertTrue(d.getSupportedLanguages().contains("go")); + } + + @Test + void deterministic() { + String code = """ + r := gin.Default() + r.Use(cors) + r.GET("/a", a) + r.POST("/b", b) + r.PUT("/c/:id", c) + r.DELETE("/d/:id", d) + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("go", content); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("go", "package main")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("go", "r := gin.Default()\nr.GET(\"/a\", a)\nr.POST(\"/b\", b)")); } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/kotlin/KotlinStructuresDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/kotlin/KotlinStructuresDetectorTest.java index 254de463..abb7d568 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/kotlin/KotlinStructuresDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/kotlin/KotlinStructuresDetectorTest.java @@ -1,11 +1,141 @@ package io.github.randomcodespace.iq.detector.kotlin; -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 KotlinStructuresDetectorTest { + private final KotlinStructuresDetector d = new KotlinStructuresDetector(); - @Test void detectsClassAndInterface() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("kotlin", "class User\ninterface Repo\nfun findAll() {}")); + + @Test + void detectsClass() { + String code = "class UserService(private val repo: UserRepository)\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "UserService".equals(n.getLabel()))); + } + + @Test + void detectsDataClass() { + String code = "data class User(val id: Long, val name: String)\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "User".equals(n.getLabel()))); + } + + @Test + void detectsSealedClass() { + String code = "sealed class Result\nclass Success(val data: T) : Result()\nclass Failure(val error: String) : Result()\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().filter(n -> n.getKind() == NodeKind.CLASS).count() >= 3); + } + + @Test + void detectsClassWithSupertype() { + String code = "class AdminUser : BaseUser()\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 detectsInterface() { + String code = "interface UserRepository {\n fun findAll(): List\n fun save(user: User): User\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.INTERFACE && "UserRepository".equals(n.getLabel()))); + } + + @Test + void detectsObject() { + String code = "object DatabaseConfig {\n const val URL = \"jdbc:postgresql://localhost:5432/db\"\n}\n"; + DetectorResult r = d.detect(ctx(code)); + var obj = r.nodes().stream().filter(n -> "DatabaseConfig".equals(n.getLabel())).findFirst().orElseThrow(); + assertEquals("object", obj.getProperties().get("type")); + } + + @Test + void detectsFun() { + String code = "fun processOrder(order: Order): Result {\n return Success(Unit)\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.METHOD && "processOrder".equals(n.getLabel()))); + } + + @Test + void detectsSuspendFun() { + String code = "suspend fun fetchUser(id: Long): User {\n return repo.findById(id)\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.METHOD && "fetchUser".equals(n.getLabel()))); + } + + @Test + void detectsOverrideFun() { + String code = "override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.METHOD && "onCreate".equals(n.getLabel()))); + } + + @Test + void detectsImports() { + String code = "import kotlinx.coroutines.launch\nimport io.ktor.server.application.*\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count()); + } + + @Test + void detectsClassAndInterface() { + DetectorResult r = d.detect(ctx("class User\ninterface Repo\nfun findAll() {}")); assertTrue(r.nodes().size() >= 3); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("kotlin", "")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("kotlin", "data class Foo(val x: Int)\nobject Bar")); } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.kt", "kotlin", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("kotlin_structures", d.getName()); + } + + @Test + void supportedLanguagesContainsKotlin() { + assertTrue(d.getSupportedLanguages().contains("kotlin")); + } + + @Test + void deterministic() { + String code = """ + import kotlinx.coroutines.launch + import io.ktor.server.routing.* + data class User(val id: Long, val name: String) + sealed class ApiResult + interface UserService { + suspend fun findById(id: Long): User + } + object Config { + const val PORT = 8080 + } + fun main() { + launch { println("started") } + } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("kotlin", content); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/kotlin/KtorRouteDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/kotlin/KtorRouteDetectorTest.java index a2f44a5d..6668b7ee 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/kotlin/KtorRouteDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/kotlin/KtorRouteDetectorTest.java @@ -1,11 +1,148 @@ package io.github.randomcodespace.iq.detector.kotlin; -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.NodeKind; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + class KtorRouteDetectorTest { + private final KtorRouteDetector d = new KtorRouteDetector(); - @Test void detectsKtorRoute() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("kotlin", "routing {\n get(\"/hello\") {\n call.respond(\"hi\")\n }\n}")); - assertTrue(r.nodes().size() >= 2); + + @Test + void detectsRoutingBlock() { + String code = "routing {\n get(\"/hello\") { call.respond(\"hi\") }\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE)); + } + + @Test + void detectsGetRoute() { + String code = "routing {\n get(\"/users\") { call.respond(listOf()) }\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENDPOINT + && n.getLabel().contains("GET"))); + } + + @Test + void detectsPostRoute() { + String code = "routing {\n post(\"/users\") { val user = call.receive() }\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENDPOINT + && n.getLabel().contains("POST"))); + } + + @Test + void detectsPutAndDeleteRoutes() { + String code = """ + routing { + put("/users/{id}") { } + delete("/users/{id}") { } + } + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count()); + } + + @Test + void detectsInstallMiddleware() { + String code = "install(ContentNegotiation) {\n json()\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MIDDLEWARE)); + } + + @Test + void detectsAuthenticate() { + String code = """ + routing { + authenticate("jwt") { + get("/profile") { } + } + } + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.GUARD)); + } + + @Test + void detectsRoutePrefix() { + String code = """ + routing { + route("/api") { + get("/users") { } + post("/users") { } + } + } + """; + DetectorResult r = d.detect(ctx(code)); + // Endpoints should have prefixed paths + var endpoints = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).toList(); + assertEquals(2, endpoints.size()); + } + + @Test + void detectsMultipleInstalls() { + String code = """ + install(ContentNegotiation) { json() } + install(CallLogging) + install(CORS) { anyHost() } + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(3, r.nodes().stream().filter(n -> n.getKind() == NodeKind.MIDDLEWARE).count()); + } + + @Test + void noMatchOnPlainKotlin() { + DetectorResult r = d.detect(ctx("fun main() {}")); + assertEquals(0, r.nodes().size()); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.kt", "kotlin", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("ktor_routes", d.getName()); + } + + @Test + void supportedLanguagesContainsKotlin() { + assertTrue(d.getSupportedLanguages().contains("kotlin")); + } + + @Test + void deterministic() { + String code = """ + install(ContentNegotiation) { json() } + install(CallLogging) + routing { + authenticate("jwt") { + route("/api") { + get("/users") { } + post("/users") { } + put("/users/{id}") { } + delete("/users/{id}") { } + } + } + } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("kotlin", content); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("kotlin", "fun main() {}")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("kotlin", "routing {\n get(\"/a\") {}\n post(\"/b\") {}\n}")); } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/proto/ProtoStructureDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/proto/ProtoStructureDetectorTest.java index a9be3932..8e93bee1 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/proto/ProtoStructureDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/proto/ProtoStructureDetectorTest.java @@ -1,11 +1,181 @@ package io.github.randomcodespace.iq.detector.proto; -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 ProtoStructureDetectorTest { + private final ProtoStructureDetector d = new ProtoStructureDetector(); - @Test void detectsServiceAndMessage() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("proto", "package grpc.test;\nservice UserService {\n rpc GetUser(GetUserReq) returns (User);\n}\nmessage User {\n string name = 1;\n}")); + + @Test + void detectsPackage() { + String code = "syntax = \"proto3\";\npackage com.example.api;\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CONFIG_KEY + && "com.example.api".equals(n.getFqn()))); + } + + @Test + void detectsService() { + String code = """ + service UserService { + rpc GetUser(GetUserRequest) returns (User); + } + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.INTERFACE + && "UserService".equals(n.getLabel()))); + } + + @Test + void detectsRpc() { + String code = """ + service OrderService { + rpc CreateOrder(CreateOrderRequest) returns (Order); + rpc GetOrder(GetOrderRequest) returns (Order); + } + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).count()); + } + + @Test + void rpcNodeHasRequestAndResponseType() { + String code = """ + service Svc { + rpc DoThing(ThingRequest) returns (ThingResponse); + } + """; + DetectorResult r = d.detect(ctx(code)); + var rpc = r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).findFirst().orElseThrow(); + assertEquals("ThingRequest", rpc.getProperties().get("request_type")); + assertEquals("ThingResponse", rpc.getProperties().get("response_type")); + } + + @Test + void detectsRpcContainsEdge() { + String code = """ + service UserService { + rpc GetUser(GetUserRequest) returns (User); + } + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.CONTAINS)); + } + + @Test + void detectsMessage() { + String code = """ + message User { + int64 id = 1; + string name = 2; + string email = 3; + } + """; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.PROTOCOL_MESSAGE + && "User".equals(n.getLabel()))); + } + + @Test + void detectsImport() { + String code = "import \"google/protobuf/timestamp.proto\";\nimport \"google/protobuf/empty.proto\";\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count()); + } + + @Test + void detectsServiceAndMessage() { + String code = "package grpc.test;\nservice UserService {\n rpc GetUser(GetUserReq) returns (User);\n}\nmessage User {\n string name = 1;\n}"; + DetectorResult r = d.detect(ctx(code)); assertTrue(r.nodes().size() >= 3); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("proto", "// comment")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("proto", "service Svc {\n rpc Do(Req) returns (Resp);\n}\nmessage Req {}")); } + + @Test + void fullProtoFile() { + String code = """ + syntax = "proto3"; + package com.example.orders; + import "google/protobuf/timestamp.proto"; + service OrderService { + rpc CreateOrder(CreateOrderRequest) returns (Order); + rpc GetOrder(GetOrderRequest) returns (Order); + rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse); + } + message Order { + int64 id = 1; + string status = 2; + } + message CreateOrderRequest { + string item = 1; + } + message GetOrderRequest { + int64 id = 1; + } + message ListOrdersRequest {} + message ListOrdersResponse { + repeated Order orders = 1; + } + """; + DetectorResult r = d.detect(ctx(code)); + // 1 package + 1 service + 3 RPCs + 5 messages + assertTrue(r.nodes().size() >= 10); + } + + @Test + void noMatchOnComment() { + DetectorResult r = d.detect(ctx("// comment\n/* multi-line comment */\n")); + assertEquals(0, r.nodes().size()); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.proto", "proto", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("proto_structure", d.getName()); + } + + @Test + void supportedLanguagesContainsProto() { + assertTrue(d.getSupportedLanguages().contains("proto")); + } + + @Test + void deterministic() { + String code = """ + syntax = "proto3"; + package example; + import "google/protobuf/empty.proto"; + service Svc { + rpc Do(Req) returns (Resp); + rpc List(google.protobuf.Empty) returns (ListResp); + } + message Req { string id = 1; } + message Resp { string result = 1; } + message ListResp { repeated Resp items = 1; } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("proto", content); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/rust/ActixWebDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/rust/ActixWebDetectorTest.java index be096939..b2b74369 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/rust/ActixWebDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/rust/ActixWebDetectorTest.java @@ -1,11 +1,153 @@ package io.github.randomcodespace.iq.detector.rust; -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.NodeKind; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + class ActixWebDetectorTest { + private final ActixWebDetector d = new ActixWebDetector(); - @Test void detectsActixRoute() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("rust", "#[get(\"/hello\")]\nasync fn hello() -> impl Responder {}")); - assertTrue(r.nodes().size() >= 1); assertEquals(NodeKind.ENDPOINT, r.nodes().get(0).getKind()); + + @Test + void detectsActixGetRoute() { + String code = "#[get(\"/hello\")]\nasync fn hello() -> impl Responder {}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().size() >= 1); + assertEquals(NodeKind.ENDPOINT, r.nodes().get(0).getKind()); + } + + @Test + void detectsActixRouteHttpMethod() { + String code = "#[post(\"/users\")]\nasync fn create_user() -> impl Responder {}\n"; + DetectorResult r = d.detect(ctx(code)); + var ep = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).findFirst().orElseThrow(); + assertEquals("POST", ep.getProperties().get("http_method")); + assertEquals("actix_web", ep.getProperties().get("framework")); + } + + @Test + void detectsAllHttpMethods() { + String code = """ + #[get("/items")] + async fn list_items() -> impl Responder {} + #[post("/items")] + async fn create_item() -> impl Responder {} + #[put("/items/{id}")] + async fn update_item() -> impl Responder {} + #[delete("/items/{id}")] + async fn delete_item() -> impl Responder {} + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(4, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count()); + } + + @Test + void detectsHttpServerNew() { + String code = "HttpServer::new(|| { App::new() })\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE + && "HttpServer".equals(n.getLabel()))); + } + + @Test + void detectsWebRoute() { + String code = "#[actix_web::main]\nasync fn main() {\n HttpServer::new(|| {\n App::new().route(\"/hello\", web::get().to(hello))\n })\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertFalse(r.nodes().isEmpty()); + } + + @Test + void detectsServiceResource() { + String code = "#[actix_web::main]\nasync fn main() {\n HttpServer::new(|| {\n App::new().service(web::resource(\"/api/users\"))\n })\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENDPOINT)); + } + + @Test + void detectsActixWebMainAttr() { + String code = "#[actix_web::main]\nasync fn main() -> std::io::Result<()> {}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE)); + } + + @Test + void detectsTokioMainAttr() { + String code = "#[tokio::main]\nasync fn main() {}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE)); + } + + @Test + void detectsAxumRoute() { + String code = "fn app() -> Router {\n Router::new().route(\"/api/users\", get(list_users))\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENDPOINT + && "axum".equals(n.getProperties().get("framework")))); + } + + @Test + void detectsAxumLayer() { + String code = "fn app() -> Router {\n Router::new().layer(AuthLayer)\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MIDDLEWARE)); + } + + @Test + void noMatchOnPlainRust() { + DetectorResult r = d.detect(ctx("fn main() {}")); + assertEquals(0, r.nodes().size()); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.rs", "rust", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("actix_web", d.getName()); + } + + @Test + void supportedLanguagesContainsRust() { + assertTrue(d.getSupportedLanguages().contains("rust")); + } + + @Test + void deterministic() { + String code = """ + #[actix_web::main] + async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .route("/hello", web::get().to(hello)) + .service(web::resource("/api/users")) + }) + .bind("127.0.0.1:8080")? + .run() + .await + } + #[get("/items")] + async fn list_items() -> impl Responder {} + #[post("/items")] + async fn create_item() -> impl Responder {} + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("rust", content); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("rust", "fn main() {}")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("rust", "#[get(\"/a\")]\nasync fn a() {}\n#[post(\"/b\")]\nasync fn b() {}")); } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/rust/RustStructuresDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/rust/RustStructuresDetectorTest.java index f9bfa5a6..dbe23102 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/rust/RustStructuresDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/rust/RustStructuresDetectorTest.java @@ -1,11 +1,144 @@ package io.github.randomcodespace.iq.detector.rust; -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 RustStructuresDetectorTest { + private final RustStructuresDetector d = new RustStructuresDetector(); - @Test void detectsStructAndTrait() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("rust", "pub struct User {}\npub trait Serialize {}")); + + @Test + void detectsStruct() { + String code = "pub struct User {\n name: String,\n age: u32,\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "User".equals(n.getLabel()))); + } + + @Test + void detectsTrait() { + String code = "pub trait Serializable {\n fn serialize(&self) -> String;\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.INTERFACE && "Serializable".equals(n.getLabel()))); + } + + @Test + void detectsEnum() { + String code = "pub enum 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 && "Color".equals(n.getLabel()))); + } + + @Test + void detectsFunction() { + String code = "pub fn process(input: &str) -> Result {\n Ok(input.to_string())\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.METHOD && "process".equals(n.getLabel()))); + } + + @Test + void detectsAsyncFunction() { + String code = "pub async fn fetch_data(url: &str) -> reqwest::Result {\n Ok(String::new())\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.METHOD && "fetch_data".equals(n.getLabel()))); + } + + @Test + void detectsMod() { + String code = "pub mod handlers;\nmod internal;\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.MODULE).count()); + } + + @Test + void detectsUseStatement() { + String code = "use std::collections::HashMap;\nuse crate::models::User;\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count()); + } + + @Test + void detectsImplBlock() { + String code = "struct Foo {}\nimpl Foo {\n fn bar(&self) {}\n}\n"; + DetectorResult r = d.detect(ctx(code)); + // Should produce a DEFINES edge (impl without for) + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.DEFINES)); + } + + @Test + void detectsImplTrait() { + String code = "trait Display {}\nstruct Foo {}\nimpl Display for Foo {\n fn fmt(&self) {}\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.IMPLEMENTS)); + } + + @Test + void detectsMacro() { + String code = "macro_rules! my_macro {\n () => {};\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getLabel().contains("my_macro"))); + } + + @Test + void detectsStructAndTrait() { + DetectorResult r = d.detect(ctx("pub struct User {}\npub trait Serialize {}")); assertTrue(r.nodes().size() >= 2); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("rust", "")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("rust", "struct A {}\ntrait B {}\nfn c() {}")); } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.rs", "rust", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("rust_structures", d.getName()); + } + + @Test + void supportedLanguagesContainsRust() { + assertTrue(d.getSupportedLanguages().contains("rust")); + } + + @Test + void deterministic() { + String code = """ + use std::collections::HashMap; + mod handlers; + pub struct Config { + name: String, + } + pub trait Configurable { + fn configure(&self); + } + pub enum Status { Active, Inactive } + impl Config { + pub fn new() -> Self { Config { name: String::new() } } + } + impl Configurable for Config { + fn configure(&self) {} + } + pub fn run() {} + macro_rules! log_info { ($msg:expr) => {}; } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("rust", content); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/scala/ScalaStructuresDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/scala/ScalaStructuresDetectorTest.java index 5d2d4e76..bf9bd8fc 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/scala/ScalaStructuresDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/scala/ScalaStructuresDetectorTest.java @@ -1,11 +1,130 @@ package io.github.randomcodespace.iq.detector.scala; -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 ScalaStructuresDetectorTest { + private final ScalaStructuresDetector d = new ScalaStructuresDetector(); - @Test void detectsClassAndTrait() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("scala", "class User extends Entity\ntrait Serializable\ndef process(x: Int) = x")); + + @Test + void detectsClass() { + String code = "class User(val name: String, val age: Int)\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "User".equals(n.getLabel()))); + } + + @Test + void detectsCaseClass() { + String code = "case class Event(id: Long, name: String)\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "Event".equals(n.getLabel()))); + } + + @Test + void detectsClassWithExtends() { + String code = "class AdminUser extends User\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 detectsClassWithWith() { + String code = "class Service extends Actor with Serializable with Logging\n"; + DetectorResult r = d.detect(ctx(code)); + // has EXTENDS for Actor and IMPLEMENTS for Serializable, Logging + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.EXTENDS)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.IMPLEMENTS)); + } + + @Test + void detectsTrait() { + String code = "trait Serializable {\n def serialize(): String\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.INTERFACE && "Serializable".equals(n.getLabel()))); + } + + @Test + void detectsObject() { + String code = "object UserFactory {\n def create() = new User()\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> "UserFactory".equals(n.getLabel()))); + var obj = r.nodes().stream().filter(n -> "UserFactory".equals(n.getLabel())).findFirst().orElseThrow(); + assertEquals("object", obj.getProperties().get("type")); + } + + @Test + void detectsDef() { + String code = "def processRequest(req: Request): Response = ???\ndef validate(input: String): Boolean = input.nonEmpty\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).count()); + } + + @Test + void detectsImport() { + String code = "import scala.collection.mutable.ListBuffer\nimport akka.actor.ActorSystem\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count()); + } + + @Test + void detectsClassAndTrait() { + DetectorResult r = d.detect(ctx("class User extends Entity\ntrait Serializable\ndef process(x: Int) = x")); assertTrue(r.nodes().size() >= 3); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("scala", "")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("scala", "case class Foo(x: Int)\nobject Bar")); } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.scala", "scala", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("scala_structures", d.getName()); + } + + @Test + void supportedLanguagesContainsScala() { + assertTrue(d.getSupportedLanguages().contains("scala")); + } + + @Test + void deterministic() { + String code = """ + import scala.collection.mutable.ListBuffer + import akka.actor.Actor + case class User(id: Long, name: String) + class UserService extends Actor with Serializable { + def receive = ??? + } + trait Repository[T] { + def findAll(): List[T] + def save(entity: T): Unit + } + object Main { + def main(args: Array[String]): Unit = {} + } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("scala", content); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/shell/BashDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/shell/BashDetectorTest.java index 654b72a6..4a344a53 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/shell/BashDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/shell/BashDetectorTest.java @@ -1,11 +1,170 @@ package io.github.randomcodespace.iq.detector.shell; -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 BashDetectorTest { + private final BashDetector d = new BashDetector(); - @Test void detectsFunction() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("bash", "#!/bin/bash\nfunction deploy() {\n docker build .\n}")); + + @Test + void detectsShebangModuleNode() { + String code = "#!/bin/bash\necho \"hello\"\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE)); + var module = r.nodes().stream().filter(n -> n.getKind() == NodeKind.MODULE).findFirst().orElseThrow(); + assertEquals("bash", module.getProperties().get("shell")); + } + + @Test + void detectsShebangWithEnv() { + String code = "#!/usr/bin/env bash\necho \"hi\"\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE)); + } + + @Test + void detectsFunctionKeywordStyle() { + String code = "#!/bin/bash\nfunction deploy() {\n echo \"deploying\"\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.METHOD && "deploy".equals(n.getLabel()))); + } + + @Test + void detectsFunctionParenStyle() { + String code = "#!/bin/bash\nbuild() {\n mvn package\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.METHOD && "build".equals(n.getLabel()))); + } + + @Test + void detectsMultipleFunctions() { + String code = """ + #!/bin/bash + function build() { + mvn package + } + function test() { + mvn test + } + function deploy() { + kubectl apply -f manifest.yaml + } + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(3, r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).count()); + } + + @Test + void detectsSourceImport() { + String code = "#!/bin/bash\nsource ./utils.sh\n. ./config.sh\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.IMPORTS)); + } + + @Test + void detectsExportVariable() { + String code = "#!/bin/bash\nexport DATABASE_URL=postgres://localhost/mydb\nexport API_KEY=secret123\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.CONFIG_DEFINITION).count()); + } + + @Test + void detectsDockerToolUsage() { + String code = "#!/bin/bash\ndocker build -t myapp .\ndocker push myapp\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.CALLS + && "docker".equals(e.getProperties().get("tool")))); + } + + @Test + void detectsKubectlToolUsage() { + String code = "#!/bin/bash\nkubectl apply -f deployment.yaml\nkubectl get pods\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.CALLS + && "kubectl".equals(e.getProperties().get("tool")))); + } + + @Test + void detectsTerraformToolUsage() { + String code = "#!/bin/bash\nterraform init\nterraform plan\nterraform apply\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.CALLS + && "terraform".equals(e.getProperties().get("tool")))); + } + + @Test + void toolSeenOnlyOnce() { + // Docker appears multiple times but should only produce one CALLS edge + String code = "#!/bin/bash\ndocker build .\ndocker push .\ndocker run app\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(1, r.edges().stream() + .filter(e -> e.getKind() == EdgeKind.CALLS && "docker".equals(e.getProperties().get("tool"))) + .count()); + } + + @Test + void toolInCommentNotDetected() { + String code = "#!/bin/bash\n# docker build . -- this is a comment\necho \"done\"\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(0, r.edges().stream().filter(e -> e.getKind() == EdgeKind.CALLS).count()); + } + + @Test + void detectsFunction() { + DetectorResult r = d.detect(ctx("#!/bin/bash\nfunction deploy() {\n docker build .\n}")); assertTrue(r.nodes().size() >= 2); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("bash", "")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("bash", "#!/bin/bash\nfunction a() {\n echo hi\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.sh", "bash", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("bash", d.getName()); + } + + @Test + void supportedLanguagesContainsBash() { + assertTrue(d.getSupportedLanguages().contains("bash")); + } + + @Test + void deterministic() { + String code = """ + #!/bin/bash + source ./common.sh + export APP_ENV=production + export DATABASE_URL=postgres://localhost/prod + function build() { + docker build -t myapp . + } + function deploy() { + kubectl apply -f k8s/ + terraform apply + } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("bash", content); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/shell/PowerShellDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/shell/PowerShellDetectorTest.java index 2c7f149a..0a58a7c2 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/shell/PowerShellDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/shell/PowerShellDetectorTest.java @@ -1,11 +1,127 @@ package io.github.randomcodespace.iq.detector.shell; -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 PowerShellDetectorTest { + private final PowerShellDetector d = new PowerShellDetector(); - @Test void detectsFunction() { - DetectorResult r = d.detect(DetectorTestUtils.contextFor("powershell", "function Get-Users {\n param()\n}")); - assertTrue(r.nodes().size() >= 1); assertEquals(NodeKind.METHOD, r.nodes().get(0).getKind()); + + @Test + void detectsFunction() { + String code = "function Get-Users {\n param()\n Get-ADUser -Filter *\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.METHOD && "Get-Users".equals(n.getLabel()))); + } + + @Test + void detectsMultipleFunctions() { + String code = """ + function Get-Users { } + function Set-Config { } + function Remove-OldData { } + """; + DetectorResult r = d.detect(ctx(code)); + assertEquals(3, r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).count()); + } + + @Test + void detectsCaseInsensitiveFunction() { + String code = "Function Get-Data {\n Write-Host \"hi\"\n}\n"; + DetectorResult r = d.detect(ctx(code)); + assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.METHOD)); + } + + @Test + void detectsImportModule() { + String code = "Import-Module Az\nImport-Module ActiveDirectory\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count()); + } + + @Test + void detectsDotSource() { + String code = ". ./helpers.ps1\n. ./config.psm1\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count()); + } + + @Test + void detectsDotSourceWithQuotes() { + String code = ". \"./helpers.ps1\"\n. './config.psm1'\n"; + DetectorResult r = d.detect(ctx(code)); + assertEquals(2, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count()); + } + + @Test + void detectsAdvancedFunction() { + // CmdletBinding after function definition -> marks as advanced + String code = """ + function Get-Report { + [CmdletBinding()] + param() + Get-Content report.txt + } + """; + DetectorResult r = d.detect(ctx(code)); + var fn = r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).findFirst().orElseThrow(); + assertEquals(true, fn.getProperties().get("advanced_function")); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorResult r = d.detect(ctx("")); + assertTrue(r.nodes().isEmpty()); + assertTrue(r.edges().isEmpty()); + } + + @Test + void nullContentReturnsEmpty() { + DetectorContext ctxNull = new DetectorContext("test.ps1", "powershell", null); + DetectorResult r = d.detect(ctxNull); + assertTrue(r.nodes().isEmpty()); + } + + @Test + void returnsCorrectName() { + assertEquals("powershell", d.getName()); + } + + @Test + void supportedLanguagesContainsPowershell() { + assertTrue(d.getSupportedLanguages().contains("powershell")); + } + + @Test + void deterministic() { + String code = """ + Import-Module Az + Import-Module ActiveDirectory + . ./shared.ps1 + function Get-AllUsers { + [CmdletBinding()] + param() + Get-ADUser -Filter * | Select-Object Name, Email + } + function Set-UserConfig { + param([string]$Username) + Write-Host "Configuring $Username" + } + function Remove-StaleData { + Import-Module SqlServer + Invoke-Sqlcmd -Query "DELETE FROM stale" + } + """; + DetectorTestUtils.assertDeterministic(d, ctx(code)); + } + + private static DetectorContext ctx(String content) { + return DetectorTestUtils.contextFor("powershell", content); } - @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("powershell", "")).nodes().size()); } - @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("powershell", "function Do-Thing {\n Import-Module Az\n}")); } } From e3a34ece4da36db76bdb7a54291be6e438457bb2 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 4 Apr 2026 08:13:11 +0000 Subject: [PATCH 2/5] test(coverage): expand TypeScript detector tests to boost coverage toward 70%+ Rewrote and expanded all 13 TypeScript detector test files, adding ~1,600 lines of new tests covering happy paths, negative paths, edge cases, and determinism for every detector in the typescript package. Co-Authored-By: Claude Sonnet 4.6 --- .../typescript/ExpressRouteDetectorTest.java | 100 ++++++++++ .../typescript/FastifyRouteDetectorTest.java | 97 +++++++++- .../GraphQLResolverDetectorTest.java | 117 +++++++++++- .../typescript/KafkaJSDetectorTest.java | 110 ++++++++++- .../typescript/MongooseORMDetectorTest.java | 127 ++++++++++++- .../NestJSControllerDetectorTest.java | 124 ++++++++++++ .../typescript/NestJSGuardsDetectorTest.java | 122 ++++++++++++ .../typescript/PassportJwtDetectorTest.java | 111 +++++++++++ .../typescript/PrismaORMDetectorTest.java | 125 ++++++++++++- .../typescript/RemixRouteDetectorTest.java | 133 +++++++++++++ .../typescript/SequelizeORMDetectorTest.java | 117 +++++++++++- .../typescript/TypeORMEntityDetectorTest.java | 154 ++++++++++++++- .../TypeScriptStructuresDetectorTest.java | 177 +++++++++++++++++- 13 files changed, 1597 insertions(+), 17 deletions(-) diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/ExpressRouteDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/ExpressRouteDetectorTest.java index 63b61cf8..760e4fb1 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/ExpressRouteDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/ExpressRouteDetectorTest.java @@ -6,6 +6,7 @@ import io.github.randomcodespace.iq.model.NodeKind; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class ExpressRouteDetectorTest { @@ -31,6 +32,80 @@ void detectsExpressRoutes() { assertEquals("app", result.nodes().get(0).getProperties().get("router")); } + @Test + void detectsAllHttpMethods() { + String code = """ + app.get('/items', getAll); + app.post('/items', create); + app.put('/items/:id', update); + app.patch('/items/:id', patch); + app.delete('/items/:id', remove); + app.options('/items', options); + app.head('/items', head); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/items.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(7, result.nodes().size()); + assertThat(result.nodes()).anyMatch(n -> n.getLabel().equals("GET /items")); + assertThat(result.nodes()).anyMatch(n -> n.getLabel().equals("PUT /items/:id")); + assertThat(result.nodes()).anyMatch(n -> n.getLabel().equals("PATCH /items/:id")); + assertThat(result.nodes()).anyMatch(n -> n.getLabel().equals("DELETE /items/:id")); + assertThat(result.nodes()).anyMatch(n -> n.getLabel().equals("OPTIONS /items")); + assertThat(result.nodes()).anyMatch(n -> n.getLabel().equals("HEAD /items")); + } + + @Test + void detectsRouterRoutes() { + String code = """ + const router = express.Router(); + router.get('/health', checkHealth); + router.post('/login', login); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/router.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(2, result.nodes().size()); + assertEquals("router", result.nodes().get(0).getProperties().get("router")); + assertEquals("GET /health", result.nodes().get(0).getLabel()); + } + + @Test + void detectsRoutesWithDoubleQuotes() { + String code = """ + app.get("/api/data", getData); + app.post("/api/data", postData); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/routes.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(2, result.nodes().size()); + assertEquals("GET /api/data", result.nodes().get(0).getLabel()); + } + + @Test + void detectsRoutesWithPathParams() { + String code = """ + app.get('/users/:userId/posts/:postId', getPost); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/routes.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(1, result.nodes().size()); + assertEquals("GET /users/:userId/posts/:postId", result.nodes().get(0).getLabel()); + assertEquals("/users/:userId/posts/:postId", result.nodes().get(0).getProperties().get("path_pattern")); + } + + @Test + void setsProtocolToREST() { + String code = "app.get('/ping', handler);"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/routes.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals("REST", result.nodes().get(0).getProperties().get("protocol")); + assertEquals("GET", result.nodes().get(0).getProperties().get("http_method")); + } + @Test void noMatchOnNonExpressCode() { String code = """ @@ -42,10 +117,35 @@ void noMatchOnNonExpressCode() { assertTrue(result.nodes().isEmpty()); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + + @Test + void noEdgesReturned() { + String code = "app.get('/test', handler);"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/routes.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + assertTrue(result.edges().isEmpty()); + } + @Test void deterministic() { String code = "app.get('/test', handler);\nrouter.post('/data', fn);"; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("typescript.express_routes", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/FastifyRouteDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/FastifyRouteDetectorTest.java index d22d3703..9d6e46f0 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/FastifyRouteDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/FastifyRouteDetectorTest.java @@ -3,9 +3,11 @@ 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.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class FastifyRouteDetectorTest { @@ -29,18 +31,71 @@ void detectsShorthandRoutes() { assertEquals("fastify", result.nodes().get(0).getProperties().get("framework")); } + @Test + void detectsAllHttpMethodsShorthand() { + String code = """ + import Fastify from 'fastify'; + fastify.get('/items', h); + fastify.post('/items', h); + fastify.put('/items/:id', h); + fastify.patch('/items/:id', h); + fastify.delete('/items/:id', h); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/items.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(5, result.nodes().size()); + assertThat(result.nodes()).anyMatch(n -> "GET /items".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "DELETE /items/:id".equals(n.getLabel())); + assertThat(result.nodes()).allMatch(n -> n.getKind() == NodeKind.ENDPOINT); + } + + @Test + void detectsRouteObjectStyle() { + String code = """ + import Fastify from 'fastify'; + fastify.route({ + method: 'GET', + url: '/api/health', + handler: async (request, reply) => ({ status: 'ok' }) + }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/health.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertThat(result.nodes()).anyMatch(n -> "GET /api/health".equals(n.getLabel())); + } + @Test void detectsHooks() { String code = """ import Fastify from 'fastify'; fastify.addHook('onRequest', async (request, reply) => {}); + fastify.addHook('preHandler', async (request, reply) => {}); """; DetectorContext ctx = DetectorTestUtils.contextFor("src/app.ts", "typescript", code); DetectorResult result = detector.detect(ctx); - assertEquals(1, result.nodes().size()); - assertEquals(NodeKind.MIDDLEWARE, result.nodes().get(0).getKind()); - assertEquals("hook:onRequest", result.nodes().get(0).getLabel()); + assertEquals(2, result.nodes().size()); + assertThat(result.nodes()).allMatch(n -> n.getKind() == NodeKind.MIDDLEWARE); + assertThat(result.nodes()).anyMatch(n -> "hook:onRequest".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "hook:preHandler".equals(n.getLabel())); + } + + @Test + void detectsPluginRegistrationEdges() { + String code = """ + import Fastify from 'fastify'; + import authPlugin from './auth'; + fastify.register(authPlugin); + fastify.register(corsPlugin); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/app.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.edges().isEmpty()); + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.IMPORTS); } @Test @@ -78,13 +133,49 @@ void matchesWithRequireFastify() { assertEquals("fastify", result.nodes().get(0).getProperties().get("framework")); } + @Test + void endpointNodeHasProtocol() { + String code = """ + import Fastify from 'fastify'; + fastify.get('/data', handler); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/routes.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals("REST", result.nodes().get(0).getProperties().get("protocol")); + assertEquals("GET", result.nodes().get(0).getProperties().get("http_method")); + } + + @Test + void detectsWithNamedImport() { + String code = """ + import { fastify } from 'fastify'; + fastify.get('/status', statusHandler); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/routes.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + assertFalse(result.nodes().isEmpty()); + } + @Test void deterministic() { String code = """ import Fastify from 'fastify'; fastify.get('/test', handler); + fastify.post('/test', handler); """; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("fastify_routes", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/GraphQLResolverDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/GraphQLResolverDetectorTest.java index 52c2c430..5a234724 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/GraphQLResolverDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/GraphQLResolverDetectorTest.java @@ -6,6 +6,7 @@ import io.github.randomcodespace.iq.model.NodeKind; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class GraphQLResolverDetectorTest { @@ -36,7 +37,30 @@ async createUser() {} } @Test - void detectsSchemaDefinedResolvers() { + void detectsNestJSResolverWithEntity() { + String code = """ + @Resolver(Post) + export class PostResolver { + @Query(() => [Post]) + async posts() {} + @Mutation(() => Post) + async deletePost() {} + @Subscription() + async postAdded() {} + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/post.resolver.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertTrue(result.nodes().size() >= 4); + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.CLASS && "PostResolver".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> n.getLabel().contains("Query")); + assertThat(result.nodes()).anyMatch(n -> n.getLabel().contains("Mutation")); + assertThat(result.nodes()).anyMatch(n -> n.getLabel().contains("Subscription")); + } + + @Test + void detectsSchemaDefinedQueryResolvers() { String code = """ type Query { users: [User] @@ -48,6 +72,56 @@ void detectsSchemaDefinedResolvers() { assertEquals(2, result.nodes().size()); assertEquals("GraphQL Query: users", result.nodes().get(0).getLabel()); + assertThat(result.nodes()).allMatch(n -> n.getKind() == NodeKind.ENDPOINT); + assertThat(result.nodes()).allMatch(n -> "GraphQL".equals(n.getProperties().get("protocol"))); + } + + @Test + void detectsSchemaDefinedMutationResolvers() { + String code = """ + type Mutation { + createUser(name: String!): User + updateUser(id: ID!, name: String): User + deleteUser(id: ID!): Boolean + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/schema.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(3, result.nodes().size()); + assertThat(result.nodes()).anyMatch(n -> "GraphQL Mutation: createUser".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "GraphQL Mutation: deleteUser".equals(n.getLabel())); + assertThat(result.nodes()).allMatch(n -> "mutation".equals(n.getProperties().get("operation_type"))); + } + + @Test + void detectsSubscriptionResolvers() { + String code = """ + type Subscription { + messageAdded: Message + userJoined: User + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/subscriptions.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(2, result.nodes().size()); + assertThat(result.nodes()).anyMatch(n -> "GraphQL Subscription: messageAdded".equals(n.getLabel())); + } + + @Test + void resolverAnnotationHasNestjsGraphqlFramework() { + String code = """ + @Resolver(of => User) + export class UserResolver { + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/user.resolver.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals("nestjs-graphql", result.nodes().get(0).getProperties().get("framework")); + assertThat(result.nodes().get(0).getAnnotations()).contains("@Resolver"); } @Test @@ -59,9 +133,48 @@ void noMatchOnNonGraphQLCode() { } @Test - void deterministic() { + void noMatchOnPlainTypeDefinition() { + // 'type' keyword without Query/Mutation/Subscription body should not produce endpoints + String code = """ + type User = { + id: string; + name: string; + }; + """; + DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + + @Test + void noEdgesReturned() { String code = "type Query { users: [User] }"; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); + DetectorResult result = detector.detect(ctx); + assertTrue(result.edges().isEmpty()); + } + + @Test + void deterministic() { + String code = "type Query { users: [User] }\ntype Mutation { createUser: User }"; + DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("typescript.graphql_resolvers", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/KafkaJSDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/KafkaJSDetectorTest.java index be01596e..540075bb 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/KafkaJSDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/KafkaJSDetectorTest.java @@ -3,9 +3,11 @@ 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.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class KafkaJSDetectorTest { @@ -37,6 +39,91 @@ void detectsKafkaUsage() { assertTrue(result.edges().size() >= 2); } + @Test + void detectsKafkaConnectionNode() { + String code = """ + const kafka = new Kafka({ + brokers: ['kafka:9092'] + }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/kafka.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var conn = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.DATABASE_CONNECTION) + .findFirst(); + assertTrue(conn.isPresent()); + assertEquals("kafka", conn.get().getProperties().get("broker")); + assertEquals("kafkajs", conn.get().getProperties().get("library")); + } + + @Test + void detectsProducerSendWithTopicNode() { + String code = """ + const kafka = new Kafka({ brokers: [] }); + const producer = kafka.producer(); + await producer.send({ topic: 'order-placed', messages: [{ value: 'data' }] }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/producer.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.TOPIC + && n.getLabel().contains("order-placed")); + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.PRODUCES); + } + + @Test + void detectsConsumerWithGroupIdAndSubscribe() { + String code = """ + const kafka = new Kafka({ brokers: [] }); + const consumer = kafka.consumer({ groupId: 'payments-service' }); + await consumer.subscribe({ topic: 'order-placed' }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/consumer.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var consumerNode = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.TOPIC && "consumer".equals(n.getProperties().get("role"))) + .findFirst(); + assertTrue(consumerNode.isPresent()); + assertEquals("payments-service", consumerNode.get().getProperties().get("group_id")); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.CONSUMES); + } + + @Test + void detectsEachMessageHandler() { + String code = """ + const kafka = new Kafka({ brokers: [] }); + await consumer.run({ eachMessage: async ({ message }) => { + console.log(message.value); + }}); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/consumer.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.EVENT + && "kafka:eachMessage".equals(n.getLabel())); + } + + @Test + void topicDeduplicationForSameTopicProducedAndConsumed() { + // Same topic referenced by send and subscribe should only produce one TOPIC node + String code = """ + const kafka = new Kafka({ brokers: [] }); + producer.send({ topic: 'events', messages: [] }); + consumer.subscribe({ topic: 'events' }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/events.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + long topicCount = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.TOPIC && n.getLabel().contains("events")) + .count(); + assertEquals(1, topicCount, "Same topic should not be deduplicated into multiple nodes"); + } + @Test void noMatchWithoutKafka() { String code = "const x = 42;"; @@ -46,10 +133,31 @@ void noMatchWithoutKafka() { assertTrue(result.edges().isEmpty()); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + @Test void deterministic() { - String code = "const kafka = new Kafka({\n brokers: []\n});"; + String code = """ + const kafka = new Kafka({ brokers: [] }); + const producer = kafka.producer(); + producer.send({ topic: 'events', messages: [] }); + """; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("kafka_js", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/MongooseORMDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/MongooseORMDetectorTest.java index 3be43db5..f3c01a79 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/MongooseORMDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/MongooseORMDetectorTest.java @@ -3,9 +3,11 @@ 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.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class MongooseORMDetectorTest { @@ -34,6 +36,112 @@ void detectsMongooseUsage() { assertFalse(result.edges().isEmpty()); } + @Test + void detectsConnectionNode() { + String code = "mongoose.connect('mongodb://localhost:27017/mydb');"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/db.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var conn = result.nodes().get(0); + assertEquals(NodeKind.DATABASE_CONNECTION, conn.getKind()); + assertEquals("mongoose.connect", conn.getLabel()); + assertEquals("mongoose", conn.getProperties().get("framework")); + } + + @Test + void detectsSchemaAsEntity() { + String code = """ + const postSchema = new mongoose.Schema({ + title: { type: String, required: true }, + body: String, + createdAt: Date + }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/post.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var schema = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.ENTITY) + .findFirst(); + assertTrue(schema.isPresent()); + assertEquals("postSchema", schema.get().getLabel()); + assertEquals("schema", schema.get().getProperties().get("definition")); + } + + @Test + void detectsModelAsEntity() { + String code = """ + const User = mongoose.model('User', userSchema); + const Post = mongoose.model('Post', postSchema); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/models.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(2, result.nodes().size()); + assertThat(result.nodes()).allMatch(n -> n.getKind() == NodeKind.ENTITY); + assertThat(result.nodes()).allMatch(n -> "model".equals(n.getProperties().get("definition"))); + assertThat(result.nodes()).anyMatch(n -> "User".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "Post".equals(n.getLabel())); + } + + @Test + void detectsQueryOperationsAsEdges() { + String code = """ + const userSchema = new Schema({ name: String }); + const User = mongoose.model('User', userSchema); + User.find({ active: true }); + User.findById(id); + User.updateOne({ _id: id }, data); + User.deleteMany({}); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/user.repo.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.edges().isEmpty()); + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.QUERIES + && "find".equals(e.getProperties().get("operation"))); + assertThat(result.edges()).anyMatch(e -> "deleteMany".equals(e.getProperties().get("operation"))); + } + + @Test + void detectsSchemaHooksAsEvents() { + String code = """ + const userSchema = new mongoose.Schema({ name: String }); + userSchema.pre('save', async function(next) { next(); }); + userSchema.post('find', function(docs) {}); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/user.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.EVENT + && "pre:save".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.EVENT + && "post:find".equals(n.getLabel())); + } + + @Test + void detectsVirtuals() { + String code = """ + const userSchema = new mongoose.Schema({ + firstName: String, + lastName: String + }); + userSchema.virtual('fullName').get(function() { + return this.firstName + ' ' + this.lastName; + }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/user.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var schemaNode = result.nodes().stream() + .filter(n -> "userSchema".equals(n.getLabel())) + .findFirst(); + assertTrue(schemaNode.isPresent()); + assertNotNull(schemaNode.get().getProperties().get("virtuals")); + } + @Test void noMatchOnNonMongooseCode() { String code = "const x = 42;"; @@ -42,10 +150,27 @@ void noMatchOnNonMongooseCode() { assertTrue(result.nodes().isEmpty()); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + @Test void deterministic() { - String code = "mongoose.connect('mongodb://localhost');\nconst s = new Schema({});"; + String code = "mongoose.connect('mongodb://localhost');\nconst s = new Schema({});\nconst M = mongoose.model('M', s);"; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("mongoose_orm", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSControllerDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSControllerDetectorTest.java index b6c79954..560e6fac 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSControllerDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSControllerDetectorTest.java @@ -7,6 +7,7 @@ import io.github.randomcodespace.iq.model.NodeKind; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class NestJSControllerDetectorTest { @@ -43,6 +44,112 @@ export class UsersController { e.getKind() == EdgeKind.EXPOSES && e.getTarget() != null)); } + @Test + void detectsControllerClassNode() { + String code = """ + import { Controller, Get } from '@nestjs/common'; + @Controller('products') + export class ProductsController { + @Get() + findAll() {} + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/products.controller.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var classNode = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.CLASS) + .findFirst(); + assertTrue(classNode.isPresent()); + assertEquals("ProductsController", classNode.get().getLabel()); + assertThat(classNode.get().getAnnotations()).contains("@Controller"); + assertEquals("controller", classNode.get().getProperties().get("stereotype")); + } + + @Test + void buildsFullPathFromControllerAndRoute() { + String code = """ + import { Controller, Get, Post } from '@nestjs/common'; + @Controller('api/v1/users') + export class UsersController { + @Get('/:id') + findOne() {} + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/users.controller.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> + n.getKind() == NodeKind.ENDPOINT && "GET /api/v1/users/:id".equals(n.getLabel())); + } + + @Test + void detectsAllHttpMethodDecorators() { + String code = """ + import { Controller, Get, Post, Put, Delete, Patch } from '@nestjs/common'; + @Controller('items') + export class ItemsController { + @Get() + list() {} + @Post() + create() {} + @Put('/:id') + update() {} + @Delete('/:id') + remove() {} + @Patch('/:id') + patch() {} + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/items.controller.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> "GET /items".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "POST /items".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "PUT /items/:id".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "DELETE /items/:id".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "PATCH /items/:id".equals(n.getLabel())); + } + + @Test + void endpointHasRestProtocol() { + String code = """ + import { Controller, Get } from '@nestjs/common'; + @Controller('data') + export class DataController { + @Get() + getData() {} + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/data.controller.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var endpoint = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.ENDPOINT) + .findFirst(); + assertTrue(endpoint.isPresent()); + assertEquals("REST", endpoint.get().getProperties().get("protocol")); + assertEquals("nestjs", endpoint.get().getProperties().get("framework")); + } + + @Test + void detectsHttpClientCallEdge() { + String code = """ + import { Controller, Get } from '@nestjs/common'; + @Controller('proxy') + export class ProxyController { + @Get('/external') + async getExternal() { + return this.httpService.get('https://api.example.com/data'); + } + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/proxy.controller.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.CALLS); + } + @Test void noMatchWithoutNestJSImport() { // Generic TypeScript with @Controller-like patterns but no @nestjs import @@ -99,10 +206,27 @@ export class ItemsComponent { assertTrue(result.nodes().isEmpty(), "Should not match Angular component without @nestjs/ import"); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + @Test void deterministic() { String code = "import { Controller, Get } from '@nestjs/common';\n@Controller('test')\nexport class TestController {\n @Get()\n find() {}\n}"; DetectorContext ctx = DetectorTestUtils.contextFor("src/test.controller.ts", "typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("typescript.nestjs_controllers", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetectorTest.java index 15c459a5..12573999 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetectorTest.java @@ -6,6 +6,9 @@ import io.github.randomcodespace.iq.model.NodeKind; import org.junit.jupiter.api.Test; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class NestJSGuardsDetectorTest { @@ -36,6 +39,100 @@ void detectsGuardsAndRoles() { n.getLabel().contains("Roles(admin, user)"))); } + @Test + void detectsUseGuardsWithSingleGuard() { + String code = """ + import { UseGuards } from '@nestjs/common'; + @UseGuards(JwtAuthGuard) + class UsersController {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/users.controller.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var guard = result.nodes().get(0); + assertEquals(NodeKind.GUARD, guard.getKind()); + assertEquals("UseGuards(JwtAuthGuard)", guard.getLabel()); + assertEquals("nestjs_guard", guard.getProperties().get("auth_type")); + assertEquals("JwtAuthGuard", guard.getProperties().get("guard_name")); + assertThat(guard.getAnnotations()).contains("@UseGuards"); + } + + @Test + void detectsUseGuardsWithMultipleGuards() { + String code = """ + import { UseGuards } from '@nestjs/common'; + @UseGuards(AuthGuard, RolesGuard, ThrottleGuard) + class AppController {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/app.controller.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(3, result.nodes().size()); + assertThat(result.nodes()).anyMatch(n -> "UseGuards(AuthGuard)".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "UseGuards(RolesGuard)".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "UseGuards(ThrottleGuard)".equals(n.getLabel())); + } + + @Test + void detectsRolesDecorator() { + String code = """ + import { UseGuards } from '@nestjs/common'; + @Roles('admin', 'super-admin') + @UseGuards(RolesGuard) + class AdminController {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/admin.controller.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var rolesNode = result.nodes().stream() + .filter(n -> n.getLabel().startsWith("Roles(")) + .findFirst(); + assertTrue(rolesNode.isPresent()); + @SuppressWarnings("unchecked") + List roles = (List) rolesNode.get().getProperties().get("roles"); + assertThat(roles).contains("admin", "super-admin"); + assertThat(rolesNode.get().getAnnotations()).contains("@Roles"); + } + + @Test + void detectsCanActivateImplementation() { + String code = """ + import { CanActivate } from '@nestjs/common'; + export class CustomGuard implements CanActivate { + async canActivate(context: ExecutionContext) { + return this.authService.validate(context); + } + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/custom.guard.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var canActivateNode = result.nodes().stream() + .filter(n -> "canActivate()".equals(n.getLabel())) + .findFirst(); + assertTrue(canActivateNode.isPresent()); + assertEquals("canActivate", canActivateNode.get().getProperties().get("guard_impl")); + } + + @Test + void detectsAuthGuardStrategy() { + String code = """ + import { UseGuards } from '@nestjs/common'; + @UseGuards(AuthGuard('jwt')) + class JwtController {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/jwt.controller.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var authGuardNode = result.nodes().stream() + .filter(n -> n.getLabel().startsWith("AuthGuard(")) + .findFirst(); + assertTrue(authGuardNode.isPresent()); + assertEquals("jwt", authGuardNode.get().getProperties().get("strategy")); + } + @Test void noMatchWithoutNestJSImport() { // Generic TypeScript with canActivate() but no @nestjs import @@ -73,10 +170,35 @@ export class AuthGuard implements CanActivate { assertTrue(result.nodes().isEmpty(), "Should not match Angular guard without @nestjs/ import"); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + + @Test + void noEdgesReturned() { + String code = "import { UseGuards } from '@nestjs/common';\n@UseGuards(AuthGuard)\ncanActivate() {}"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/auth.guard.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + assertTrue(result.edges().isEmpty()); + } + @Test void deterministic() { String code = "import { UseGuards } from '@nestjs/common';\n@UseGuards(AuthGuard)\n@Roles('admin')"; DetectorContext ctx = DetectorTestUtils.contextFor("src/auth.guard.ts", "typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("typescript.nestjs_guards", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/PassportJwtDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/PassportJwtDetectorTest.java index 7368b292..1b45e670 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/PassportJwtDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/PassportJwtDetectorTest.java @@ -6,6 +6,7 @@ import io.github.randomcodespace.iq.model.NodeKind; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class PassportJwtDetectorTest { @@ -29,6 +30,91 @@ void detectsPassportAndJwt() { assertEquals("passport", result.nodes().get(0).getProperties().get("auth_type")); } + @Test + void detectsPassportUseWithJwtStrategy() { + String code = "passport.use(new JwtStrategy({ secretOrKey: 'secret' }, callback));"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/auth.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var node = result.nodes().get(0); + assertEquals(NodeKind.GUARD, node.getKind()); + assertEquals("passport", node.getProperties().get("auth_type")); + assertEquals("JwtStrategy", node.getProperties().get("strategy")); + } + + @Test + void detectsPassportUseWithLocalStrategy() { + String code = "passport.use(new LocalStrategy({ usernameField: 'email' }, cb));"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/auth.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals("LocalStrategy", result.nodes().get(0).getProperties().get("strategy")); + } + + @Test + void detectsPassportAuthenticate() { + String code = "app.post('/login', passport.authenticate('local', { session: false }));"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/routes.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var node = result.nodes().get(0); + assertEquals(NodeKind.MIDDLEWARE, node.getKind()); + assertEquals("jwt", node.getProperties().get("auth_type")); + assertEquals("local", node.getProperties().get("strategy")); + } + + @Test + void detectsJwtVerify() { + String code = "const decoded = jwt.verify(token, process.env.JWT_SECRET);"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/middleware.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals(NodeKind.MIDDLEWARE, result.nodes().get(0).getKind()); + assertEquals("jwt", result.nodes().get(0).getProperties().get("auth_type")); + } + + @Test + void detectsRequireExpressJwt() { + String code = "const expressJwt = require('express-jwt');"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/auth.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals(NodeKind.MIDDLEWARE, result.nodes().get(0).getKind()); + assertEquals("express-jwt", result.nodes().get(0).getProperties().get("library")); + } + + @Test + void detectsImportExpressJwt() { + String code = "import { expressjwt } from 'express-jwt';"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/auth.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals(NodeKind.MIDDLEWARE, result.nodes().get(0).getKind()); + assertEquals("express-jwt", result.nodes().get(0).getProperties().get("library")); + } + + @Test + void detectsMultipleStrategies() { + String code = """ + passport.use(new JwtStrategy(opts, cb)); + passport.use(new GoogleStrategy(opts, cb)); + passport.use(new GitHubStrategy(opts, cb)); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/auth.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(3, result.nodes().size()); + assertThat(result.nodes()).anyMatch(n -> "JwtStrategy".equals(n.getProperties().get("strategy"))); + assertThat(result.nodes()).anyMatch(n -> "GoogleStrategy".equals(n.getProperties().get("strategy"))); + assertThat(result.nodes()).anyMatch(n -> "GitHubStrategy".equals(n.getProperties().get("strategy"))); + } + @Test void noMatchOnNonAuthCode() { String code = "const x = 42;"; @@ -37,10 +123,35 @@ void noMatchOnNonAuthCode() { assertTrue(result.nodes().isEmpty()); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + + @Test + void noEdgesReturned() { + String code = "passport.use(new JwtStrategy(opts));\njwt.verify(token, secret);"; + DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); + DetectorResult result = detector.detect(ctx); + assertTrue(result.edges().isEmpty()); + } + @Test void deterministic() { String code = "passport.use(new JwtStrategy(opts));\njwt.verify(token, secret);"; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("typescript.passport_jwt", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/PrismaORMDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/PrismaORMDetectorTest.java index 79197f04..ca4e6780 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/PrismaORMDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/PrismaORMDetectorTest.java @@ -3,9 +3,11 @@ 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.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class PrismaORMDetectorTest { @@ -31,6 +33,106 @@ void detectsPrismaUsage() { assertTrue(result.edges().size() >= 3); } + @Test + void detectsPrismaClientConnectionNode() { + String code = "const prisma = new PrismaClient();"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/db.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var connNode = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.DATABASE_CONNECTION) + .findFirst(); + assertTrue(connNode.isPresent()); + assertEquals("PrismaClient", connNode.get().getLabel()); + assertEquals("prisma", connNode.get().getProperties().get("framework")); + } + + @Test + void detectsPrismaImportEdge() { + String code = "import { PrismaClient } from '@prisma/client';"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/db.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.edges().isEmpty()); + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.IMPORTS); + } + + @Test + void detectsPrismaRequireImport() { + String code = "const { PrismaClient } = require('@prisma/client');"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/db.js", "javascript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.IMPORTS); + } + + @Test + void detectsModelEntitiesFromOperations() { + String code = """ + const prisma = new PrismaClient(); + const allUsers = await prisma.user.findMany(); + const profile = await prisma.profile.findUnique({ where: { id: 1 } }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/repo.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.ENTITY && "user".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.ENTITY && "profile".equals(n.getLabel())); + } + + @Test + void detectsMultipleOperationsAsQueryEdges() { + String code = """ + prisma.user.findMany(); + prisma.user.create({ data: {} }); + prisma.user.delete({ where: { id: 1 } }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/repo.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.QUERIES + && "findMany".equals(e.getProperties().get("operation"))); + assertThat(result.edges()).anyMatch(e -> "create".equals(e.getProperties().get("operation"))); + assertThat(result.edges()).anyMatch(e -> "delete".equals(e.getProperties().get("operation"))); + } + + @Test + void detectsTransactionFlag() { + String code = """ + const prisma = new PrismaClient(); + await prisma.$transaction([ + prisma.user.create({ data: {} }), + prisma.profile.create({ data: {} }) + ]); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/db.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var connNode = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.DATABASE_CONNECTION) + .findFirst(); + assertTrue(connNode.isPresent()); + assertEquals(true, connNode.get().getProperties().get("transaction")); + } + + @Test + void deduplicatesModelNodes() { + // Multiple operations on same model should produce only one ENTITY node + String code = """ + prisma.user.findMany(); + prisma.user.create({ data: {} }); + prisma.user.delete({ where: {} }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/repo.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + long userEntityCount = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.ENTITY && "user".equals(n.getLabel())) + .count(); + assertEquals(1, userEntityCount); + } + @Test void noMatchOnNonPrismaCode() { String code = "const x = 42;"; @@ -39,10 +141,31 @@ void noMatchOnNonPrismaCode() { assertTrue(result.nodes().isEmpty()); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + @Test void deterministic() { - String code = "const p = new PrismaClient();\nprisma.user.findMany();"; + String code = """ + const p = new PrismaClient(); + prisma.user.findMany(); + prisma.post.create({ data: {} }); + """; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("prisma_orm", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/RemixRouteDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/RemixRouteDetectorTest.java index d938fbec..e4ce6b7b 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/RemixRouteDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/RemixRouteDetectorTest.java @@ -6,6 +6,7 @@ import io.github.randomcodespace.iq.model.NodeKind; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class RemixRouteDetectorTest { @@ -38,6 +39,121 @@ export default function Users() { assertEquals("/users", result.nodes().get(0).getProperties().get("route_path")); } + @Test + void detectsLoaderWithHttpGetMethod() { + String code = "export async function loader({ request }) { return json({}); }"; + DetectorContext ctx = DetectorTestUtils.contextFor("app/routes/items.tsx", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var loader = result.nodes().get(0); + assertEquals(NodeKind.ENDPOINT, loader.getKind()); + assertEquals("GET", loader.getProperties().get("http_method")); + assertEquals("loader", loader.getProperties().get("type")); + assertEquals("remix", loader.getProperties().get("framework")); + } + + @Test + void detectsActionWithHttpPostMethod() { + String code = "export async function action({ request }) { return redirect('/'); }"; + DetectorContext ctx = DetectorTestUtils.contextFor("app/routes/items.tsx", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var action = result.nodes().get(0); + assertEquals(NodeKind.ENDPOINT, action.getKind()); + assertEquals("POST", action.getProperties().get("http_method")); + assertEquals("action", action.getProperties().get("type")); + } + + @Test + void detectsDefaultComponentExport() { + String code = """ + export default function ProductPage() { + return
Products
; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("app/routes/products.tsx", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals(NodeKind.COMPONENT, result.nodes().get(0).getKind()); + assertEquals("ProductPage", result.nodes().get(0).getLabel()); + } + + @Test + void derivesRoutePathFromFilePath() { + String code = "export async function loader() { return json({}); }"; + DetectorContext ctx = DetectorTestUtils.contextFor( + "app/routes/blog.posts.tsx", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals("/blog/posts", result.nodes().get(0).getProperties().get("route_path")); + } + + @Test + void derivesIndexRouteFromUnderscore() { + String code = "export async function loader() { return json([]); }"; + DetectorContext ctx = DetectorTestUtils.contextFor( + "app/routes/_index.tsx", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals("/", result.nodes().get(0).getProperties().get("route_path")); + } + + @Test + void derivesParamRouteFromDollarSign() { + String code = "export async function loader() { return json({}); }"; + DetectorContext ctx = DetectorTestUtils.contextFor( + "app/routes/users.$id.tsx", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals("/users/:id", result.nodes().get(0).getProperties().get("route_path")); + } + + @Test + void noRoutePathWhenNotUnderAppRoutes() { + String code = "export async function loader() { return json({}); }"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/utils/loader.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertNull(result.nodes().get(0).getProperties().get("route_path")); + } + + @Test + void componentWithLoaderDataFlagSet() { + String code = """ + export default function Index() { + const data = useLoaderData(); + return
{data}
; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("app/routes/_index.tsx", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals(true, result.nodes().get(0).getProperties().get("uses_loader_data")); + } + + @Test + void componentWithActionDataFlagSet() { + String code = """ + export default function Form() { + const actionData = useActionData(); + return
{actionData}
; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("app/routes/form.tsx", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertEquals(true, result.nodes().get(0).getProperties().get("uses_action_data")); + } + @Test void noMatchOnNonRemixCode() { String code = "const x = 42;"; @@ -46,6 +162,13 @@ void noMatchOnNonRemixCode() { assertTrue(result.nodes().isEmpty()); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("app/routes/empty.tsx", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + @Test void deterministic() { String code = "export async function loader() {}\nexport default function Page() {}"; @@ -53,4 +176,14 @@ void deterministic() { "app/routes/_index.tsx", "typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("remix_routes", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/SequelizeORMDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/SequelizeORMDetectorTest.java index c8ecc486..b2f07798 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/SequelizeORMDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/SequelizeORMDetectorTest.java @@ -3,9 +3,11 @@ 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.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class SequelizeORMDetectorTest { @@ -32,6 +34,98 @@ class Post extends Model {} assertTrue(result.edges().size() >= 2); } + @Test + void detectsSequelizeConnectionNode() { + String code = "const sequelize = new Sequelize('postgres://user:pass@localhost:5432/db');"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/db.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + var conn = result.nodes().get(0); + assertEquals(NodeKind.DATABASE_CONNECTION, conn.getKind()); + assertEquals("Sequelize", conn.getLabel()); + assertEquals("sequelize", conn.getProperties().get("framework")); + } + + @Test + void detectsDefineModel() { + String code = """ + sequelize.define('User', { name: DataTypes.STRING, email: DataTypes.STRING }); + sequelize.define('Post', { title: DataTypes.STRING, body: DataTypes.TEXT }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/models.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(2, result.nodes().size()); + assertThat(result.nodes()).anyMatch(n -> "User".equals(n.getLabel()) && "define".equals(n.getProperties().get("definition"))); + assertThat(result.nodes()).anyMatch(n -> "Post".equals(n.getLabel())); + } + + @Test + void detectsClassExtendsModel() { + String code = """ + class User extends Model {} + class Comment extends Model {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/models.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(2, result.nodes().size()); + assertThat(result.nodes()).allMatch(n -> n.getKind() == NodeKind.ENTITY); + assertThat(result.nodes()).allMatch(n -> "class".equals(n.getProperties().get("definition"))); + } + + @Test + void detectsAssociationsAsEdges() { + String code = """ + const sequelize = new Sequelize('sqlite::memory:'); + const User = sequelize.define('User', {}); + const Post = sequelize.define('Post', {}); + User.hasMany(Post); + Post.belongsTo(User); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/assoc.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.DEPENDS_ON + && "hasMany".equals(e.getProperties().get("association"))); + assertThat(result.edges()).anyMatch(e -> "belongsTo".equals(e.getProperties().get("association"))); + } + + @Test + void detectsQueryOperations() { + String code = """ + const sequelize = new Sequelize('sqlite::memory:'); + const User = sequelize.define('User', {}); + User.findAll({ where: {} }); + User.findOne({ where: { id: 1 } }); + User.create({ name: 'Alice' }); + User.destroy({ where: {} }); + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/repo.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.QUERIES + && "findAll".equals(e.getProperties().get("operation"))); + assertThat(result.edges()).anyMatch(e -> "destroy".equals(e.getProperties().get("operation"))); + } + + @Test + void doesNotDuplicateClassAndDefineModels() { + // If same model defined via define() and class extends, don't duplicate + String code = """ + const User = sequelize.define('User', {}); + class User extends Model {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/models.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + long userCount = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.ENTITY && "User".equals(n.getLabel())) + .count(); + assertEquals(1, userCount, "User should not be duplicated from both define and class extends"); + } + @Test void noMatchOnNonSequelizeCode() { String code = "const x = 42;"; @@ -40,10 +134,31 @@ void noMatchOnNonSequelizeCode() { assertTrue(result.nodes().isEmpty()); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + @Test void deterministic() { - String code = "const s = new Sequelize('test');\nsequelize.define('Item', {});"; + String code = """ + const s = new Sequelize('test'); + sequelize.define('Item', {}); + Item.findAll(); + """; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("sequelize_orm", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/TypeORMEntityDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/TypeORMEntityDetectorTest.java index 1545874e..d1600d3c 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/TypeORMEntityDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/TypeORMEntityDetectorTest.java @@ -3,9 +3,13 @@ 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 java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class TypeORMEntityDetectorTest { @@ -37,6 +41,137 @@ export class User { assertEquals(2, result.edges().size()); } + @Test + void detectsEntityWithExplicitTableName() { + String code = """ + @Entity('order_items') + export class OrderItem { + @Column() + quantity: number; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/order-item.entity.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var entity = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.ENTITY) + .findFirst(); + assertTrue(entity.isPresent()); + assertEquals("order_items", entity.get().getProperties().get("table_name")); + assertThat(entity.get().getAnnotations()).contains("@Entity"); + } + + @Test + void detectsEntityWithoutTableNameUsesClassName() { + // @Entity() without table name: defaults to lowercase class name + 's' + String code = """ + @Entity() + export class Product { + @Column() + name: string; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/product.entity.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var entity = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.ENTITY) + .findFirst(); + assertTrue(entity.isPresent()); + assertEquals("Product", entity.get().getLabel()); + // Default table name = className.toLowerCase() + 's' + assertEquals("products", entity.get().getProperties().get("table_name")); + } + + @Test + void extractsColumns() { + String code = """ + @Entity('products') + export class Product { + @Column() + name: string; + @Column() + price: number; + @Column() + description: string; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/product.entity.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + var entity = result.nodes().stream() + .filter(n -> n.getKind() == NodeKind.ENTITY) + .findFirst(); + assertTrue(entity.isPresent()); + @SuppressWarnings("unchecked") + List columns = (List) entity.get().getProperties().get("columns"); + assertNotNull(columns); + assertThat(columns).contains("name", "price", "description"); + } + + @Test + void detectsRelationshipsAsEdges() { + String code = """ + @Entity('orders') + export class Order { + @ManyToOne(() => User) + user: User; + @OneToMany(() => OrderItem) + items: OrderItem[]; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/order.entity.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.MAPS_TO); + } + + @Test + void detectsManyToManyRelationship() { + String code = """ + @Entity('students') + export class Student { + @ManyToMany(() => Course) + courses: Course[]; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/student.entity.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.MAPS_TO); + } + + @Test + void detectsOneToOneRelationship() { + String code = """ + @Entity('profiles') + export class Profile { + @OneToOne(() => User) + user: User; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/profile.entity.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.MAPS_TO); + } + + @Test + void connectsToDatabaseNode() { + String code = """ + @Entity('items') + export class Item { + @Column() + name: string; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/item.entity.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.edges()).anyMatch(e -> e.getKind() == EdgeKind.CONNECTS_TO); + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.DATABASE_CONNECTION); + } + @Test void noMatchOnNonTypeORMCode() { String code = "class SomeService {}"; @@ -45,10 +180,27 @@ void noMatchOnNonTypeORMCode() { assertTrue(result.nodes().isEmpty()); } + @Test + void emptyContentReturnsEmpty() { + DetectorContext ctx = DetectorTestUtils.contextFor("src/empty.ts", "typescript", ""); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); + } + @Test void deterministic() { - String code = "@Entity()\nexport class Item {\n @Column()\n name: string;\n}"; + String code = "@Entity('items')\nexport class Item {\n @Column()\n name: string;\n}"; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); DetectorTestUtils.assertDeterministic(detector, ctx); } + + @Test + void getName() { + assertEquals("typescript.typeorm_entities", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript"); + } } diff --git a/src/test/java/io/github/randomcodespace/iq/detector/typescript/TypeScriptStructuresDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/typescript/TypeScriptStructuresDetectorTest.java index 0767f17a..d4ab39ef 100644 --- a/src/test/java/io/github/randomcodespace/iq/detector/typescript/TypeScriptStructuresDetectorTest.java +++ b/src/test/java/io/github/randomcodespace/iq/detector/typescript/TypeScriptStructuresDetectorTest.java @@ -3,9 +3,11 @@ 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.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class TypeScriptStructuresDetectorTest { @@ -38,18 +40,147 @@ export enum UserRole { ADMIN, USER } } @Test - void noMatchOnEmptyFile() { - String code = ""; - DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); + void detectsInterface() { + String code = """ + export interface UserDTO { + id: string; + name: string; + } + interface InternalState { + count: number; + } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/user.ts", "typescript", code); DetectorResult result = detector.detect(ctx); - assertTrue(result.nodes().isEmpty()); + + long ifaces = result.nodes().stream().filter(n -> n.getKind() == NodeKind.INTERFACE).count(); + assertEquals(2, ifaces); + assertThat(result.nodes()).anyMatch(n -> "UserDTO".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "InternalState".equals(n.getLabel())); } @Test - void deterministic() { - String code = "interface A {}\nclass B {}\nfunction c() {}"; + void detectsTypeAlias() { + String code = """ + export type UserId = string; + type Handler = (req: Request) => Response; + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/types.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.CLASS + && "UserId".equals(n.getLabel()) + && Boolean.TRUE.equals(n.getProperties().get("type_alias"))); + assertThat(result.nodes()).anyMatch(n -> "Handler".equals(n.getLabel())); + } + + @Test + void detectsClass() { + String code = """ + export class UserService { + findAll() {} + } + export abstract class BaseService {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/user.service.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + long classes = result.nodes().stream().filter(n -> n.getKind() == NodeKind.CLASS).count(); + assertEquals(2, classes); + assertThat(result.nodes()).anyMatch(n -> "UserService".equals(n.getLabel())); + } + + @Test + void detectsNamedFunction() { + String code = """ + export function getUser(id: string) {} + export async function createUser(data: any) {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/user.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.METHOD && "getUser".equals(n.getLabel())); + var createUserFn = result.nodes().stream() + .filter(n -> "createUser".equals(n.getLabel())) + .findFirst(); + assertTrue(createUserFn.isPresent()); + assertEquals(NodeKind.METHOD, createUserFn.get().getKind()); + } + + @Test + void detectsArrowFunction() { + String code = """ + export const handleRequest = async (req: Request) => { + return req.body; + }; + const helper = (x: number) => x * 2; + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/handler.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.METHOD && "handleRequest".equals(n.getLabel())); + } + + @Test + void detectsEnum() { + String code = """ + export enum UserRole { ADMIN, USER, GUEST } + export const enum Direction { Up, Down, Left, Right } + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/enums.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + long enums = result.nodes().stream().filter(n -> n.getKind() == NodeKind.ENUM).count(); + assertEquals(2, enums); + assertThat(result.nodes()).anyMatch(n -> "UserRole".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "Direction".equals(n.getLabel())); + } + + @Test + void detectsNamespace() { + String code = """ + export namespace Utils { + export function helper() {} + } + namespace Internal {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/utils.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertThat(result.nodes()).anyMatch(n -> n.getKind() == NodeKind.MODULE && "Utils".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "Internal".equals(n.getLabel())); + } + + @Test + void detectsImportsAsEdges() { + String code = """ + import { UserService } from './user.service'; + import { DatabaseModule } from '@app/database'; + import express from 'express'; + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/app.ts", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertEquals(3, result.edges().size()); + assertThat(result.edges()).allMatch(e -> e.getKind() == EdgeKind.IMPORTS); + } + + @Test + void detectsDefaultExportFunction() { + String code = "export default function HomePage() { return null; }"; + DetectorContext ctx = DetectorTestUtils.contextFor("src/home.tsx", "typescript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertThat(result.nodes()).anyMatch(n -> "HomePage".equals(n.getLabel()) && n.getKind() == NodeKind.METHOD); + } + + @Test + void noMatchOnEmptyFile() { + String code = ""; DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); - DetectorTestUtils.assertDeterministic(detector, ctx); + DetectorResult result = detector.detect(ctx); + assertTrue(result.nodes().isEmpty()); } @Test @@ -66,4 +197,36 @@ export function handler() {} .count(); assertEquals(1, handlerCount); } + + @Test + void worksForJavaScriptFiles() { + String code = """ + const express = require('express'); + function createRouter() {} + class UserController {} + """; + DetectorContext ctx = DetectorTestUtils.contextFor("src/router.js", "javascript", code); + DetectorResult result = detector.detect(ctx); + + assertFalse(result.nodes().isEmpty()); + assertThat(result.nodes()).anyMatch(n -> "createRouter".equals(n.getLabel())); + assertThat(result.nodes()).anyMatch(n -> "UserController".equals(n.getLabel())); + } + + @Test + void deterministic() { + String code = "interface A {}\nclass B {}\nfunction c() {}\nexport enum D { X }"; + DetectorContext ctx = DetectorTestUtils.contextFor("typescript", code); + DetectorTestUtils.assertDeterministic(detector, ctx); + } + + @Test + void getName() { + assertEquals("typescript_structures", detector.getName()); + } + + @Test + void getSupportedLanguages() { + assertThat(detector.getSupportedLanguages()).contains("typescript", "javascript"); + } } From 98c13a7129523e4a1301077afaa73c383af6b15b Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 4 Apr 2026 08:14:12 +0000 Subject: [PATCH 3/5] test(coverage): expand frontend/C#/auth/linker/cache/intelligence tests Add 203 new targeted unit tests to boost SonarCloud coverage across frontend detectors (React, Vue, Angular, Svelte, FrontendRoute), C# detectors (EfCore, MinimalApis, Structures), auth detectors (Certificate, LDAP, SessionHeader), analyzer linkers (GuardLinker, EntityLinker, ModuleContainmentLinker, TopicLinker), cache (AnalysisCache, FileHasher), and intelligence/query (QueryPlanner, CapabilityMatrix, QueryPlan record). Co-Authored-By: Claude Sonnet 4.6 --- .../analyzer/linker/LinkersCoverageTest.java | 377 ++++++++++++ .../iq/cache/CacheCoverageTest.java | 245 ++++++++ .../auth/AuthDetectorsCoverageTest.java | 471 +++++++++++++++ .../csharp/CSharpDetectorsCoverageTest.java | 413 +++++++++++++ .../FrontendDetectorsCoverageTest.java | 570 ++++++++++++++++++ .../query/QueryIntelligenceCoverageTest.java | 295 +++++++++ 6 files changed, 2371 insertions(+) create mode 100644 src/test/java/io/github/randomcodespace/iq/analyzer/linker/LinkersCoverageTest.java create mode 100644 src/test/java/io/github/randomcodespace/iq/cache/CacheCoverageTest.java create mode 100644 src/test/java/io/github/randomcodespace/iq/detector/auth/AuthDetectorsCoverageTest.java create mode 100644 src/test/java/io/github/randomcodespace/iq/detector/csharp/CSharpDetectorsCoverageTest.java create mode 100644 src/test/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorsCoverageTest.java create mode 100644 src/test/java/io/github/randomcodespace/iq/intelligence/query/QueryIntelligenceCoverageTest.java 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/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/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 = () =>