audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration

This commit is contained in:
master
2026-01-14 10:48:00 +02:00
parent d7be6ba34b
commit 95d5898650
379 changed files with 40695 additions and 19041 deletions

View File

@@ -0,0 +1,188 @@
using StellaOps.Scanner.Contracts;
using System.Text.Json;
using Xunit;
namespace StellaOps.Scanner.Contracts.Tests;
/// <summary>
/// Tests for Visibility enum.
/// </summary>
public sealed class VisibilityTests
{
[Theory]
[InlineData(Visibility.Public)]
[InlineData(Visibility.Internal)]
[InlineData(Visibility.Protected)]
[InlineData(Visibility.Private)]
public void Visibility_AllValues_AreDefined(Visibility visibility)
{
Assert.True(Enum.IsDefined(visibility));
}
[Fact]
public void Visibility_AllValues_AreCounted()
{
var values = Enum.GetValues<Visibility>();
Assert.Equal(4, values.Length);
}
[Fact]
public void Visibility_JsonSerialization_UsesStringValue()
{
var json = JsonSerializer.Serialize(Visibility.Public);
Assert.Equal("\"Public\"", json);
}
}
/// <summary>
/// Tests for CallKind enum.
/// </summary>
public sealed class CallKindTests
{
[Theory]
[InlineData(CallKind.Direct)]
[InlineData(CallKind.Virtual)]
[InlineData(CallKind.Delegate)]
[InlineData(CallKind.Reflection)]
[InlineData(CallKind.Dynamic)]
[InlineData(CallKind.Plt)]
[InlineData(CallKind.Iat)]
public void CallKind_AllValues_AreDefined(CallKind kind)
{
Assert.True(Enum.IsDefined(kind));
}
[Fact]
public void CallKind_AllValues_AreCounted()
{
var values = Enum.GetValues<CallKind>();
Assert.Equal(7, values.Length);
}
[Fact]
public void CallKind_JsonSerialization_UsesStringValue()
{
var json = JsonSerializer.Serialize(CallKind.Direct);
Assert.Equal("\"Direct\"", json);
}
}
/// <summary>
/// Tests for EntrypointType enum.
/// </summary>
public sealed class EntrypointTypeTests
{
[Theory]
[InlineData(EntrypointType.HttpHandler)]
[InlineData(EntrypointType.GrpcMethod)]
[InlineData(EntrypointType.CliCommand)]
[InlineData(EntrypointType.BackgroundJob)]
[InlineData(EntrypointType.ScheduledJob)]
[InlineData(EntrypointType.MessageHandler)]
[InlineData(EntrypointType.EventSubscriber)]
[InlineData(EntrypointType.WebSocketHandler)]
[InlineData(EntrypointType.EventHandler)]
[InlineData(EntrypointType.Lambda)]
[InlineData(EntrypointType.Unknown)]
public void EntrypointType_AllValues_AreDefined(EntrypointType type)
{
Assert.True(Enum.IsDefined(type));
}
[Fact]
public void EntrypointType_AllValues_AreCounted()
{
var values = Enum.GetValues<EntrypointType>();
Assert.Equal(11, values.Length);
}
}
/// <summary>
/// Tests for SinkCategory enum.
/// </summary>
public sealed class SinkCategoryTests
{
[Theory]
[InlineData(SinkCategory.CmdExec)]
[InlineData(SinkCategory.UnsafeDeser)]
[InlineData(SinkCategory.SqlRaw)]
[InlineData(SinkCategory.SqlInjection)]
[InlineData(SinkCategory.Ssrf)]
[InlineData(SinkCategory.FileWrite)]
[InlineData(SinkCategory.PathTraversal)]
[InlineData(SinkCategory.TemplateInjection)]
[InlineData(SinkCategory.CryptoWeak)]
[InlineData(SinkCategory.AuthzBypass)]
[InlineData(SinkCategory.LdapInjection)]
[InlineData(SinkCategory.XPathInjection)]
[InlineData(SinkCategory.XxeInjection)]
[InlineData(SinkCategory.CodeInjection)]
[InlineData(SinkCategory.LogInjection)]
[InlineData(SinkCategory.Reflection)]
public void SinkCategory_KnownValues_AreDefined(SinkCategory category)
{
Assert.True(Enum.IsDefined(category));
}
[Fact]
public void SinkCategory_JsonSerialization_UsesJsonAttribute()
{
// SinkCategory has JsonStringEnumMemberName attributes
var json = JsonSerializer.Serialize(SinkCategory.CmdExec);
Assert.Equal("\"CMD_EXEC\"", json);
}
[Fact]
public void SinkCategory_JsonDeserialization_FromJsonAttribute()
{
var category = JsonSerializer.Deserialize<SinkCategory>("\"SQL_INJECTION\"");
Assert.Equal(SinkCategory.SqlInjection, category);
}
[Theory]
[InlineData("CMD_EXEC", SinkCategory.CmdExec)]
[InlineData("UNSAFE_DESER", SinkCategory.UnsafeDeser)]
[InlineData("SQL_RAW", SinkCategory.SqlRaw)]
[InlineData("SSRF", SinkCategory.Ssrf)]
[InlineData("FILE_WRITE", SinkCategory.FileWrite)]
[InlineData("PATH_TRAVERSAL", SinkCategory.PathTraversal)]
[InlineData("CRYPTO_WEAK", SinkCategory.CryptoWeak)]
[InlineData("AUTHZ_BYPASS", SinkCategory.AuthzBypass)]
[InlineData("XXE", SinkCategory.XxeInjection)]
[InlineData("CODE_INJECTION", SinkCategory.CodeInjection)]
public void SinkCategory_JsonRoundTrip_PreservesValue(string jsonValue, SinkCategory expected)
{
var json = $"\"{jsonValue}\"";
var deserialized = JsonSerializer.Deserialize<SinkCategory>(json);
Assert.Equal(expected, deserialized);
var serialized = JsonSerializer.Serialize(expected);
Assert.Equal(json, serialized);
}
}
/// <summary>
/// Tests for CallEdgeExplanationType enum.
/// </summary>
public sealed class CallEdgeExplanationTypeTests
{
[Theory]
[InlineData(CallEdgeExplanationType.Import)]
[InlineData(CallEdgeExplanationType.DynamicLoad)]
[InlineData(CallEdgeExplanationType.Reflection)]
[InlineData(CallEdgeExplanationType.Ffi)]
[InlineData(CallEdgeExplanationType.EnvGuard)]
[InlineData(CallEdgeExplanationType.FeatureFlag)]
[InlineData(CallEdgeExplanationType.PlatformArch)]
public void CallEdgeExplanationType_KnownValues_AreDefined(CallEdgeExplanationType type)
{
Assert.True(Enum.IsDefined(type));
}
[Fact]
public void CallEdgeExplanationType_JsonSerialization_UsesStringValue()
{
var json = JsonSerializer.Serialize(CallEdgeExplanationType.Import);
Assert.Equal("\"Import\"", json);
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<UseXunitV3>true</UseXunitV3>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Contracts\StellaOps.Scanner.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": -1
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<UseXunitV3>true</UseXunitV3>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.ProofIntegration\StellaOps.Scanner.ProofIntegration.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,145 @@
using StellaOps.Scanner.ProofIntegration;
using Xunit;
namespace StellaOps.Scanner.ProofIntegration.Tests;
/// <summary>
/// Tests for VulnerabilityFinding record.
/// </summary>
public sealed class VulnerabilityFindingTests
{
[Fact]
public void VulnerabilityFinding_RequiredProperties_MustBeSet()
{
var finding = new VulnerabilityFinding
{
CveId = "CVE-2026-12345",
PackagePurl = "pkg:npm/lodash@4.17.20",
PackageName = "lodash",
PackageVersion = "4.17.20",
Severity = "HIGH"
};
Assert.Equal("CVE-2026-12345", finding.CveId);
Assert.Equal("pkg:npm/lodash@4.17.20", finding.PackagePurl);
Assert.Equal("lodash", finding.PackageName);
Assert.Equal("4.17.20", finding.PackageVersion);
Assert.Equal("HIGH", finding.Severity);
}
[Theory]
[InlineData("LOW")]
[InlineData("MEDIUM")]
[InlineData("HIGH")]
[InlineData("CRITICAL")]
public void VulnerabilityFinding_SeverityLevels_AreAccepted(string severity)
{
var finding = new VulnerabilityFinding
{
CveId = "CVE-2026-00001",
PackagePurl = "pkg:pypi/requests@2.28.0",
PackageName = "requests",
PackageVersion = "2.28.0",
Severity = severity
};
Assert.Equal(severity, finding.Severity);
}
[Fact]
public void VulnerabilityFinding_WithDifferentEcosystems_WorksCorrectly()
{
// npm
var npmFinding = new VulnerabilityFinding
{
CveId = "CVE-2026-NPM01",
PackagePurl = "pkg:npm/@angular/core@17.0.0",
PackageName = "@angular/core",
PackageVersion = "17.0.0",
Severity = "MEDIUM"
};
Assert.Contains("npm", npmFinding.PackagePurl);
// pypi
var pypiFinding = new VulnerabilityFinding
{
CveId = "CVE-2026-PYPI01",
PackagePurl = "pkg:pypi/django@4.2.0",
PackageName = "django",
PackageVersion = "4.2.0",
Severity = "HIGH"
};
Assert.Contains("pypi", pypiFinding.PackagePurl);
// nuget
var nugetFinding = new VulnerabilityFinding
{
CveId = "CVE-2026-NUGET01",
PackagePurl = "pkg:nuget/Newtonsoft.Json@13.0.3",
PackageName = "Newtonsoft.Json",
PackageVersion = "13.0.3",
Severity = "LOW"
};
Assert.Contains("nuget", nugetFinding.PackagePurl);
// golang
var goFinding = new VulnerabilityFinding
{
CveId = "CVE-2026-GO01",
PackagePurl = "pkg:golang/github.com/gin-gonic/gin@1.9.0",
PackageName = "github.com/gin-gonic/gin",
PackageVersion = "1.9.0",
Severity = "CRITICAL"
};
Assert.Contains("golang", goFinding.PackagePurl);
}
[Fact]
public void VulnerabilityFinding_RecordEquality_WorksCorrectly()
{
var finding1 = new VulnerabilityFinding
{
CveId = "CVE-2026-00001",
PackagePurl = "pkg:npm/lodash@4.17.20",
PackageName = "lodash",
PackageVersion = "4.17.20",
Severity = "HIGH"
};
var finding2 = new VulnerabilityFinding
{
CveId = "CVE-2026-00001",
PackagePurl = "pkg:npm/lodash@4.17.20",
PackageName = "lodash",
PackageVersion = "4.17.20",
Severity = "HIGH"
};
Assert.Equal(finding1, finding2);
Assert.Equal(finding1.GetHashCode(), finding2.GetHashCode());
}
[Fact]
public void VulnerabilityFinding_DifferentCve_NotEqual()
{
var finding1 = new VulnerabilityFinding
{
CveId = "CVE-2026-00001",
PackagePurl = "pkg:npm/lodash@4.17.20",
PackageName = "lodash",
PackageVersion = "4.17.20",
Severity = "HIGH"
};
var finding2 = new VulnerabilityFinding
{
CveId = "CVE-2026-00002",
PackagePurl = "pkg:npm/lodash@4.17.20",
PackageName = "lodash",
PackageVersion = "4.17.20",
Severity = "HIGH"
};
Assert.NotEqual(finding1, finding2);
}
}

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": -1
}

View File

@@ -207,10 +207,17 @@ public class SourceConfigValidatorTests
// Arrange
var config = JsonDocument.Parse("""
{
"acceptedFormats": ["CycloneDX", "SPDX"],
"validationRules": {
"requireSignature": false,
"maxFileSizeBytes": 10485760
"allowedTools": ["stella-cli"],
"validation": {
"requireSignedSbom": false,
"maxSbomSizeBytes": 10485760,
"allowedFormats": ["CycloneDxJson", "SpdxJson"]
},
"attribution": {
"requireBuildId": false,
"requireRepository": false,
"requireCommitSha": false,
"requirePipelineId": false
}
}
""");
@@ -228,7 +235,16 @@ public class SourceConfigValidatorTests
// Arrange
var config = JsonDocument.Parse("""
{
"acceptedFormats": ["InvalidFormat"]
"allowedTools": ["stella-cli"],
"validation": {
"allowedFormats": ["InvalidFormat"]
},
"attribution": {
"requireBuildId": false,
"requireRepository": false,
"requireCommitSha": false,
"requirePipelineId": false
}
}
""");
@@ -241,7 +257,7 @@ public class SourceConfigValidatorTests
}
[Fact]
public void Validate_CliConfig_Empty_ReturnsWarning()
public void Validate_CliConfig_Empty_ReturnsFailure()
{
// Arrange
var config = JsonDocument.Parse("{}");
@@ -250,8 +266,8 @@ public class SourceConfigValidatorTests
var result = _validator.Validate(SbomSourceType.Cli, config);
// Assert
result.IsValid.Should().BeTrue();
result.Warnings.Should().Contain(w => w.Contains("validation rules"));
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("allowedTools"));
}
#endregion
@@ -267,8 +283,16 @@ public class SourceConfigValidatorTests
"repositoryUrl": "https://github.com/example/repo",
"provider": "GitHub",
"authMethod": "Token",
"branchConfig": {
"defaultBranch": "main"
"branches": {
"include": ["main"]
},
"triggers": {
"onPush": true,
"onPullRequest": false,
"onTag": false
},
"scanOptions": {
"analyzers": ["nuget"]
}
}
""");
@@ -288,7 +312,18 @@ public class SourceConfigValidatorTests
{
"repositoryUrl": "git@github.com:example/repo.git",
"provider": "GitHub",
"authMethod": "SshKey"
"authMethod": "Ssh",
"branches": {
"include": ["main"]
},
"triggers": {
"onPush": false,
"onPullRequest": false,
"onTag": true
},
"scanOptions": {
"analyzers": ["nuget"]
}
}
""");
@@ -305,7 +340,16 @@ public class SourceConfigValidatorTests
// Arrange
var config = JsonDocument.Parse("""
{
"provider": "GitHub"
"provider": "GitHub",
"branches": {
"include": ["main"]
},
"triggers": {
"onPush": true
},
"scanOptions": {
"analyzers": ["nuget"]
}
}
""");
@@ -324,7 +368,16 @@ public class SourceConfigValidatorTests
var config = JsonDocument.Parse("""
{
"repositoryUrl": "https://github.com/example/repo",
"provider": "InvalidProvider"
"provider": "InvalidProvider",
"branches": {
"include": ["main"]
},
"triggers": {
"onPush": true
},
"scanOptions": {
"analyzers": ["nuget"]
}
}
""");
@@ -337,13 +390,19 @@ public class SourceConfigValidatorTests
}
[Fact]
public void Validate_GitConfig_NoBranchConfig_ReturnsWarning()
public void Validate_GitConfig_MissingBranches_ReturnsFailure()
{
// Arrange
var config = JsonDocument.Parse("""
{
"repositoryUrl": "https://github.com/example/repo",
"provider": "GitHub"
"provider": "GitHub",
"triggers": {
"onPush": true
},
"scanOptions": {
"analyzers": ["nuget"]
}
}
""");
@@ -351,8 +410,8 @@ public class SourceConfigValidatorTests
var result = _validator.Validate(SbomSourceType.Git, config);
// Assert
result.IsValid.Should().BeTrue();
result.Warnings.Should().Contain(w => w.Contains("branch configuration"));
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("branches"));
}
#endregion

