partly or unimplemented features - now implemented
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BaselineResolverTests.cs
|
||||
// Sprint: SPRINT_20260208_029_Cli_baseline_selection_logic
|
||||
// Description: Unit tests for baseline resolution service.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Services;
|
||||
|
||||
public sealed class BaselineResolverTests
|
||||
{
|
||||
private readonly IForensicSnapshotClient _forensicClient;
|
||||
private readonly StellaOpsCliOptions _options;
|
||||
private readonly ILogger<BaselineResolver> _logger;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly BaselineResolver _resolver;
|
||||
|
||||
public BaselineResolverTests()
|
||||
{
|
||||
_forensicClient = Substitute.For<IForensicSnapshotClient>();
|
||||
_options = new StellaOpsCliOptions { DefaultTenant = "test-tenant" };
|
||||
_logger = Substitute.For<ILogger<BaselineResolver>>();
|
||||
_timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
|
||||
_resolver = new BaselineResolver(
|
||||
_forensicClient,
|
||||
_options,
|
||||
_logger,
|
||||
_timeProvider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ExplicitStrategy_ReturnsProvidedDigest()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp@sha256:abc123",
|
||||
Strategy = BaselineStrategy.Explicit,
|
||||
ExplicitDigest = "sha256:cafebabe"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("sha256:cafebabe", result.Digest);
|
||||
Assert.Equal(BaselineStrategy.Explicit, result.Strategy);
|
||||
Assert.Null(result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ExplicitStrategy_WithoutDigest_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp@sha256:abc123",
|
||||
Strategy = BaselineStrategy.Explicit,
|
||||
ExplicitDigest = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Null(result.Digest);
|
||||
Assert.Contains("requires a digest", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_LastGreen_ReturnsLatestPassingSnapshot()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp",
|
||||
Strategy = BaselineStrategy.LastGreen
|
||||
};
|
||||
|
||||
var snapshot = new ForensicSnapshotDocument
|
||||
{
|
||||
SnapshotId = "snap-001",
|
||||
CaseId = "case-001",
|
||||
Tenant = "test-tenant",
|
||||
Status = ForensicSnapshotStatus.Ready,
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Tags = ["verdict:pass", "artifact:pkg%3Aoci%2Fmyapp"],
|
||||
Manifest = new ForensicSnapshotManifest
|
||||
{
|
||||
ManifestId = "manifest-001",
|
||||
Digest = "sha256:lastgreen123"
|
||||
}
|
||||
};
|
||||
|
||||
_forensicClient
|
||||
.ListSnapshotsAsync(Arg.Any<ForensicSnapshotListQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ForensicSnapshotListResponse
|
||||
{
|
||||
Snapshots = [snapshot],
|
||||
Total = 1
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("sha256:lastgreen123", result.Digest);
|
||||
Assert.Equal(BaselineStrategy.LastGreen, result.Strategy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_LastGreen_NoPassingSnapshots_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp",
|
||||
Strategy = BaselineStrategy.LastGreen
|
||||
};
|
||||
|
||||
_forensicClient
|
||||
.ListSnapshotsAsync(Arg.Any<ForensicSnapshotListQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ForensicSnapshotListResponse
|
||||
{
|
||||
Snapshots = [],
|
||||
Total = 0
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Null(result.Digest);
|
||||
Assert.Contains("No passing snapshot found", result.Error);
|
||||
Assert.NotNull(result.Suggestion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_PreviousRelease_ReturnsOlderRelease()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp",
|
||||
Strategy = BaselineStrategy.PreviousRelease,
|
||||
CurrentVersion = "v2.0.0"
|
||||
};
|
||||
|
||||
var v2Snapshot = new ForensicSnapshotDocument
|
||||
{
|
||||
SnapshotId = "snap-002",
|
||||
CaseId = "case-001",
|
||||
Tenant = "test-tenant",
|
||||
Status = ForensicSnapshotStatus.Ready,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Tags = ["release:true", "version:v2.0.0", "artifact:pkg%3Aoci%2Fmyapp"],
|
||||
Manifest = new ForensicSnapshotManifest
|
||||
{
|
||||
ManifestId = "manifest-002",
|
||||
Digest = "sha256:v2digest"
|
||||
}
|
||||
};
|
||||
|
||||
var v1Snapshot = new ForensicSnapshotDocument
|
||||
{
|
||||
SnapshotId = "snap-001",
|
||||
CaseId = "case-001",
|
||||
Tenant = "test-tenant",
|
||||
Status = ForensicSnapshotStatus.Ready,
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddDays(-7),
|
||||
Tags = ["release:true", "version:v1.0.0", "artifact:pkg%3Aoci%2Fmyapp"],
|
||||
Manifest = new ForensicSnapshotManifest
|
||||
{
|
||||
ManifestId = "manifest-001",
|
||||
Digest = "sha256:v1digest"
|
||||
}
|
||||
};
|
||||
|
||||
_forensicClient
|
||||
.ListSnapshotsAsync(Arg.Any<ForensicSnapshotListQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ForensicSnapshotListResponse
|
||||
{
|
||||
Snapshots = [v2Snapshot, v1Snapshot],
|
||||
Total = 2
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("sha256:v1digest", result.Digest);
|
||||
Assert.Equal(BaselineStrategy.PreviousRelease, result.Strategy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_PreviousRelease_NoReleases_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp",
|
||||
Strategy = BaselineStrategy.PreviousRelease
|
||||
};
|
||||
|
||||
_forensicClient
|
||||
.ListSnapshotsAsync(Arg.Any<ForensicSnapshotListQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ForensicSnapshotListResponse
|
||||
{
|
||||
Snapshots = [],
|
||||
Total = 0
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Null(result.Digest);
|
||||
Assert.Contains("No release snapshots found", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ClientException_ReturnsFalseWithError()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp",
|
||||
Strategy = BaselineStrategy.LastGreen
|
||||
};
|
||||
|
||||
_forensicClient
|
||||
.ListSnapshotsAsync(Arg.Any<ForensicSnapshotListQuery>(), Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new HttpRequestException("Connection refused"));
|
||||
|
||||
// Act
|
||||
var result = await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Null(result.Digest);
|
||||
Assert.Contains("Connection refused", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSuggestionsAsync_ReturnsSuggestionsFromMultipleSources()
|
||||
{
|
||||
// Arrange
|
||||
var passingSnapshot = new ForensicSnapshotDocument
|
||||
{
|
||||
SnapshotId = "snap-pass-001",
|
||||
CaseId = "case-001",
|
||||
Tenant = "test-tenant",
|
||||
Status = ForensicSnapshotStatus.Ready,
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Tags = ["verdict:pass", "version:v1.0.0", "artifact:pkg%3Aoci%2Fmyapp"],
|
||||
Manifest = new ForensicSnapshotManifest
|
||||
{
|
||||
ManifestId = "manifest-001",
|
||||
Digest = "sha256:passing123"
|
||||
}
|
||||
};
|
||||
|
||||
var releaseSnapshot = new ForensicSnapshotDocument
|
||||
{
|
||||
SnapshotId = "snap-rel-001",
|
||||
CaseId = "case-001",
|
||||
Tenant = "test-tenant",
|
||||
Status = ForensicSnapshotStatus.Ready,
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddDays(-2),
|
||||
Tags = ["release:true", "version:v0.9.0", "artifact:pkg%3Aoci%2Fmyapp"],
|
||||
Manifest = new ForensicSnapshotManifest
|
||||
{
|
||||
ManifestId = "manifest-002",
|
||||
Digest = "sha256:release123"
|
||||
}
|
||||
};
|
||||
|
||||
_forensicClient
|
||||
.ListSnapshotsAsync(Arg.Any<ForensicSnapshotListQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
new ForensicSnapshotListResponse { Snapshots = [passingSnapshot], Total = 1 },
|
||||
new ForensicSnapshotListResponse { Snapshots = [releaseSnapshot], Total = 1 });
|
||||
|
||||
// Act
|
||||
var suggestions = await _resolver.GetSuggestionsAsync("pkg:oci/myapp");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, suggestions.Count);
|
||||
Assert.Contains(suggestions, s => s.Digest == "sha256:passing123" && s.RecommendedStrategy == BaselineStrategy.LastGreen);
|
||||
Assert.Contains(suggestions, s => s.Digest == "sha256:release123" && s.RecommendedStrategy == BaselineStrategy.PreviousRelease);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_UsesDefaultTenant_WhenNotProvided()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp",
|
||||
Strategy = BaselineStrategy.LastGreen,
|
||||
TenantId = null
|
||||
};
|
||||
|
||||
_forensicClient
|
||||
.ListSnapshotsAsync(Arg.Any<ForensicSnapshotListQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ForensicSnapshotListResponse { Snapshots = [], Total = 0 });
|
||||
|
||||
// Act
|
||||
await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
await _forensicClient.Received(1).ListSnapshotsAsync(
|
||||
Arg.Is<ForensicSnapshotListQuery>(q => q.Tenant == "test-tenant"),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_UsesTenantFromRequest_WhenProvided()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp",
|
||||
Strategy = BaselineStrategy.LastGreen,
|
||||
TenantId = "custom-tenant"
|
||||
};
|
||||
|
||||
_forensicClient
|
||||
.ListSnapshotsAsync(Arg.Any<ForensicSnapshotListQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ForensicSnapshotListResponse { Snapshots = [], Total = 0 });
|
||||
|
||||
// Act
|
||||
await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
await _forensicClient.Received(1).ListSnapshotsAsync(
|
||||
Arg.Is<ForensicSnapshotListQuery>(q => q.Tenant == "custom-tenant"),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_UnknownStrategy_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BaselineResolutionRequest
|
||||
{
|
||||
ArtifactId = "pkg:oci/myapp",
|
||||
Strategy = (BaselineStrategy)99 // Invalid strategy
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _resolver.ResolveAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("Unknown baseline strategy", result.Error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user