// ----------------------------------------------------------------------------- // NodeCallGraphExtractorTests.cs // Sprint: SPRINT_3610_0003_0001_nodejs_callgraph // Description: Unit tests for Node.js/JavaScript call graph extraction. // ----------------------------------------------------------------------------- using StellaOps.Scanner.CallGraph.Node; using Xunit; namespace StellaOps.Scanner.CallGraph.Tests; public class NodeCallGraphExtractorTests { [Fact] public void BabelResultParser_ParsesValidJson() { // Arrange var json = """ { "module": "my-app", "version": "1.0.0", "nodes": [ { "id": "js:my-app/src/index.handleRequest", "package": "my-app", "name": "handleRequest", "visibility": "public" } ], "edges": [ { "from": "js:my-app/src/index.handleRequest", "to": "js:external/express.Router", "kind": "direct" } ], "entrypoints": [ { "id": "js:my-app/src/index.handleRequest", "type": "http_handler", "route": "/api/users", "method": "GET" } ] } """; // Act var result = BabelResultParser.Parse(json); // Assert Assert.Equal("my-app", result.Module); Assert.Equal("1.0.0", result.Version); Assert.Single(result.Nodes); Assert.Single(result.Edges); Assert.Single(result.Entrypoints); } [Fact] public void BabelResultParser_ParsesNodeWithPosition() { // Arrange var json = """ { "module": "test", "nodes": [ { "id": "js:test/app.main", "package": "test", "name": "main", "position": { "file": "app.js", "line": 10, "column": 5 } } ], "edges": [], "entrypoints": [] } """; // Act var result = BabelResultParser.Parse(json); // Assert Assert.Single(result.Nodes); var node = result.Nodes[0]; Assert.NotNull(node.Position); Assert.Equal("app.js", node.Position.File); Assert.Equal(10, node.Position.Line); Assert.Equal(5, node.Position.Column); } [Fact] public void BabelResultParser_ParsesEdgeWithSite() { // Arrange var json = """ { "module": "test", "nodes": [], "edges": [ { "from": "js:test/a.foo", "to": "js:test/b.bar", "kind": "callback", "site": { "file": "a.js", "line": 25 } } ], "entrypoints": [] } """; // Act var result = BabelResultParser.Parse(json); // Assert Assert.Single(result.Edges); var edge = result.Edges[0]; Assert.Equal("callback", edge.Kind); Assert.NotNull(edge.Site); Assert.Equal("a.js", edge.Site.File); Assert.Equal(25, edge.Site.Line); } [Fact] public void BabelResultParser_ThrowsOnEmptyInput() { // Arrange & Act & Assert Assert.Throws(() => BabelResultParser.Parse("")); Assert.Throws(() => BabelResultParser.Parse(null!)); } [Fact] public void BabelResultParser_ParsesNdjson() { // Arrange var ndjson = """ {"type": "progress", "percent": 50} {"type": "progress", "percent": 100} {"module": "app", "nodes": [], "edges": [], "entrypoints": []} """; // Act var result = BabelResultParser.ParseNdjson(ndjson); // Assert Assert.Equal("app", result.Module); } [Fact] public void JsEntrypointInfo_HasCorrectProperties() { // Arrange var json = """ { "module": "api", "nodes": [], "edges": [], "entrypoints": [ { "id": "js:api/routes.getUsers", "type": "http_handler", "route": "/users/:id", "method": "GET" } ] } """; // Act var result = BabelResultParser.Parse(json); // Assert Assert.Single(result.Entrypoints); var ep = result.Entrypoints[0]; Assert.Equal("js:api/routes.getUsers", ep.Id); Assert.Equal("http_handler", ep.Type); Assert.Equal("/users/:id", ep.Route); Assert.Equal("GET", ep.Method); } [Fact] public void BabelResultParser_ParsesSinks() { // Arrange var json = """ { "module": "test", "nodes": [ { "id": "js:test/handler.processRequest", "package": "test", "name": "processRequest" } ], "edges": [], "entrypoints": [], "sinks": [ { "caller": "js:test/handler.processRequest", "category": "command_injection", "method": "child_process.exec", "site": { "file": "handler.js", "line": 42, "column": 8 } } ] } """; // Act var result = BabelResultParser.Parse(json); // Assert Assert.Single(result.Sinks); var sink = result.Sinks[0]; Assert.Equal("js:test/handler.processRequest", sink.Caller); Assert.Equal("command_injection", sink.Category); Assert.Equal("child_process.exec", sink.Method); Assert.NotNull(sink.Site); Assert.Equal("handler.js", sink.Site.File); Assert.Equal(42, sink.Site.Line); } [Fact] public void BabelResultParser_ParsesMultipleSinkCategories() { // Arrange var json = """ { "module": "vulnerable-app", "nodes": [], "edges": [], "entrypoints": [], "sinks": [ { "caller": "js:vulnerable-app/db.query", "category": "sql_injection", "method": "connection.query" }, { "caller": "js:vulnerable-app/api.fetch", "category": "ssrf", "method": "fetch" }, { "caller": "js:vulnerable-app/file.write", "category": "file_write", "method": "fs.writeFileSync" } ] } """; // Act var result = BabelResultParser.Parse(json); // Assert Assert.Equal(3, result.Sinks.Count); Assert.Contains(result.Sinks, s => s.Category == "sql_injection"); Assert.Contains(result.Sinks, s => s.Category == "ssrf"); Assert.Contains(result.Sinks, s => s.Category == "file_write"); } [Fact] public void BabelResultParser_ParsesEmptySinks() { // Arrange var json = """ { "module": "safe-app", "nodes": [], "edges": [], "entrypoints": [], "sinks": [] } """; // Act var result = BabelResultParser.Parse(json); // Assert Assert.Empty(result.Sinks); } [Fact] public void BabelResultParser_ParsesMissingSinks() { // Arrange - sinks field omitted entirely var json = """ { "module": "legacy-app", "nodes": [], "edges": [], "entrypoints": [] } """; // Act var result = BabelResultParser.Parse(json); // Assert - should default to empty list Assert.Empty(result.Sinks); } [Fact] public void BabelResultParser_ParsesSinkWithoutSite() { // Arrange var json = """ { "module": "test", "nodes": [], "edges": [], "entrypoints": [], "sinks": [ { "caller": "js:test/func", "category": "deserialization", "method": "eval" } ] } """; // Act var result = BabelResultParser.Parse(json); // Assert Assert.Single(result.Sinks); Assert.Null(result.Sinks[0].Site); } }