// ----------------------------------------------------------------------------- // PythonCallGraphExtractorTests.cs // Sprint: SPRINT_3610_0004_0001_python_callgraph // Description: Unit tests for Python call graph extraction. // ----------------------------------------------------------------------------- using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Scanner.CallGraph.Python; using StellaOps.Scanner.Reachability; using Xunit; using StellaOps.TestKit; namespace StellaOps.Scanner.CallGraph.Tests; public class PythonCallGraphExtractorTests { [Trait("Category", TestCategories.Unit)] [Fact] public void PythonEntrypointClassifier_ClassifiesFlaskRoute() { // Arrange var classifier = new PythonEntrypointClassifier(); var node = new CallGraphNode( NodeId: "py:myapp/views.get_users", Symbol: "get_users", File: "views.py", Line: 10, Package: "myapp", Visibility: Visibility.Public, IsEntrypoint: false, EntrypointType: null, IsSink: false, SinkCategory: null); var decorators = new[] { "@app.route('/users')", "@login_required" }; // Act var result = classifier.Classify(node, decorators); // Assert Assert.True(result.HasValue); Assert.Equal(EntrypointType.HttpHandler, result.Value); } [Trait("Category", TestCategories.Unit)] [Fact] public void PythonEntrypointClassifier_ClassifiesFastApiRoute() { // Arrange var classifier = new PythonEntrypointClassifier(); var node = new CallGraphNode( NodeId: "py:api/endpoints.create_user", Symbol: "create_user", File: "endpoints.py", Line: 25, Package: "api", Visibility: Visibility.Public, IsEntrypoint: false, EntrypointType: null, IsSink: false, SinkCategory: null); var decorators = new[] { "@router.post('/users')" }; // Act var result = classifier.Classify(node, decorators); // Assert Assert.True(result.HasValue); Assert.Equal(EntrypointType.HttpHandler, result.Value); } [Trait("Category", TestCategories.Unit)] [Fact] public void PythonEntrypointClassifier_ClassifiesCeleryTask() { // Arrange var classifier = new PythonEntrypointClassifier(); var node = new CallGraphNode( NodeId: "py:tasks/email.send_notification", Symbol: "send_notification", File: "email.py", Line: 15, Package: "tasks", Visibility: Visibility.Public, IsEntrypoint: false, EntrypointType: null, IsSink: false, SinkCategory: null); var decorators = new[] { "@app.task(bind=True)" }; // Act var result = classifier.Classify(node, decorators); // Assert Assert.True(result.HasValue); Assert.Equal(EntrypointType.BackgroundJob, result.Value); } [Trait("Category", TestCategories.Unit)] [Fact] public void PythonEntrypointClassifier_ClassifiesClickCommand() { // Arrange var classifier = new PythonEntrypointClassifier(); var node = new CallGraphNode( NodeId: "py:cli/commands.deploy", Symbol: "deploy", File: "commands.py", Line: 50, Package: "cli", Visibility: Visibility.Public, IsEntrypoint: false, EntrypointType: null, IsSink: false, SinkCategory: null); var decorators = new[] { "@cli.command()" }; // Act var result = classifier.Classify(node, decorators); // Assert Assert.True(result.HasValue); Assert.Equal(EntrypointType.CliCommand, result.Value); } [Trait("Category", TestCategories.Unit)] [Fact] public void PythonSinkMatcher_MatchesSubprocessCall() { // Arrange var matcher = new PythonSinkMatcher(); // Act var result = matcher.Match("subprocess", "call"); // Assert Assert.Equal(SinkCategory.CmdExec, result); } [Trait("Category", TestCategories.Unit)] [Fact] public void PythonSinkMatcher_MatchesEval() { // Arrange var matcher = new PythonSinkMatcher(); // Act var result = matcher.Match("builtins", "eval"); // Assert Assert.Equal(SinkCategory.CodeInjection, result); } [Trait("Category", TestCategories.Unit)] [Fact] public void PythonSinkMatcher_MatchesPickleLoads() { // Arrange var matcher = new PythonSinkMatcher(); // Act var result = matcher.Match("pickle", "loads"); // Assert Assert.Equal(SinkCategory.UnsafeDeser, result); } [Trait("Category", TestCategories.Unit)] [Fact] public void PythonSinkMatcher_MatchesSqlAlchemyExecute() { // Arrange var matcher = new PythonSinkMatcher(); // Act var result = matcher.Match("sqlalchemy.engine.Connection", "execute"); // Assert Assert.Equal(SinkCategory.SqlRaw, result); } [Trait("Category", TestCategories.Unit)] [Fact] public void PythonSinkMatcher_ReturnsNullForSafeFunction() { // Arrange var matcher = new PythonSinkMatcher(); // Act var result = matcher.Match("myapp.utils", "format_string"); // Assert Assert.Null(result); } }