Frontend gaps fill work. Testing fixes work. Auditing in progress.

This commit is contained in:
StellaOps Bot
2025-12-30 01:22:58 +02:00
parent 1dc4bcbf10
commit 7a5210e2aa
928 changed files with 183942 additions and 3941 deletions

View File

@@ -50,4 +50,5 @@
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslLegacyShim.cs" />
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslAutoInit.cs" />
</ItemGroup>
</Project>
</Project>

View File

@@ -43,4 +43,5 @@
Link="Fixtures\\%(RecursiveDir)%(Filename)%(Extension)"
CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -43,4 +43,5 @@
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -42,4 +42,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
</Project>

View File

@@ -49,4 +49,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
</Project>

View File

@@ -36,3 +36,5 @@
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -51,4 +51,5 @@
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslLegacyShim.cs" />
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslAutoInit.cs" />
</ItemGroup>
</Project>
</Project>

View File

@@ -42,4 +42,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
</Project>

View File

@@ -45,4 +45,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
</Project>

View File

@@ -46,4 +46,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
</Project>

View File

@@ -50,4 +50,5 @@
<Compile Remove="Fixtures\**\*.cs" />
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -26,4 +26,5 @@
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -26,4 +26,5 @@
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -26,4 +26,5 @@
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -28,4 +28,5 @@
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -26,4 +26,5 @@
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -26,4 +26,5 @@
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -26,4 +26,5 @@
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -119,9 +119,9 @@ public sealed class LayerCacheRoundTripTests : IAsyncLifetime
(await _fileCas.TryGetAsync(casHash, CancellationToken.None)).Should().BeNull();
}
public Task InitializeAsync() => Task.CompletedTask;
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
public Task DisposeAsync()
public ValueTask DisposeAsync()
{
try
{
@@ -135,9 +135,12 @@ public sealed class LayerCacheRoundTripTests : IAsyncLifetime
// Ignored best effort cleanup.
}
return Task.CompletedTask;
return ValueTask.CompletedTask;
}
private static MemoryStream CreateStream(string content)
=> new(Encoding.UTF8.GetBytes(content));
}

View File

@@ -597,3 +597,7 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
#endregion
}

View File

@@ -20,4 +20,5 @@
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Scanner.CallGraph\\StellaOps.Scanner.CallGraph.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -123,3 +123,7 @@ public class ValkeyCallGraphCacheServiceTests : IAsyncLifetime
}
}

View File

@@ -11,7 +11,6 @@ using System.Text;
using System.Text.Json;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Scanner.Core.Tests.Perf;

View File

@@ -1,4 +1,4 @@
using System.Collections.Immutable;
using System.Collections.Immutable;
using System.IO;
using System.IO.Compression;
using System.Text;
@@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.EntryTrace.Diagnostics;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

View File

@@ -18,13 +18,13 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
{
private DirectoryInfo _tempDir = null!;
public Task InitializeAsync()
public ValueTask InitializeAsync()
{
_tempDir = Directory.CreateTempSubdirectory("attesting-writer-test-");
return Task.CompletedTask;
return ValueTask.CompletedTask;
}
public Task DisposeAsync()
public ValueTask DisposeAsync()
{
try
{
@@ -37,7 +37,7 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
{
// Ignore cleanup errors
}
return Task.CompletedTask;
return ValueTask.CompletedTask;
}
[Trait("Category", TestCategories.Unit)]
@@ -308,3 +308,6 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
=> $"blake3:{ComputeHashHex(data)}";
}
}

View File

@@ -10,7 +10,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Reachability.Cache;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Scanner.Reachability.Tests.Benchmarks;

View File

@@ -11,7 +11,6 @@ using StellaOps.Scanner.Reachability.Cache;
using StellaOps.Scanner.Reachability.Ordering;
using StellaOps.Scanner.Reachability.Subgraph;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Scanner.Reachability.Tests.Perf;

View File

