Implement incident mode management service and models
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added IPackRunIncidentModeService interface for managing incident mode activation, deactivation, and status retrieval.
- Created PackRunIncidentModeService class implementing the service interface with methods for activating, deactivating, and escalating incident modes.
- Introduced incident mode status model (PackRunIncidentModeStatus) and related enums for escalation levels and activation sources.
- Developed retention policy, telemetry settings, and debug capture settings models to manage incident mode configurations.
- Implemented SLO breach notification handling to activate incident mode based on severity.
- Added in-memory store (InMemoryPackRunIncidentModeStore) for testing purposes.
- Created comprehensive unit tests for incident mode service, covering activation, deactivation, status retrieval, and SLO breach handling.
This commit is contained in:
StellaOps Bot
2025-12-06 22:33:00 +02:00
parent 4042fc2184
commit 9bd6a73926
23 changed files with 7779 additions and 12 deletions

View File

@@ -0,0 +1,325 @@
using System.Collections.Immutable;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.BuildMetadata;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.PropertyResolution;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests.Parsers;
public sealed class JavaPropertyResolverTests
{
[Fact]
public void ResolvesSimpleProperty()
{
var properties = new Dictionary<string, string>
{
["version"] = "1.0.0"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(properties);
var result = resolver.Resolve("${version}");
Assert.Equal("1.0.0", result.ResolvedValue);
Assert.True(result.IsFullyResolved);
Assert.Empty(result.UnresolvedProperties);
}
[Fact]
public void ResolvesMultipleProperties()
{
var properties = new Dictionary<string, string>
{
["groupId"] = "com.example",
["artifactId"] = "demo",
["version"] = "2.0.0"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(properties);
var result = resolver.Resolve("${groupId}:${artifactId}:${version}");
Assert.Equal("com.example:demo:2.0.0", result.ResolvedValue);
Assert.True(result.IsFullyResolved);
}
[Fact]
public void ResolvesNestedProperties()
{
var properties = new Dictionary<string, string>
{
["slf4j.version"] = "2.0.7",
["logging.version"] = "${slf4j.version}"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(properties);
var result = resolver.Resolve("${logging.version}");
Assert.Equal("2.0.7", result.ResolvedValue);
Assert.True(result.IsFullyResolved);
}
[Fact]
public void HandlesCircularReference()
{
// Circular: a → b → a (should stop at max depth)
var properties = new Dictionary<string, string>
{
["a"] = "${b}",
["b"] = "${a}"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(properties);
var result = resolver.Resolve("${a}");
// Should stop recursing and return whatever state it reaches
Assert.False(result.IsFullyResolved);
}
[Fact]
public void HandlesMaxRecursionDepth()
{
// Create a chain of 15 nested properties (exceeds max depth of 10)
var properties = new Dictionary<string, string>();
for (int i = 0; i < 15; i++)
{
properties[$"prop{i}"] = $"${{prop{i + 1}}}";
}
properties["prop15"] = "final-value";
var resolver = new JavaPropertyResolver(properties.ToImmutableDictionary());
var result = resolver.Resolve("${prop0}");
// Should not reach final-value due to depth limit
Assert.Contains("${", result.ResolvedValue);
}
[Fact]
public void PreservesUnresolvedPlaceholder()
{
var properties = new Dictionary<string, string>
{
["known"] = "value"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(properties);
var result = resolver.Resolve("${unknown}");
Assert.Equal("${unknown}", result.ResolvedValue);
Assert.False(result.IsFullyResolved);
Assert.Single(result.UnresolvedProperties);
Assert.Contains("unknown", result.UnresolvedProperties);
}
[Fact]
public void ResolvesMavenStandardProperties()
{
var resolver = new JavaPropertyResolver();
var basedir = resolver.Resolve("${project.basedir}");
Assert.Equal(".", basedir.ResolvedValue);
var buildDir = resolver.Resolve("${project.build.directory}");
Assert.Equal("target", buildDir.ResolvedValue);
var sourceDir = resolver.Resolve("${project.build.sourceDirectory}");
Assert.Equal("src/main/java", sourceDir.ResolvedValue);
}
[Fact]
public void HandlesEmptyInput()
{
var resolver = new JavaPropertyResolver();
var nullResult = resolver.Resolve(null);
Assert.Equal(PropertyResolutionResult.Empty, nullResult);
var emptyResult = resolver.Resolve(string.Empty);
Assert.Equal(PropertyResolutionResult.Empty, emptyResult);
}
[Fact]
public void HandlesInputWithoutPlaceholders()
{
var resolver = new JavaPropertyResolver();
var result = resolver.Resolve("plain-text-value");
Assert.Equal("plain-text-value", result.ResolvedValue);
Assert.True(result.IsFullyResolved);
Assert.Empty(result.UnresolvedProperties);
}
[Fact]
public void ResolvesFromParentChain()
{
var childProps = new Dictionary<string, string>
{
["child.version"] = "1.0.0"
}.ToImmutableDictionary();
var parentProps = new Dictionary<string, string>
{
["parent.version"] = "2.0.0",
["shared.version"] = "parent-value"
}.ToImmutableDictionary();
var grandparentProps = new Dictionary<string, string>
{
["grandparent.version"] = "3.0.0"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(childProps, [parentProps, grandparentProps]);
// Child property
Assert.Equal("1.0.0", resolver.Resolve("${child.version}").ResolvedValue);
// Parent property
Assert.Equal("2.0.0", resolver.Resolve("${parent.version}").ResolvedValue);
// Grandparent property
Assert.Equal("3.0.0", resolver.Resolve("${grandparent.version}").ResolvedValue);
}
[Fact]
public void ChildPropertyOverridesParent()
{
var childProps = new Dictionary<string, string>
{
["version"] = "child-value"
}.ToImmutableDictionary();
var parentProps = new Dictionary<string, string>
{
["version"] = "parent-value"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(childProps, [parentProps]);
var result = resolver.Resolve("${version}");
Assert.Equal("child-value", result.ResolvedValue);
}
[Fact]
public void ResolvesProjectCoordinateProperties()
{
var builder = new JavaPropertyBuilder()
.AddProjectCoordinates("com.example", "demo", "1.0.0");
var resolver = new JavaPropertyResolver(builder.Build());
Assert.Equal("com.example", resolver.Resolve("${project.groupId}").ResolvedValue);
Assert.Equal("com.example", resolver.Resolve("${groupId}").ResolvedValue);
Assert.Equal("demo", resolver.Resolve("${project.artifactId}").ResolvedValue);
Assert.Equal("1.0.0", resolver.Resolve("${project.version}").ResolvedValue);
}
[Fact]
public void ResolvesDependencyVersion()
{
var properties = new Dictionary<string, string>
{
["slf4j.version"] = "2.0.7"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(properties);
var dependency = new JavaDependencyDeclaration
{
GroupId = "org.slf4j",
ArtifactId = "slf4j-api",
Version = "${slf4j.version}",
Source = "pom.xml"
};
var resolved = resolver.ResolveDependency(dependency);
Assert.Equal("2.0.7", resolved.Version);
Assert.Equal(JavaVersionSource.Property, resolved.VersionSource);
Assert.Equal("slf4j.version", resolved.VersionProperty);
}
[Fact]
public void ResolvesDependencyWithUnresolvedVersion()
{
var resolver = new JavaPropertyResolver();
var dependency = new JavaDependencyDeclaration
{
GroupId = "org.unknown",
ArtifactId = "unknown",
Version = "${unknown.version}",
Source = "pom.xml"
};
var resolved = resolver.ResolveDependency(dependency);
Assert.Equal("${unknown.version}", resolved.Version);
Assert.Equal(JavaVersionSource.Unresolved, resolved.VersionSource);
}
[Fact]
public void PropertyBuilderAddRange()
{
var existing = new Dictionary<string, string>
{
["existing"] = "value1",
["override"] = "original"
};
var builder = new JavaPropertyBuilder()
.Add("override", "new-value") // Added first
.AddRange(existing); // Won't override
var props = builder.Build();
Assert.Equal("new-value", props["override"]);
Assert.Equal("value1", props["existing"]);
}
[Fact]
public void PropertyBuilderAddParentCoordinates()
{
var parent = new JavaParentReference
{
GroupId = "org.springframework.boot",
ArtifactId = "spring-boot-starter-parent",
Version = "3.1.0"
};
var builder = new JavaPropertyBuilder().AddParentCoordinates(parent);
var props = builder.Build();
Assert.Equal("org.springframework.boot", props["project.parent.groupId"]);
Assert.Equal("spring-boot-starter-parent", props["project.parent.artifactId"]);
Assert.Equal("3.1.0", props["project.parent.version"]);
}
[Fact]
public void ResolvesComplexMavenExpression()
{
var properties = new Dictionary<string, string>
{
["spring.version"] = "6.0.0",
["project.version"] = "1.0.0-SNAPSHOT"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(properties);
var result = resolver.Resolve("spring-${spring.version}-app-${project.version}");
Assert.Equal("spring-6.0.0-app-1.0.0-SNAPSHOT", result.ResolvedValue);
Assert.True(result.IsFullyResolved);
}
[Fact]
public void HandlesMixedResolvedAndUnresolved()
{
var properties = new Dictionary<string, string>
{
["known"] = "resolved"
}.ToImmutableDictionary();
var resolver = new JavaPropertyResolver(properties);
var result = resolver.Resolve("${known}-${unknown}");
Assert.Equal("resolved-${unknown}", result.ResolvedValue);
Assert.False(result.IsFullyResolved);
Assert.Single(result.UnresolvedProperties);
Assert.Contains("unknown", result.UnresolvedProperties);
}
}

View File

@@ -0,0 +1,547 @@
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.BuildMetadata;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Maven;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests.Parsers;
public sealed class MavenEffectivePomBuilderTests
{
[Fact]
public async Task BuildsEffectivePomWithParentPropertiesAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Parent with properties
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<java.version>17</java.version>
<guava.version>31.1-jre</guava.version>
</properties>
</project>
""", cancellationToken);
// Child using parent properties
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var builder = new MavenEffectivePomBuilder(root);
var result = await builder.BuildAsync(childPom, cancellationToken);
Assert.True(result.IsFullyResolved);
Assert.Equal("17", result.EffectiveProperties["java.version"]);
Assert.Single(result.ResolvedDependencies);
var dep = result.ResolvedDependencies[0];
Assert.Equal("31.1-jre", dep.Version);
Assert.Equal(JavaVersionSource.Property, dep.VersionSource);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task MergesParentDependencyManagementAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Parent with dependencyManagement
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
""", cancellationToken);
// Child with version-less dependencies
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var builder = new MavenEffectivePomBuilder(root);
var result = await builder.BuildAsync(childPom, cancellationToken);
Assert.Equal(2, result.ResolvedDependencies.Length);
var slf4j = result.ResolvedDependencies.First(d => d.ArtifactId == "slf4j-api");
Assert.Equal("2.0.7", slf4j.Version);
var junit = result.ResolvedDependencies.First(d => d.ArtifactId == "junit");
Assert.Equal("4.13.2", junit.Version);
Assert.Equal("test", junit.Scope);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ChildDependencyManagementOverridesParentAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Parent with version
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
""", cancellationToken);
// Child overriding version
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var builder = new MavenEffectivePomBuilder(root);
var result = await builder.BuildAsync(childPom, cancellationToken);
var dep = result.ResolvedDependencies.First(d => d.ArtifactId == "slf4j-api");
Assert.Equal("2.0.9", dep.Version); // Child's version wins
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task HandlesStandalonePomAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var pomPath = Path.Combine(root, "pom.xml");
await File.WriteAllTextAsync(pomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>standalone</artifactId>
<version>1.0.0</version>
<properties>
<encoding>UTF-8</encoding>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
</project>
""", cancellationToken);
var pom = await MavenPomParser.ParseAsync(pomPath, cancellationToken);
var builder = new MavenEffectivePomBuilder(root);
var result = await builder.BuildAsync(pom, cancellationToken);
Assert.True(result.IsFullyResolved);
Assert.Single(result.ParentChain); // Only the POM itself
Assert.Equal("UTF-8", result.EffectiveProperties["encoding"]);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ResolvesPropertyInDependencyManagementVersionAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var pomPath = Path.Combine(root, "pom.xml");
await File.WriteAllTextAsync(pomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>1.0.0</version>
<properties>
<commons.version>3.12.0</commons.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>
""", cancellationToken);
var pom = await MavenPomParser.ParseAsync(pomPath, cancellationToken);
var builder = new MavenEffectivePomBuilder(root);
var result = await builder.BuildAsync(pom, cancellationToken);
var dep = result.ResolvedDependencies.First(d => d.ArtifactId == "commons-lang3");
Assert.Equal("3.12.0", dep.Version);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task TracksVersionSourceCorrectlyAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Parent with dependencyManagement
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
""", cancellationToken);
// Child with various version sources
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
<properties>
<guava.version>31.1-jre</guava.version>
</properties>
<dependencies>
<!-- Direct version -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<!-- Property version -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Dependency management version -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var builder = new MavenEffectivePomBuilder(root);
var result = await builder.BuildAsync(childPom, cancellationToken);
var junit = result.ResolvedDependencies.First(d => d.ArtifactId == "junit");
Assert.Equal("4.13.2", junit.Version);
Assert.Equal(JavaVersionSource.Direct, junit.VersionSource);
var guava = result.ResolvedDependencies.First(d => d.ArtifactId == "guava");
Assert.Equal("31.1-jre", guava.Version);
Assert.Equal(JavaVersionSource.Property, guava.VersionSource);
Assert.Equal("guava.version", guava.VersionProperty);
var slf4j = result.ResolvedDependencies.First(d => d.ArtifactId == "slf4j-api");
Assert.Equal("2.0.7", slf4j.Version);
// From parent's dependencyManagement
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task CollectsAllLicensesAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Parent with Apache license
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<licenses>
<license>
<name>Apache License 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
</project>
""", cancellationToken);
// Child (inherits license)
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var builder = new MavenEffectivePomBuilder(root);
var result = await builder.BuildAsync(childPom, cancellationToken);
Assert.Single(result.Licenses);
Assert.Equal("Apache-2.0", result.Licenses[0].SpdxId);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task GetsUnresolvedDependenciesAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var pomPath = Path.Combine(root, "pom.xml");
await File.WriteAllTextAsync(pomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- Unresolved property -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${undefined.version}</version>
</dependency>
<!-- No version and not in dependencyManagement -->
<dependency>
<groupId>com.example</groupId>
<artifactId>missing</artifactId>
</dependency>
</dependencies>
</project>
""", cancellationToken);
var pom = await MavenPomParser.ParseAsync(pomPath, cancellationToken);
var builder = new MavenEffectivePomBuilder(root);
var result = await builder.BuildAsync(pom, cancellationToken);
var unresolved = result.GetUnresolvedDependencies().ToList();
Assert.Equal(2, unresolved.Count);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task PopulatesManagedVersionsIndexAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var pomPath = Path.Combine(root, "pom.xml");
await File.WriteAllTextAsync(pomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>1.0.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
""", cancellationToken);
var pom = await MavenPomParser.ParseAsync(pomPath, cancellationToken);
var builder = new MavenEffectivePomBuilder(root);
var result = await builder.BuildAsync(pom, cancellationToken);
Assert.Equal(2, result.ManagedVersions.Count);
Assert.True(result.ManagedVersions.ContainsKey("org.slf4j:slf4j-api"));
Assert.True(result.ManagedVersions.ContainsKey("com.google.guava:guava"));
}
finally
{
TestPaths.SafeDelete(root);
}
}
}

View File

@@ -0,0 +1,556 @@
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Maven;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests.Parsers;
public sealed class MavenParentResolverTests
{
[Fact]
public async Task ResolvesRelativePathParentAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Create parent/pom.xml
var parentDir = Path.Combine(root, "parent");
Directory.CreateDirectory(parentDir);
await File.WriteAllTextAsync(Path.Combine(parentDir, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<java.version>17</java.version>
</properties>
</project>
""", cancellationToken);
// Create child/pom.xml with relativePath to parent
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>child</artifactId>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(childPom, cancellationToken);
Assert.True(result.IsFullyResolved);
Assert.Equal(2, result.ParentChain.Length); // child + parent
Assert.Equal("17", result.EffectiveProperties["java.version"]);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ResolvesDefaultRelativePathAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Create parent pom.xml in parent directory (default ../pom.xml)
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>2.0.0</version>
<packaging>pom</packaging>
</project>
""", cancellationToken);
// Create child in subdirectory with no relativePath (defaults to ../pom.xml)
var childDir = Path.Combine(root, "module");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>2.0.0</version>
</parent>
<artifactId>module</artifactId>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(childPom, cancellationToken);
Assert.True(result.IsFullyResolved);
Assert.Equal("2.0.0", result.EffectiveVersion);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ResolvesMultiLevelParentChainAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Grandparent
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>grandparent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<grandparent.prop>gp-value</grandparent.prop>
</properties>
</project>
""", cancellationToken);
// Parent
var parentDir = Path.Combine(root, "parent");
Directory.CreateDirectory(parentDir);
await File.WriteAllTextAsync(Path.Combine(parentDir, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>grandparent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>parent</artifactId>
<properties>
<parent.prop>parent-value</parent.prop>
</properties>
</project>
""", cancellationToken);
// Child
var childDir = Path.Combine(parentDir, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(childPom, cancellationToken);
Assert.True(result.IsFullyResolved);
Assert.Equal(3, result.ParentChain.Length); // child, parent, grandparent
Assert.Equal("gp-value", result.EffectiveProperties["grandparent.prop"]);
Assert.Equal("parent-value", result.EffectiveProperties["parent.prop"]);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ReturnsUnresolvedForMissingParentAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var childPomPath = Path.Combine(root, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>missing-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>orphan</artifactId>
<version>1.0.0</version>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(childPom, cancellationToken);
Assert.False(result.IsFullyResolved);
Assert.Single(result.UnresolvedParents);
Assert.Contains("com.example:missing-parent:1.0.0", result.UnresolvedParents);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task HandlesNoParentAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var pomPath = Path.Combine(root, "pom.xml");
await File.WriteAllTextAsync(pomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>standalone</artifactId>
<version>1.0.0</version>
</project>
""", cancellationToken);
var pom = await MavenPomParser.ParseAsync(pomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(pom, cancellationToken);
Assert.True(result.IsFullyResolved);
Assert.Single(result.ParentChain); // Only the original POM
Assert.Equal("com.example", result.EffectiveGroupId);
Assert.Equal("1.0.0", result.EffectiveVersion);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task InheritsGroupIdFromParentAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Parent with groupId
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>org.parent</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
</project>
""", cancellationToken);
// Child without explicit groupId
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>org.parent</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(childPom, cancellationToken);
Assert.Equal("org.parent", result.EffectiveGroupId);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task InheritsVersionFromParentAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>2.5.0</version>
<packaging>pom</packaging>
</project>
""", cancellationToken);
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>2.5.0</version>
</parent>
<artifactId>child</artifactId>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(childPom, cancellationToken);
Assert.Equal("2.5.0", result.EffectiveVersion);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ResolvesDependencyVersionFromManagementAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Parent with dependencyManagement
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
""", cancellationToken);
// Child with dependency without version
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(childPom, cancellationToken);
Assert.Single(result.ResolvedDependencies);
var dep = result.ResolvedDependencies[0];
Assert.Equal("2.0.7", dep.Version);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ResolvesPropertyInDependencyVersionAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var pomPath = Path.Combine(root, "pom.xml");
await File.WriteAllTextAsync(pomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>1.0.0</version>
<properties>
<guava.version>31.1-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
</project>
""", cancellationToken);
var pom = await MavenPomParser.ParseAsync(pomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(pom, cancellationToken);
Assert.Single(result.ResolvedDependencies);
var dep = result.ResolvedDependencies[0];
Assert.Equal("31.1-jre", dep.Version);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task CollectsLicensesFromChainAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Parent with license
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<licenses>
<license>
<name>Apache License 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
</project>
""", cancellationToken);
// Child
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(childPom, cancellationToken);
Assert.Single(result.AllLicenses);
Assert.Equal("Apache-2.0", result.AllLicenses[0].SpdxId);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ChildPropertyOverridesParentPropertyAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
// Parent with property
await File.WriteAllTextAsync(Path.Combine(root, "pom.xml"), """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<java.version>11</java.version>
</properties>
</project>
""", cancellationToken);
// Child overriding property
var childDir = Path.Combine(root, "child");
Directory.CreateDirectory(childDir);
var childPomPath = Path.Combine(childDir, "pom.xml");
await File.WriteAllTextAsync(childPomPath, """
<?xml version="1.0" encoding="UTF-8"?>
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
<properties>
<java.version>17</java.version>
</properties>
</project>
""", cancellationToken);
var childPom = await MavenPomParser.ParseAsync(childPomPath, cancellationToken);
var resolver = new MavenParentResolver(root);
var result = await resolver.ResolveAsync(childPom, cancellationToken);
// Child property should win
Assert.Equal("17", result.EffectiveProperties["java.version"]);
}
finally
{
TestPaths.SafeDelete(root);
}
}
}