using System.Net; using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using Moq.Protected; using StellaOps.SbomService.Models; using StellaOps.SbomService.Services; using Xunit; namespace StellaOps.SbomService.Tests; public sealed class ScanJobEmitterServiceTests { [Trait("Category", "Unit")] [Fact] public async Task SubmitScanAsync_RejectsDisallowedScannerHost() { // Arrange var configuration = BuildConfiguration("http://blocked.example.com"); var httpClientFactory = new Mock(); httpClientFactory .Setup(f => f.CreateClient(It.IsAny())) .Returns(new HttpClient(new Mock().Object)); var service = new ScanJobEmitterService( httpClientFactory.Object, configuration, NullLogger.Instance, Options.Create(new ScannerHttpOptions { AllowedHosts = new List { "scanner.local" } }), new QueueGuidProvider(new[] { Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") })); var request = new ScanJobRequest( ImageReference: "library/nginx:latest", Digest: "sha256:deadbeef", Platform: "linux/amd64", Force: false, ClientRequestId: null, SourceId: "source-1", TriggerType: "manual"); // Act var result = await service.SubmitScanAsync(request); // Assert result.Success.Should().BeFalse(); result.Error.Should().Contain("allowlisted"); } [Trait("Category", "Unit")] [Fact] public async Task SubmitBatchScanAsync_UsesDeterministicRequestId() { // Arrange var configuration = BuildConfiguration("http://scanner.local"); var handler = new Mock(); HttpRequestMessage? captured = null; handler .Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) .Callback((request, _) => captured = request) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("{\"snapshot\":{\"id\":\"job-123\",\"status\":\"queued\"},\"created\":true}") }); var httpClientFactory = new Mock(); httpClientFactory .Setup(f => f.CreateClient(It.IsAny())) .Returns(new HttpClient(handler.Object)); var service = new ScanJobEmitterService( httpClientFactory.Object, configuration, NullLogger.Instance, Options.Create(new ScannerHttpOptions { AllowedHosts = new List { "scanner.local" } }), new QueueGuidProvider(new[] { Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") })); var images = new[] { new DiscoveredImage("library/nginx", "latest", "sha256:deadbeef") }; // Act var result = await service.SubmitBatchScanAsync("source-1", images); // Assert result.Submitted.Should().Be(1); captured.Should().NotBeNull(); var payload = await captured!.Content!.ReadAsStringAsync(); using var doc = JsonDocument.Parse(payload); var clientRequestId = doc.RootElement.GetProperty("clientRequestId").GetString(); clientRequestId.Should().Be("registry-source-1-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); } private static IConfiguration BuildConfiguration(string scannerUrl) { return new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["SbomService:ScannerUrl"] = scannerUrl, ["SbomService:BatchScanSize"] = "1", ["SbomService:BatchScanDelayMs"] = "0" }) .Build(); } }