@@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="FsCheck" />
<PackageReference Include="FsCheck.Xunit" />
<PackageReference Include="FsCheck.Xunit.v3" />
<PackageReference Include="JsonSchema.Net" />
<PackageReference Include="Moq" />
</ItemGroup>
@@ -19,4 +19,4 @@
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -24,4 +24,5 @@
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Scanner.CallGraph\\StellaOps.Scanner.CallGraph.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -9,7 +9,6 @@ using System.Diagnostics;
using System.Text.Json;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Scanner.SmartDiffTests.Benchmarks;

View File

@@ -13,7 +13,7 @@
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="FsCheck" />
<PackageReference Include="FsCheck.Xunit" />
<PackageReference Include="FsCheck.Xunit.v3" />
<PackageReference Include="JsonSchema.Net" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
@@ -29,4 +29,6 @@
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>

View File

@@ -13,10 +13,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="coverlet.collector" />
</ItemGroup>
</Project>

View File

@@ -30,7 +30,7 @@ public sealed class VerdictE2ETests : IAsyncLifetime
private string _registryHost = string.Empty;
private HttpClient? _httpClient;
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
_registryContainer = new ContainerBuilder()
.WithImage("registry:2")
@@ -47,7 +47,7 @@ public sealed class VerdictE2ETests : IAsyncLifetime
_httpClient = new HttpClient();
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
_httpClient?.Dispose();
if (_registryContainer is not null)
@@ -441,3 +441,6 @@ public sealed class VerdictE2ETests : IAsyncLifetime
public string? GraphRevisionId { get; init; }
}
}

View File

@@ -26,7 +26,7 @@ public sealed class VerdictOciPublisherIntegrationTests : IAsyncLifetime
private string _registryHost = string.Empty;
private HttpClient? _httpClient;
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
// Start a local OCI Distribution registry container
_registryContainer = new ContainerBuilder()
@@ -44,7 +44,7 @@ public sealed class VerdictOciPublisherIntegrationTests : IAsyncLifetime
_httpClient = new HttpClient();
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
_httpClient?.Dispose();
if (_registryContainer is not null)
@@ -377,3 +377,6 @@ public sealed class VerdictOciPublisherIntegrationTests : IAsyncLifetime
return System.Text.Encoding.UTF8.GetBytes(envelope);
}
}

View File

@@ -25,7 +25,7 @@ public sealed class BinaryEvidenceServiceTests : IAsyncLifetime
_fixture = fixture;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
@@ -44,7 +44,7 @@ public sealed class BinaryEvidenceServiceTests : IAsyncLifetime
_service = new BinaryEvidenceService(_repository, NullLogger<BinaryEvidenceService>.Instance);
}
public Task DisposeAsync() => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
[Trait("Category", TestCategories.Unit)]
[Fact]
@@ -187,3 +187,6 @@ public sealed class BinaryEvidenceServiceTests : IAsyncLifetime
return scanId;
}
}

View File

@@ -20,7 +20,7 @@ public sealed class EpssRepositoryChangesIntegrationTests : IAsyncLifetime
_fixture = fixture;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
@@ -37,7 +37,7 @@ public sealed class EpssRepositoryChangesIntegrationTests : IAsyncLifetime
_repository = new PostgresEpssRepository(_dataSource);
}
public Task DisposeAsync() => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
[Trait("Category", TestCategories.Unit)]
[Fact]
@@ -119,3 +119,6 @@ public sealed class EpssRepositoryChangesIntegrationTests : IAsyncLifetime
}
}

View File

@@ -22,7 +22,7 @@ public sealed class EpssRepositoryIntegrationTests : IAsyncLifetime
_fixture = fixture;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
@@ -39,7 +39,7 @@ public sealed class EpssRepositoryIntegrationTests : IAsyncLifetime
_repository = new PostgresEpssRepository(_dataSource);
}
public Task DisposeAsync() => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
[Trait("Category", TestCategories.Unit)]
[Fact]
@@ -127,3 +127,6 @@ public sealed class EpssRepositoryIntegrationTests : IAsyncLifetime
public int flags { get; set; }
}
}