View File

@@ -5,6 +5,6 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0769-M | DONE | Revalidated 2026-01-07 (test project). |
| AUDIT-0769-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0769-A | DONE | Waived (test project; revalidated 2026-01-07). |
| AUDIT-0738-M | DONE | Revalidated 2026-01-12 (test project). |
| AUDIT-0738-T | DONE | Revalidated 2026-01-12. |
| AUDIT-0738-A | DONE | Applied 2026-01-14. |

View File

@@ -0,0 +1,288 @@
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Determinism;
using StellaOps.Scanner.Sources.Configuration;
using StellaOps.Scanner.Sources.Contracts;
using StellaOps.Scanner.Sources.Domain;
using StellaOps.Scanner.Sources.Handlers;
using StellaOps.Scanner.Sources.Persistence;
using StellaOps.Scanner.Sources.Triggers;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Scanner.Sources.Tests.Triggers;
public sealed class SourceTriggerDispatcherTests
{
private static readonly FakeTimeProvider TimeProvider = new(
new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
private static readonly JsonDocument MinimalConfig = JsonDocument.Parse("{}");
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task DispatchAsync_QueuesTargetsAndCompletesRun()
{
var guidProvider = new SequentialGuidProvider();
var source = CreateSource(guidProvider);
source.Activate("tester", TimeProvider);
var sourceRepo = new InMemorySourceRepository();
sourceRepo.Add(source);
var runRepo = new InMemoryRunRepository();
var handler = new InMemoryHandler(new[]
{
ScanTarget.Image("registry.example.com/app:1.0.0"),
ScanTarget.Image("registry.example.com/app:1.1.0")
});
var queue = new InMemoryScanJobQueue(guidProvider);
var dispatcher = new SourceTriggerDispatcher(
sourceRepo,
runRepo,
new[] { handler },
queue,
NullLogger<SourceTriggerDispatcher>.Instance,
TimeProvider,
guidProvider);
var result = await dispatcher.DispatchAsync(
source.SourceId,
SbomSourceRunTrigger.Manual,
"manual",
TestContext.Current.CancellationToken);
result.Success.Should().BeTrue();
result.JobsQueued.Should().Be(2);
result.Run.ItemsDiscovered.Should().Be(2);
result.Run.ItemsSucceeded.Should().Be(2);
result.Run.Status.Should().Be(SbomSourceRunStatus.Succeeded);
queue.Requests.Should().HaveCount(2);
runRepo.Runs.Should().ContainKey(result.Run.RunId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task ProcessScheduledSourcesAsync_DispatchesDueSources()
{
var guidProvider = new SequentialGuidProvider();
var source = CreateSource(guidProvider);
source.Activate("tester", TimeProvider);
var sourceRepo = new InMemorySourceRepository
{
DueSources = new List<SbomSource> { source }
};
sourceRepo.Add(source);
var runRepo = new InMemoryRunRepository();
var handler = new InMemoryHandler(new[] { ScanTarget.Image("registry.example.com/app:1.0.0") });
var queue = new InMemoryScanJobQueue(guidProvider);
var dispatcher = new SourceTriggerDispatcher(
sourceRepo,
runRepo,
new[] { handler },
queue,
NullLogger<SourceTriggerDispatcher>.Instance,
TimeProvider,
guidProvider);
var processed = await dispatcher.ProcessScheduledSourcesAsync(
TestContext.Current.CancellationToken);
processed.Should().Be(1);
runRepo.Runs.Should().HaveCount(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task DispatchAsync_DisabledSource_ReturnsFailedRun()
{
var guidProvider = new SequentialGuidProvider();
var source = CreateSource(guidProvider);
source.Disable("tester", TimeProvider);
var sourceRepo = new InMemorySourceRepository();
sourceRepo.Add(source);
var runRepo = new InMemoryRunRepository();
var handler = new InMemoryHandler(Array.Empty<ScanTarget>());
var queue = new InMemoryScanJobQueue(guidProvider);
var dispatcher = new SourceTriggerDispatcher(
sourceRepo,
runRepo,
new[] { handler },
queue,
NullLogger<SourceTriggerDispatcher>.Instance,
TimeProvider,
guidProvider);
var result = await dispatcher.DispatchAsync(
source.SourceId,
SbomSourceRunTrigger.Manual,
"manual",
TestContext.Current.CancellationToken);
result.Success.Should().BeFalse();
result.Error.Should().Contain("disabled");
result.Run.Status.Should().Be(SbomSourceRunStatus.Failed);
}
private static SbomSource CreateSource(IGuidProvider guidProvider)
{
return SbomSource.Create(
tenantId: "tenant-1",
name: "source-1",
sourceType: SbomSourceType.Docker,
configuration: MinimalConfig,
createdBy: "tester",
timeProvider: TimeProvider,
guidProvider: guidProvider);
}
private sealed class InMemorySourceRepository : ISbomSourceRepository
{
public Dictionary<Guid, SbomSource> Sources { get; } = new();
public IReadOnlyList<SbomSource> DueSources { get; set; } = Array.Empty<SbomSource>();
public void Add(SbomSource source) => Sources[source.SourceId] = source;
public Task<SbomSource?> GetByIdAsync(string tenantId, Guid sourceId, CancellationToken ct = default)
=> Task.FromResult(Sources.TryGetValue(sourceId, out var source) ? source : null);
public Task<SbomSource?> GetByIdAnyTenantAsync(Guid sourceId, CancellationToken ct = default)
=> Task.FromResult(Sources.TryGetValue(sourceId, out var source) ? source : null);
public Task<SbomSource?> GetByNameAsync(string tenantId, string name, CancellationToken ct = default)
=> Task.FromResult(Sources.Values.FirstOrDefault(s => s.TenantId == tenantId && s.Name == name));
public Task<PagedResponse<SbomSource>> ListAsync(
string tenantId,
ListSourcesRequest request,
CancellationToken ct = default)
=> throw new NotSupportedException("ListAsync is not used in these tests.");
public Task<IReadOnlyList<SbomSource>> GetDueScheduledSourcesAsync(
DateTimeOffset asOf,
int limit = 100,
CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<SbomSource>>(DueSources.Take(limit).ToList());
public Task CreateAsync(SbomSource source, CancellationToken ct = default)
{
Sources[source.SourceId] = source;
return Task.CompletedTask;
}
public Task UpdateAsync(SbomSource source, CancellationToken ct = default)
{
Sources[source.SourceId] = source;
return Task.CompletedTask;
}
public Task DeleteAsync(string tenantId, Guid sourceId, CancellationToken ct = default)
{
Sources.Remove(sourceId);
return Task.CompletedTask;
}
public Task<bool> NameExistsAsync(
string tenantId,
string name,
Guid? excludeSourceId = null,
CancellationToken ct = default)
=> Task.FromResult(Sources.Values.Any(s =>
s.TenantId == tenantId
&& s.Name == name
&& s.SourceId != excludeSourceId));
public Task<IReadOnlyList<SbomSource>> SearchByNameAsync(string name, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<SbomSource>>(Sources.Values
.Where(s => s.Name.Contains(name, StringComparison.OrdinalIgnoreCase))
.ToList());
public Task<IReadOnlyList<SbomSource>> GetDueForScheduledRunAsync(CancellationToken ct = default)
=> Task.FromResult(DueSources);
}
private sealed class InMemoryRunRepository : ISbomSourceRunRepository
{
public Dictionary<Guid, SbomSourceRun> Runs { get; } = new();
public Task<SbomSourceRun?> GetByIdAsync(Guid runId, CancellationToken ct = default)
=> Task.FromResult(Runs.TryGetValue(runId, out var run) ? run : null);
public Task<PagedResponse<SbomSourceRun>> ListForSourceAsync(
Guid sourceId,
ListSourceRunsRequest request,
CancellationToken ct = default)
=> throw new NotSupportedException("ListForSourceAsync is not used in these tests.");
public Task CreateAsync(SbomSourceRun run, CancellationToken ct = default)
{
Runs[run.RunId] = run;
return Task.CompletedTask;
}
public Task UpdateAsync(SbomSourceRun run, CancellationToken ct = default)
{
Runs[run.RunId] = run;
return Task.CompletedTask;
}
public Task<IReadOnlyList<SbomSourceRun>> GetStaleRunsAsync(
TimeSpan olderThan,
int limit = 100,
CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<SbomSourceRun>>(Array.Empty<SbomSourceRun>());
public Task<SourceRunStats> GetStatsAsync(Guid sourceId, CancellationToken ct = default)
=> Task.FromResult(new SourceRunStats());
}
private sealed class InMemoryScanJobQueue : IScanJobQueue
{
private readonly IGuidProvider _guidProvider;
public List<ScanJobRequest> Requests { get; } = new();
public InMemoryScanJobQueue(IGuidProvider guidProvider)
{
_guidProvider = guidProvider;
}
public Task<Guid> EnqueueAsync(ScanJobRequest request, CancellationToken ct = default)
{
Requests.Add(request);
return Task.FromResult(_guidProvider.NewGuid());
}
}
private sealed class InMemoryHandler : ISourceTypeHandler
{
private readonly IReadOnlyList<ScanTarget> _targets;
public InMemoryHandler(IReadOnlyList<ScanTarget> targets)
{
_targets = targets;
}
public SbomSourceType SourceType => SbomSourceType.Docker;
public Task<IReadOnlyList<ScanTarget>> DiscoverTargetsAsync(
SbomSource source,
TriggerContext context,
CancellationToken ct = default)
=> Task.FromResult(_targets);
public ConfigValidationResult ValidateConfiguration(JsonDocument configuration)
=> ConfigValidationResult.Success();
public Task<ConnectionTestResult> TestConnectionAsync(SbomSource source, CancellationToken ct = default)
=> Task.FromResult(ConnectionTestResult.Succeeded(TimeProvider));
}
}