Add tests and implement StubBearer authentication for Signer endpoints
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints. - Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication. - Developed ConcelierExporterClient for managing Trivy DB settings and export operations. - Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering. - Implemented styles and HTML structure for Trivy DB settings page. - Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
This commit is contained in:
		| @@ -951,6 +951,15 @@ public sealed class CommandHandlersTests | ||||
|  | ||||
|         public Task<RuntimePolicyEvaluationResult> EvaluateRuntimePolicyAsync(RuntimePolicyEvaluationRequest request, CancellationToken cancellationToken) | ||||
|             => Task.FromResult(RuntimePolicyResult); | ||||
|  | ||||
|         public Task<OfflineKitDownloadResult> DownloadOfflineKitAsync(string? bundleId, string destinationDirectory, bool overwrite, bool resume, CancellationToken cancellationToken) | ||||
|             => throw new NotSupportedException(); | ||||
|  | ||||
|         public Task<OfflineKitImportResult> ImportOfflineKitAsync(OfflineKitImportRequest request, CancellationToken cancellationToken) | ||||
|             => throw new NotSupportedException(); | ||||
|  | ||||
|         public Task<OfflineKitStatus> GetOfflineKitStatusAsync(CancellationToken cancellationToken) | ||||
|             => throw new NotSupportedException(); | ||||
|     } | ||||
|  | ||||
|     private sealed class StubExecutor : IScannerExecutor | ||||
|   | ||||
| @@ -1,24 +1,25 @@ | ||||
| using System; | ||||
| using System.Collections.ObjectModel; | ||||
| using System.Globalization; | ||||
| using System.IO; | ||||
| using System.Net; | ||||
| using System.Net.Http; | ||||
| using System.Net.Http.Json; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using StellaOps.Auth.Abstractions; | ||||
| using StellaOps.Auth.Client; | ||||
| using StellaOps.Cli.Configuration; | ||||
| using StellaOps.Cli.Services; | ||||
| using StellaOps.Cli.Services.Models; | ||||
| using StellaOps.Cli.Services.Models.Transport; | ||||
| using StellaOps.Cli.Tests.Testing; | ||||
| using System.IO; | ||||
| using System.Net; | ||||
| using System.Net.Http; | ||||
| using System.Net.Http.Json; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using StellaOps.Auth.Abstractions; | ||||
| using StellaOps.Auth.Client; | ||||
| using StellaOps.Cli.Configuration; | ||||
| using StellaOps.Cli.Services; | ||||
| using StellaOps.Cli.Services.Models; | ||||
| using StellaOps.Cli.Services.Models.Transport; | ||||
| using StellaOps.Cli.Tests.Testing; | ||||
| using System.Linq; | ||||
|  | ||||
| namespace StellaOps.Cli.Tests.Services; | ||||
|  | ||||
| @@ -481,7 +482,352 @@ public sealed class BackendOperationsClientTests | ||||
|         Assert.Equal("manual-override", Assert.IsType<string>(secondary.AdditionalProperties["quietedBy"])); | ||||
|     } | ||||
|  | ||||
|     private sealed class StubTokenClient : IStellaOpsTokenClient | ||||
|     [Fact] | ||||
|     public async Task DownloadOfflineKitAsync_DownloadsBundleAndWritesMetadata() | ||||
|     { | ||||
|         using var temp = new TempDirectory(); | ||||
|  | ||||
|         var bundleBytes = Encoding.UTF8.GetBytes("bundle-data"); | ||||
|         var manifestBytes = Encoding.UTF8.GetBytes("{\"artifacts\":[]}"); | ||||
|         var bundleDigest = Convert.ToHexString(SHA256.HashData(bundleBytes)).ToLowerInvariant(); | ||||
|         var manifestDigest = Convert.ToHexString(SHA256.HashData(manifestBytes)).ToLowerInvariant(); | ||||
|  | ||||
|         var metadataPayload = JsonSerializer.Serialize(new | ||||
|         { | ||||
|             bundleId = "2025-10-20-full", | ||||
|             bundleName = "stella-ops-offline-kit-2025-10-20.tgz", | ||||
|             bundleSha256 = $"sha256:{bundleDigest}", | ||||
|             bundleSize = (long)bundleBytes.Length, | ||||
|             bundleUrl = "https://mirror.example/stella-ops-offline-kit-2025-10-20.tgz", | ||||
|             bundleSignatureName = "stella-ops-offline-kit-2025-10-20.tgz.sig", | ||||
|             bundleSignatureUrl = "https://mirror.example/stella-ops-offline-kit-2025-10-20.tgz.sig", | ||||
|             manifestName = "offline-manifest-2025-10-20.json", | ||||
|             manifestSha256 = $"sha256:{manifestDigest}", | ||||
|             manifestUrl = "https://mirror.example/offline-manifest-2025-10-20.json", | ||||
|             manifestSignatureName = "offline-manifest-2025-10-20.json.jws", | ||||
|             manifestSignatureUrl = "https://mirror.example/offline-manifest-2025-10-20.json.jws", | ||||
|             capturedAt = DateTimeOffset.UtcNow | ||||
|         }, new JsonSerializerOptions(JsonSerializerDefaults.Web)); | ||||
|  | ||||
|         var handler = new StubHttpMessageHandler( | ||||
|             (request, _) => | ||||
|             { | ||||
|                 Assert.Equal("https://backend.example/api/offline-kit/bundles/latest", request.RequestUri!.ToString()); | ||||
|                 return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                 { | ||||
|                     Content = new StringContent(metadataPayload) | ||||
|                 }; | ||||
|             }, | ||||
|             (request, _) => | ||||
|             { | ||||
|                 var absolute = request.RequestUri!.AbsoluteUri; | ||||
|                 if (absolute.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) | ||||
|                 { | ||||
|                     return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                     { | ||||
|                         Content = new ByteArrayContent(bundleBytes) | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 if (absolute.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) | ||||
|                 { | ||||
|                     return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                     { | ||||
|                         Content = new ByteArrayContent(manifestBytes) | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                 { | ||||
|                     Content = new ByteArrayContent(Array.Empty<byte>()) | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|         var httpClient = new HttpClient(handler) | ||||
|         { | ||||
|             BaseAddress = new Uri("https://backend.example") | ||||
|         }; | ||||
|  | ||||
|         var options = new StellaOpsCliOptions | ||||
|         { | ||||
|             BackendUrl = "https://backend.example", | ||||
|             Offline = new StellaOpsCliOfflineOptions | ||||
|             { | ||||
|                 KitsDirectory = temp.Path | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)); | ||||
|         var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>()); | ||||
|  | ||||
|         var result = await client.DownloadOfflineKitAsync(null, temp.Path, overwrite: false, resume: false, CancellationToken.None); | ||||
|  | ||||
|         Assert.False(result.FromCache); | ||||
|         Assert.True(File.Exists(result.BundlePath)); | ||||
|         Assert.True(File.Exists(result.ManifestPath)); | ||||
|         Assert.NotNull(result.BundleSignaturePath); | ||||
|         Assert.NotNull(result.ManifestSignaturePath); | ||||
|         Assert.True(File.Exists(result.MetadataPath)); | ||||
|  | ||||
|         using var metadata = JsonDocument.Parse(File.ReadAllText(result.MetadataPath)); | ||||
|         Assert.Equal("2025-10-20-full", metadata.RootElement.GetProperty("bundleId").GetString()); | ||||
|         Assert.Equal(bundleDigest, metadata.RootElement.GetProperty("bundleSha256").GetString()); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task DownloadOfflineKitAsync_ResumesPartialDownload() | ||||
|     { | ||||
|         using var temp = new TempDirectory(); | ||||
|  | ||||
|         var bundleBytes = Encoding.UTF8.GetBytes("partial-download-data"); | ||||
|         var manifestBytes = Encoding.UTF8.GetBytes("{\"manifest\":true}"); | ||||
|         var bundleDigest = Convert.ToHexString(SHA256.HashData(bundleBytes)).ToLowerInvariant(); | ||||
|         var manifestDigest = Convert.ToHexString(SHA256.HashData(manifestBytes)).ToLowerInvariant(); | ||||
|  | ||||
|         var metadataJson = JsonSerializer.Serialize(new | ||||
|         { | ||||
|             bundleId = "2025-10-21-full", | ||||
|             bundleName = "kit.tgz", | ||||
|             bundleSha256 = bundleDigest, | ||||
|             bundleSize = (long)bundleBytes.Length, | ||||
|             bundleUrl = "https://mirror.example/kit.tgz", | ||||
|             manifestName = "offline-manifest.json", | ||||
|             manifestSha256 = manifestDigest, | ||||
|             manifestUrl = "https://mirror.example/offline-manifest.json", | ||||
|             capturedAt = DateTimeOffset.UtcNow | ||||
|         }, new JsonSerializerOptions(JsonSerializerDefaults.Web)); | ||||
|  | ||||
|         var partialPath = Path.Combine(temp.Path, "kit.tgz.partial"); | ||||
|         await File.WriteAllBytesAsync(partialPath, bundleBytes.AsSpan(0, bundleBytes.Length / 2).ToArray()); | ||||
|  | ||||
|         var handler = new StubHttpMessageHandler( | ||||
|             (request, _) => new HttpResponseMessage(HttpStatusCode.OK) | ||||
|             { | ||||
|                 Content = new StringContent(metadataJson) | ||||
|             }, | ||||
|             (request, _) => | ||||
|             { | ||||
|                 if (request.RequestUri!.AbsoluteUri.EndsWith("kit.tgz", StringComparison.OrdinalIgnoreCase)) | ||||
|                 { | ||||
|                     Assert.NotNull(request.Headers.Range); | ||||
|                     Assert.Equal(bundleBytes.Length / 2, request.Headers.Range!.Ranges.Single().From); | ||||
|                     return new HttpResponseMessage(HttpStatusCode.PartialContent) | ||||
|                     { | ||||
|                         Content = new ByteArrayContent(bundleBytes.AsSpan(bundleBytes.Length / 2).ToArray()) | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                 { | ||||
|                     Content = new ByteArrayContent(manifestBytes) | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|         var httpClient = new HttpClient(handler) | ||||
|         { | ||||
|             BaseAddress = new Uri("https://backend.example") | ||||
|         }; | ||||
|  | ||||
|         var options = new StellaOpsCliOptions | ||||
|         { | ||||
|             BackendUrl = "https://backend.example", | ||||
|             Offline = new StellaOpsCliOfflineOptions | ||||
|             { | ||||
|                 KitsDirectory = temp.Path | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)); | ||||
|         var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>()); | ||||
|  | ||||
|         var result = await client.DownloadOfflineKitAsync(null, temp.Path, overwrite: false, resume: true, CancellationToken.None); | ||||
|  | ||||
|         Assert.Equal(bundleDigest, result.Descriptor.BundleSha256); | ||||
|         Assert.Equal(bundleBytes.Length, new FileInfo(result.BundlePath).Length); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task ImportOfflineKitAsync_SendsMultipartPayload() | ||||
|     { | ||||
|         using var temp = new TempDirectory(); | ||||
|         var bundlePath = Path.Combine(temp.Path, "kit.tgz"); | ||||
|         var manifestPath = Path.Combine(temp.Path, "offline-manifest.json"); | ||||
|  | ||||
|         var bundleBytes = Encoding.UTF8.GetBytes("bundle-content"); | ||||
|         var manifestBytes = Encoding.UTF8.GetBytes("{\"manifest\":true}"); | ||||
|         await File.WriteAllBytesAsync(bundlePath, bundleBytes); | ||||
|         await File.WriteAllBytesAsync(manifestPath, manifestBytes); | ||||
|  | ||||
|         var bundleDigest = Convert.ToHexString(SHA256.HashData(bundleBytes)).ToLowerInvariant(); | ||||
|         var manifestDigest = Convert.ToHexString(SHA256.HashData(manifestBytes)).ToLowerInvariant(); | ||||
|  | ||||
|         var metadata = new OfflineKitMetadataDocument | ||||
|         { | ||||
|             BundleId = "2025-10-21-full", | ||||
|             BundleName = "kit.tgz", | ||||
|             BundleSha256 = bundleDigest, | ||||
|             BundleSize = bundleBytes.Length, | ||||
|             BundlePath = bundlePath, | ||||
|             CapturedAt = DateTimeOffset.UtcNow, | ||||
|             DownloadedAt = DateTimeOffset.UtcNow, | ||||
|             Channel = "stable", | ||||
|             Kind = "full", | ||||
|             ManifestName = "offline-manifest.json", | ||||
|             ManifestSha256 = manifestDigest, | ||||
|             ManifestSize = manifestBytes.Length, | ||||
|             ManifestPath = manifestPath, | ||||
|             IsDelta = false, | ||||
|             BaseBundleId = null | ||||
|         }; | ||||
|  | ||||
|         await File.WriteAllTextAsync(bundlePath + ".metadata.json", JsonSerializer.Serialize(metadata, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true })); | ||||
|  | ||||
|         var recordingHandler = new ImportRecordingHandler(); | ||||
|         var httpClient = new HttpClient(recordingHandler) | ||||
|         { | ||||
|             BaseAddress = new Uri("https://backend.example") | ||||
|         }; | ||||
|  | ||||
|         var options = new StellaOpsCliOptions | ||||
|         { | ||||
|             BackendUrl = "https://backend.example" | ||||
|         }; | ||||
|  | ||||
|         var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)); | ||||
|         var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>()); | ||||
|  | ||||
|         var request = new OfflineKitImportRequest( | ||||
|             bundlePath, | ||||
|             manifestPath, | ||||
|             null, | ||||
|             null, | ||||
|             metadata.BundleId, | ||||
|             metadata.BundleSha256, | ||||
|             metadata.BundleSize, | ||||
|             metadata.CapturedAt, | ||||
|             metadata.Channel, | ||||
|             metadata.Kind, | ||||
|             metadata.IsDelta, | ||||
|             metadata.BaseBundleId, | ||||
|             metadata.ManifestSha256, | ||||
|             metadata.ManifestSize); | ||||
|  | ||||
|         var result = await client.ImportOfflineKitAsync(request, CancellationToken.None); | ||||
|  | ||||
|         Assert.Equal("imp-1", result.ImportId); | ||||
|         Assert.NotNull(recordingHandler.MetadataJson); | ||||
|         Assert.NotNull(recordingHandler.BundlePayload); | ||||
|         Assert.NotNull(recordingHandler.ManifestPayload); | ||||
|  | ||||
|         using var metadataJson = JsonDocument.Parse(recordingHandler.MetadataJson!); | ||||
|         Assert.Equal(bundleDigest, metadataJson.RootElement.GetProperty("bundleSha256").GetString()); | ||||
|         Assert.Equal(manifestDigest, metadataJson.RootElement.GetProperty("manifestSha256").GetString()); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task GetOfflineKitStatusAsync_ParsesResponse() | ||||
|     { | ||||
|         var captured = DateTimeOffset.UtcNow; | ||||
|         var imported = captured.AddMinutes(5); | ||||
|  | ||||
|         var statusJson = JsonSerializer.Serialize(new | ||||
|         { | ||||
|             current = new | ||||
|             { | ||||
|                 bundleId = "2025-10-22-full", | ||||
|                 channel = "stable", | ||||
|                 kind = "full", | ||||
|                 isDelta = false, | ||||
|                 baseBundleId = (string?)null, | ||||
|                 bundleSha256 = "sha256:abc123", | ||||
|                 bundleSize = 42, | ||||
|                 capturedAt = captured, | ||||
|                 importedAt = imported | ||||
|             }, | ||||
|             components = new[] | ||||
|             { | ||||
|                 new | ||||
|                 { | ||||
|                     name = "concelier-json", | ||||
|                     version = "2025-10-22", | ||||
|                     digest = "sha256:def456", | ||||
|                     capturedAt = captured, | ||||
|                     sizeBytes = 1234 | ||||
|                 } | ||||
|             } | ||||
|         }, new JsonSerializerOptions(JsonSerializerDefaults.Web)); | ||||
|  | ||||
|         var handler = new StubHttpMessageHandler( | ||||
|             (request, _) => | ||||
|             { | ||||
|                 Assert.Equal("https://backend.example/api/offline-kit/status", request.RequestUri!.ToString()); | ||||
|                 return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|                 { | ||||
|                     Content = new StringContent(statusJson) | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|         var httpClient = new HttpClient(handler) | ||||
|         { | ||||
|             BaseAddress = new Uri("https://backend.example") | ||||
|         }; | ||||
|  | ||||
|         var options = new StellaOpsCliOptions | ||||
|         { | ||||
|             BackendUrl = "https://backend.example" | ||||
|         }; | ||||
|  | ||||
|         var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)); | ||||
|         var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>()); | ||||
|  | ||||
|         var status = await client.GetOfflineKitStatusAsync(CancellationToken.None); | ||||
|  | ||||
|         Assert.Equal("2025-10-22-full", status.BundleId); | ||||
|         Assert.Equal("stable", status.Channel); | ||||
|         Assert.Equal("full", status.Kind); | ||||
|         Assert.False(status.IsDelta); | ||||
|         Assert.Equal(42, status.BundleSize); | ||||
|         Assert.Single(status.Components); | ||||
|         Assert.Equal("concelier-json", status.Components[0].Name); | ||||
|     } | ||||
|  | ||||
|     private sealed class ImportRecordingHandler : HttpMessageHandler | ||||
|     { | ||||
|         public string? MetadataJson { get; private set; } | ||||
|         public byte[]? BundlePayload { get; private set; } | ||||
|         public byte[]? ManifestPayload { get; private set; } | ||||
|  | ||||
|         protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||||
|         { | ||||
|             if (request.RequestUri!.AbsoluteUri.EndsWith("/api/offline-kit/import", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 Assert.IsType<MultipartFormDataContent>(request.Content); | ||||
|                 foreach (var part in (MultipartFormDataContent)request.Content!) | ||||
|                 { | ||||
|                     var name = part.Headers.ContentDisposition?.Name?.Trim('"'); | ||||
|                     switch (name) | ||||
|                     { | ||||
|                         case "metadata": | ||||
|                             MetadataJson = await part.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); | ||||
|                             break; | ||||
|                         case "bundle": | ||||
|                             BundlePayload = await part.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); | ||||
|                             break; | ||||
|                         case "manifest": | ||||
|                             ManifestPayload = await part.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); | ||||
|                             break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return new HttpResponseMessage(HttpStatusCode.OK) | ||||
|             { | ||||
|                 Content = new StringContent("{\"importId\":\"imp-1\",\"status\":\"queued\",\"submittedAt\":\"2025-10-21T00:00:00Z\"}") | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private sealed class StubTokenClient : IStellaOpsTokenClient | ||||
|     { | ||||
|         private readonly StellaOpsTokenResult _tokenResult; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user