View File

@@ -26,7 +26,7 @@ public sealed class ScanMetricsRepositoryTests : IAsyncLifetime
_fixture = fixture;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
@@ -38,7 +38,7 @@ public sealed class ScanMetricsRepositoryTests : IAsyncLifetime
_repository = new PostgresScanMetricsRepository(_dataSource, NullLogger<PostgresScanMetricsRepository>.Instance);
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
await _dataSource.DisposeAsync();
}
@@ -271,3 +271,6 @@ public sealed class ScanMetricsRepositoryTests : IAsyncLifetime
};
}
}

View File

@@ -39,7 +39,7 @@ public sealed class ScanQueryDeterminismTests : IAsyncLifetime
_fixture = fixture;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
@@ -57,7 +57,7 @@ public sealed class ScanQueryDeterminismTests : IAsyncLifetime
_cveRepository = new PostgresObservedCveRepository(_dataSource);
}
public Task DisposeAsync() => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
[Trait("Category", TestCategories.Unit)]
[Fact]
@@ -275,3 +275,6 @@ public sealed class ScanQueryDeterminismTests : IAsyncLifetime
ScannerVersion = "1.0.0"
};
}

View File

@@ -38,7 +38,7 @@ public sealed class ScanResultIdempotencyTests : IAsyncLifetime
_fixture = fixture;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
@@ -55,7 +55,7 @@ public sealed class ScanResultIdempotencyTests : IAsyncLifetime
_manifestRepository = new PostgresScanManifestRepository(_dataSource);
}
public Task DisposeAsync() => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
[Trait("Category", TestCategories.Unit)]
[Fact]
@@ -234,3 +234,6 @@ public sealed class ScanResultIdempotencyTests : IAsyncLifetime
ScannerVersion = "1.0.0"
};
}

View File

@@ -28,7 +28,7 @@ public sealed class ScannerMigrationTests : IAsyncLifetime
{
private PostgreSqlContainer _container = null!;
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
_container = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
@@ -40,7 +40,7 @@ public sealed class ScannerMigrationTests : IAsyncLifetime
await _container.StartAsync();
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
await _container.DisposeAsync();
}
@@ -286,3 +286,6 @@ public sealed class ScannerMigrationTests : IAsyncLifetime
return reader.ReadToEnd();
}
}

View File

@@ -25,7 +25,7 @@ public class SmartDiffRepositoryIntegrationTests : IAsyncLifetime
_fixture = fixture;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
@@ -45,7 +45,7 @@ public class SmartDiffRepositoryIntegrationTests : IAsyncLifetime
logger.CreateLogger<PostgresMaterialRiskChangeRepository>());
}
public Task DisposeAsync() => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
private ScannerDataSource CreateDataSource()
{
@@ -378,3 +378,6 @@ public class SmartDiffRepositoryIntegrationTests : IAsyncLifetime
#endregion
}

View File

@@ -19,4 +19,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
</Project>

View File

@@ -19,4 +19,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
</Project>

View File

@@ -20,4 +20,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
</Project>

View File

@@ -20,4 +20,5 @@
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
</Project>

View File

@@ -20,16 +20,16 @@ public sealed class TriageQueryPerformanceTests : IAsyncLifetime
_fixture = fixture;
}
public Task InitializeAsync()
public ValueTask InitializeAsync()
{
var optionsBuilder = new DbContextOptionsBuilder<TriageDbContext>()
.UseNpgsql(_fixture.ConnectionString);
_context = new TriageDbContext(optionsBuilder.Options);
return Task.CompletedTask;
return ValueTask.CompletedTask;
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
if (_context != null)
{
@@ -229,3 +229,6 @@ public sealed class TriageQueryPerformanceTests : IAsyncLifetime
await Context.SaveChangesAsync();
}
}

View File

@@ -19,16 +19,16 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
_fixture = fixture;
}
public Task InitializeAsync()
public ValueTask InitializeAsync()
{
var optionsBuilder = new DbContextOptionsBuilder<TriageDbContext>()
.UseNpgsql(_fixture.ConnectionString);
_context = new TriageDbContext(optionsBuilder.Options);
return Task.CompletedTask;
return ValueTask.CompletedTask;
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
if (_context != null)
{
@@ -292,3 +292,6 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
Assert.Contains(indexes, i => i.Contains("purl"));
}
}

View File

@@ -233,6 +233,348 @@ public sealed class OfflineKitEndpointsTests
Assert.Equal("accepted", entity.Result);
}
#region Sprint 026: OFFLINE-009 - Manifest and Validate Endpoint Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitManifest_WhenNoBundle_ReturnsNoContent()
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
using var response = await client.GetAsync("/api/offline-kit/manifest");
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitManifest_AfterImport_ReturnsManifest()
{
using var contentRoot = new TempDirectory();
var bundleBytes = Encoding.UTF8.GetBytes("deterministic-offline-kit-bundle");
var bundleSha = ComputeSha256Hex(bundleBytes);
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
config["Scanner:OfflineKit:RequireDsse"] = "false";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
// Import a bundle first
var metadataJson = JsonSerializer.Serialize(new
{
bundleId = "manifest-test-bundle",
bundleSha256 = $"sha256:{bundleSha}",
bundleSize = bundleBytes.Length,
capturedAt = DateTimeOffset.UtcNow.AddDays(-1)
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var importContent = new MultipartFormDataContent();
importContent.Add(new StringContent(metadataJson, Encoding.UTF8, "application/json"), "metadata");
var bundleContent = new ByteArrayContent(bundleBytes);
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
importContent.Add(bundleContent, "bundle", "bundle.tgz");
using var importResponse = await client.PostAsync("/api/offline-kit/import", importContent);
Assert.Equal(HttpStatusCode.Accepted, importResponse.StatusCode);
// Now fetch manifest
using var manifestResponse = await client.GetAsync("/api/offline-kit/manifest");
Assert.Equal(HttpStatusCode.OK, manifestResponse.StatusCode);
var manifestJson = await manifestResponse.Content.ReadAsStringAsync();
using var manifestDoc = JsonDocument.Parse(manifestJson);
Assert.Equal("manifest-test-bundle", manifestDoc.RootElement.GetProperty("version").GetString());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitValidate_WithValidManifest_ReturnsSuccess()
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
var manifestJson = JsonSerializer.Serialize(new
{
version = "2025.01.15",
assets = new
{
feeds = new Dictionary<string, string>
{
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
}
},
createdAt = DateTimeOffset.UtcNow.AddDays(-2)
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
var requestJson = JsonSerializer.Serialize(new
{
manifestJson,
verifyAssets = false
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var response = await client.PostAsync("/api/offline-kit/validate",
new StringContent(requestJson, Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var resultJson = await response.Content.ReadAsStringAsync();
using var resultDoc = JsonDocument.Parse(resultJson);
Assert.True(resultDoc.RootElement.GetProperty("valid").GetBoolean());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitValidate_WithInvalidManifest_ReturnsErrors()
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
var invalidManifestJson = JsonSerializer.Serialize(new
{
version = "", // Empty version - validation error
assets = new Dictionary<string, string>() // Empty assets - validation error
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
var requestJson = JsonSerializer.Serialize(new
{
manifestJson = invalidManifestJson,
verifyAssets = false
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var response = await client.PostAsync("/api/offline-kit/validate",
new StringContent(requestJson, Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var resultJson = await response.Content.ReadAsStringAsync();
using var resultDoc = JsonDocument.Parse(resultJson);
Assert.False(resultDoc.RootElement.GetProperty("valid").GetBoolean());
Assert.True(resultDoc.RootElement.GetProperty("errors").GetArrayLength() > 0);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitValidate_WithExpiredManifest_ReturnsWarning()
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
var expiredManifestJson = JsonSerializer.Serialize(new
{
version = "2024.01.15",
assets = new
{
feeds = new Dictionary<string, string>
{
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
}
},
createdAt = DateTimeOffset.UtcNow.AddDays(-60), // 60 days old - stale warning
expiresAt = DateTimeOffset.UtcNow.AddDays(-30) // Already expired
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
var requestJson = JsonSerializer.Serialize(new
{
manifestJson = expiredManifestJson,
verifyAssets = false
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var response = await client.PostAsync("/api/offline-kit/validate",
new StringContent(requestJson, Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var resultJson = await response.Content.ReadAsStringAsync();
using var resultDoc = JsonDocument.Parse(resultJson);
Assert.True(resultDoc.RootElement.GetProperty("warnings").GetArrayLength() > 0);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitValidate_WithSignature_ValidatesSignature()
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
var validManifestJson = JsonSerializer.Serialize(new
{
version = "2025.01.15",
assets = new
{
feeds = new Dictionary<string, string>
{
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
}
},
createdAt = DateTimeOffset.UtcNow
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
var requestJson = JsonSerializer.Serialize(new
{
manifestJson = validManifestJson,
signature = "sha256:" + Convert.ToBase64String(Encoding.UTF8.GetBytes("test-signature")),
verifyAssets = false
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var response = await client.PostAsync("/api/offline-kit/validate",
new StringContent(requestJson, Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var resultJson = await response.Content.ReadAsStringAsync();
using var resultDoc = JsonDocument.Parse(resultJson);
var signatureStatus = resultDoc.RootElement.GetProperty("signatureStatus");
Assert.True(signatureStatus.GetProperty("valid").GetBoolean());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitValidate_WhenDisabled_ReturnsNotFound()
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "false";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
var requestJson = JsonSerializer.Serialize(new
{
manifestJson = "{}",
verifyAssets = false
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var response = await client.PostAsync("/api/offline-kit/validate",
new StringContent(requestJson, Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
#endregion
#region Sprint 026: OFFLINE-012 - V1 Alias Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitV1Alias_Status_Works()
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
// Test v1 alias for status
using var response = await client.GetAsync("/api/v1/offline-kit/status");
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitV1Alias_Manifest_Works()
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
// Test v1 alias for manifest
using var response = await client.GetAsync("/api/v1/offline-kit/manifest");
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OfflineKitV1Alias_Validate_Works()
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
var validManifestJson = JsonSerializer.Serialize(new
{
version = "2025.01.15",
assets = new
{
feeds = new Dictionary<string, string>
{
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
}
},
createdAt = DateTimeOffset.UtcNow
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
var requestJson = JsonSerializer.Serialize(new
{
manifestJson = validManifestJson,
verifyAssets = false
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
// Test v1 alias for validate
using var response = await client.PostAsync("/api/v1/offline-kit/validate",
new StringContent(requestJson, Encoding.UTF8, "application/json"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var resultJson = await response.Content.ReadAsStringAsync();
using var resultDoc = JsonDocument.Parse(resultJson);
Assert.True(resultDoc.RootElement.GetProperty("valid").GetBoolean());
}
#endregion
private static string ComputeSha256Hex(byte[] bytes)
=> Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();

View File

@@ -22,7 +22,7 @@ public sealed class EpssSignalFlowIntegrationTests : IAsyncLifetime
_fixture = fixture;
}
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
@@ -50,7 +50,7 @@ public sealed class EpssSignalFlowIntegrationTests : IAsyncLifetime
await cmd.ExecuteNonQueryAsync();
}
public Task DisposeAsync() => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
[Fact]
public async Task GenerateSignalsAsync_WritesSignalsPerObservedTenant()
@@ -187,3 +187,6 @@ public sealed class EpssSignalFlowIntegrationTests : IAsyncLifetime
=> Task.FromResult(new EpssSignalPublishResult { Success = true, MessageId = "recorded" });
}
}

View File

@@ -13,7 +13,6 @@ using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.Worker.Orchestration;
using StellaOps.Signals.Storage;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Scanner.Worker.Tests.PoE;