feat: Implement BerkeleyDB reader for RPM databases
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
console-runner-image / build-runner-image (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
wine-csp-build / Integration Tests (push) Has been cancelled
wine-csp-build / Security Scan (push) Has been cancelled
wine-csp-build / Generate SBOM (push) Has been cancelled
wine-csp-build / Publish Image (push) Has been cancelled
wine-csp-build / Air-Gap Bundle (push) Has been cancelled
wine-csp-build / Test Summary (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
console-runner-image / build-runner-image (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
wine-csp-build / Integration Tests (push) Has been cancelled
wine-csp-build / Security Scan (push) Has been cancelled
wine-csp-build / Generate SBOM (push) Has been cancelled
wine-csp-build / Publish Image (push) Has been cancelled
wine-csp-build / Air-Gap Bundle (push) Has been cancelled
wine-csp-build / Test Summary (push) Has been cancelled
- Added BerkeleyDbReader class to read and extract RPM header blobs from BerkeleyDB hash databases. - Implemented methods to detect BerkeleyDB format and extract values, including handling of page sizes and magic numbers. - Added tests for BerkeleyDbReader to ensure correct functionality and header extraction. feat: Add Yarn PnP data tests - Created YarnPnpDataTests to validate package resolution and data loading from Yarn PnP cache. - Implemented tests for resolved keys, package presence, and loading from cache structure. test: Add egg-info package fixtures for Python tests - Created egg-info package fixtures for testing Python analyzers. - Included PKG-INFO, entry_points.txt, and installed-files.txt for comprehensive coverage. test: Enhance RPM database reader tests - Added tests for RpmDatabaseReader to validate fallback to legacy packages when SQLite is missing. - Implemented helper methods to create legacy package files and RPM headers for testing. test: Implement dual signing tests - Added DualSignTests to validate secondary signature addition when configured. - Created stub implementations for crypto providers and key resolvers to facilitate testing. chore: Update CI script for Playwright Chromium installation - Modified ci-console-exports.sh to ensure deterministic Chromium binary installation for console exports tests. - Added checks for Windows compatibility and environment variable setups for Playwright browsers.
This commit is contained in:
@@ -16,7 +16,7 @@ using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Connector.Cccs;
|
||||
using StellaOps.Concelier.Connector.Cccs.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
@@ -19,7 +19,7 @@ using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
@@ -80,19 +80,19 @@ public sealed class CccsConnectorTests : IAsyncLifetime
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedFeedResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<CccsConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
|
||||
var mongo = provider.GetRequiredService<IMongoDatabase>();
|
||||
var docCollection = mongo.GetCollection<BsonDocument>("document");
|
||||
var documentsSnapshot = await docCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
|
||||
|
||||
System.IO.Directory.CreateDirectory(System.IO.Path.Combine(AppContext.BaseDirectory, "tmp"));
|
||||
var debugPath = System.IO.Path.Combine(AppContext.BaseDirectory, "tmp", "cccs-documents.json");
|
||||
await System.IO.File.WriteAllTextAsync(debugPath, documentsSnapshot.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { Indent = true }));
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(CccsConnectorPlugin.SourceName, "https://www.cyber.gc.ca/en/alerts-advisories/test-advisory", CancellationToken.None);
|
||||
var connector = provider.GetRequiredService<CccsConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
|
||||
var mongo = provider.GetRequiredService<IMongoDatabase>();
|
||||
var docCollection = mongo.GetCollection<BsonDocument>("document");
|
||||
var documentsSnapshot = await docCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
|
||||
|
||||
System.IO.Directory.CreateDirectory(System.IO.Path.Combine(AppContext.BaseDirectory, "tmp"));
|
||||
var debugPath = System.IO.Path.Combine(AppContext.BaseDirectory, "tmp", "cccs-documents.json");
|
||||
await System.IO.File.WriteAllTextAsync(debugPath, documentsSnapshot.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { Indent = true }));
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(CccsConnectorPlugin.SourceName, "https://www.cyber.gc.ca/en/alerts-advisories/test-advisory", CancellationToken.None);
|
||||
document.Should().NotBeNull();
|
||||
document!.Status.Should().Be(DocumentStatuses.PendingParse);
|
||||
document.Metadata.Should().ContainKey("cccs.language").WhoseValue.Should().Be("en");
|
||||
|
||||
@@ -4,7 +4,7 @@ using StellaOps.Concelier.Connector.Cccs.Internal;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Html;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Cccs.Tests.Internal;
|
||||
|
||||
@@ -12,15 +12,15 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.CertBund.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
@@ -58,27 +58,27 @@ public sealed class CertBundConnectorTests : IAsyncLifetime
|
||||
advisories.Should().HaveCount(1);
|
||||
|
||||
var advisory = advisories[0];
|
||||
advisory.AdvisoryKey.Should().Be("WID-SEC-2025-2264");
|
||||
advisory.Aliases.Should().Contain("CVE-2025-1234");
|
||||
advisory.AffectedPackages.Should().Contain(package => package.Identifier.Contains("Ivanti"));
|
||||
advisory.References.Should().Contain(reference => reference.Url == DetailUri.ToString());
|
||||
advisory.Language.Should().Be("de");
|
||||
|
||||
var endpoint = advisory.AffectedPackages.Should().ContainSingle(p => p.Identifier.Contains("Endpoint Manager") && !p.Identifier.Contains("Cloud"))
|
||||
.Subject;
|
||||
endpoint.VersionRanges.Should().ContainSingle(range =>
|
||||
range.RangeKind == NormalizedVersionSchemes.SemVer &&
|
||||
range.IntroducedVersion == "2023.1" &&
|
||||
range.FixedVersion == "2024.2");
|
||||
endpoint.NormalizedVersions.Should().ContainSingle(rule =>
|
||||
rule.Min == "2023.1" &&
|
||||
rule.Max == "2024.2" &&
|
||||
rule.Notes == "certbund:WID-SEC-2025-2264:ivanti");
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
state!.Cursor.Should().NotBeNull();
|
||||
advisory.AdvisoryKey.Should().Be("WID-SEC-2025-2264");
|
||||
advisory.Aliases.Should().Contain("CVE-2025-1234");
|
||||
advisory.AffectedPackages.Should().Contain(package => package.Identifier.Contains("Ivanti"));
|
||||
advisory.References.Should().Contain(reference => reference.Url == DetailUri.ToString());
|
||||
advisory.Language.Should().Be("de");
|
||||
|
||||
var endpoint = advisory.AffectedPackages.Should().ContainSingle(p => p.Identifier.Contains("Endpoint Manager") && !p.Identifier.Contains("Cloud"))
|
||||
.Subject;
|
||||
endpoint.VersionRanges.Should().ContainSingle(range =>
|
||||
range.RangeKind == NormalizedVersionSchemes.SemVer &&
|
||||
range.IntroducedVersion == "2023.1" &&
|
||||
range.FixedVersion == "2024.2");
|
||||
endpoint.NormalizedVersions.Should().ContainSingle(rule =>
|
||||
rule.Min == "2023.1" &&
|
||||
rule.Max == "2024.2" &&
|
||||
rule.Notes == "certbund:WID-SEC-2025-2264:ivanti");
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
state!.Cursor.Should().NotBeNull();
|
||||
state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
|
||||
pendingDocs!.AsBsonArray.Should().BeEmpty();
|
||||
state.Cursor.TryGetValue("pendingMappings", out var pendingMappings).Should().BeTrue();
|
||||
|
||||
@@ -18,7 +18,7 @@ using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
@@ -22,7 +22,7 @@ using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
@@ -3,8 +3,8 @@ using System.Globalization;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.CertCc.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal;
|
||||
|
||||
@@ -22,8 +22,8 @@ using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
@@ -67,13 +67,13 @@ public sealed class CertFrConnectorTests : IAsyncLifetime
|
||||
var snapshot = SnapshotSerializer.ToSnapshot(advisories.OrderBy(static a => a.AdvisoryKey, StringComparer.Ordinal).ToArray());
|
||||
var expected = ReadFixture("certfr-advisories.snapshot.json");
|
||||
var normalizedSnapshot = Normalize(snapshot);
|
||||
var normalizedExpected = Normalize(expected);
|
||||
if (!string.Equals(normalizedExpected, normalizedSnapshot, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "CertFr", "Fixtures", "certfr-advisories.actual.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
File.WriteAllText(actualPath, snapshot);
|
||||
}
|
||||
var normalizedExpected = Normalize(expected);
|
||||
if (!string.Equals(normalizedExpected, normalizedSnapshot, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "CertFr", "Fixtures", "certfr-advisories.actual.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
File.WriteAllText(actualPath, snapshot);
|
||||
}
|
||||
|
||||
Assert.Equal(normalizedExpected, normalizedSnapshot);
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertIn.Tests;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Mongo2Go;
|
||||
@@ -12,10 +12,10 @@ using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Core.Aoc;
|
||||
using StellaOps.Concelier.Core.Linksets;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Common.Tests;
|
||||
|
||||
@@ -23,16 +23,16 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoDbRunner _runner;
|
||||
private readonly IMongoDatabase _database;
|
||||
private readonly RawDocumentStorage _rawStorage;
|
||||
private readonly ICryptoHash _hash;
|
||||
private readonly RawDocumentStorage _rawStorage;
|
||||
private readonly ICryptoHash _hash;
|
||||
|
||||
public SourceFetchServiceGuardTests()
|
||||
{
|
||||
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
|
||||
var client = new MongoClient(_runner.ConnectionString);
|
||||
_database = client.GetDatabase($"source-fetch-guard-{Guid.NewGuid():N}");
|
||||
_rawStorage = new RawDocumentStorage(_database);
|
||||
_hash = CryptoHashFactory.CreateDefault();
|
||||
_rawStorage = new RawDocumentStorage();
|
||||
_hash = CryptoHashFactory.CreateDefault();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -55,18 +55,18 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
|
||||
var linksetMapper = new NoopAdvisoryLinksetMapper();
|
||||
|
||||
var service = new SourceFetchService(
|
||||
httpClientFactory,
|
||||
_rawStorage,
|
||||
documentStore,
|
||||
NullLogger<SourceFetchService>.Instance,
|
||||
jitter,
|
||||
guard,
|
||||
linksetMapper,
|
||||
_hash,
|
||||
TimeProvider.System,
|
||||
httpOptions,
|
||||
storageOptions);
|
||||
var service = new SourceFetchService(
|
||||
httpClientFactory,
|
||||
_rawStorage,
|
||||
documentStore,
|
||||
NullLogger<SourceFetchService>.Instance,
|
||||
jitter,
|
||||
guard,
|
||||
linksetMapper,
|
||||
_hash,
|
||||
TimeProvider.System,
|
||||
httpOptions,
|
||||
storageOptions);
|
||||
|
||||
var request = new SourceFetchRequest("client", "vndr.msrc", new Uri("https://example.test/advisories/ADV-1234"))
|
||||
{
|
||||
@@ -85,7 +85,7 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
Assert.Equal("tenant-default", guard.LastDocument!.Tenant);
|
||||
Assert.Equal("msrc", guard.LastDocument.Source.Vendor);
|
||||
Assert.Equal("ADV-1234", guard.LastDocument.Upstream.UpstreamId);
|
||||
var expectedHash = _hash.ComputeHashHex(Encoding.UTF8.GetBytes(responsePayload), HashAlgorithms.Sha256);
|
||||
var expectedHash = _hash.ComputeHashHex(Encoding.UTF8.GetBytes(responsePayload), HashAlgorithms.Sha256);
|
||||
Assert.Equal(expectedHash, guard.LastDocument.Upstream.ContentHash);
|
||||
Assert.NotNull(documentStore.LastRecord);
|
||||
Assert.True(documentStore.UpsertCount > 0);
|
||||
@@ -117,18 +117,18 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
|
||||
var linksetMapper = new NoopAdvisoryLinksetMapper();
|
||||
|
||||
var service = new SourceFetchService(
|
||||
httpClientFactory,
|
||||
_rawStorage,
|
||||
documentStore,
|
||||
NullLogger<SourceFetchService>.Instance,
|
||||
jitter,
|
||||
guard,
|
||||
linksetMapper,
|
||||
_hash,
|
||||
TimeProvider.System,
|
||||
httpOptions,
|
||||
storageOptions);
|
||||
var service = new SourceFetchService(
|
||||
httpClientFactory,
|
||||
_rawStorage,
|
||||
documentStore,
|
||||
NullLogger<SourceFetchService>.Instance,
|
||||
jitter,
|
||||
guard,
|
||||
linksetMapper,
|
||||
_hash,
|
||||
TimeProvider.System,
|
||||
httpOptions,
|
||||
storageOptions);
|
||||
|
||||
var request = new SourceFetchRequest("client", "nvd", new Uri("https://example.test/data/XYZ"))
|
||||
{
|
||||
@@ -191,21 +191,21 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
|
||||
public int UpsertCount { get; private set; }
|
||||
|
||||
public Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
UpsertCount++;
|
||||
LastRecord = record;
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
|
||||
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<DocumentRecord?>(null);
|
||||
|
||||
public Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<DocumentRecord?>(null);
|
||||
|
||||
public Task<bool> UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
=> Task.FromResult(false);
|
||||
public Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class RecordingAdvisoryRawWriteGuard : IAdvisoryRawWriteGuard
|
||||
|
||||
@@ -10,7 +10,7 @@ using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.State;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Common.Tests;
|
||||
@@ -32,7 +32,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
||||
_client = new MongoClient(_runner.ConnectionString);
|
||||
_database = _client.GetDatabase($"source-state-seed-{Guid.NewGuid():N}");
|
||||
_documentStore = new DocumentStore(_database, NullLogger<DocumentStore>.Instance);
|
||||
_rawStorage = new RawDocumentStorage(_database);
|
||||
_rawStorage = new RawDocumentStorage();
|
||||
_stateRepository = new MongoSourceStateRepository(_database, NullLogger<MongoSourceStateRepository>.Instance);
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 28, 12, 0, 0, TimeSpan.Zero));
|
||||
_hash = CryptoHashFactory.CreateDefault();
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Mongo2Go" Version="4.1.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
@@ -21,4 +20,4 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -17,8 +17,8 @@ using StellaOps.Concelier.Connector.Cve.Internal;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Cve.Tests;
|
||||
|
||||
@@ -22,8 +22,8 @@ using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.Debian.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@@ -3,7 +3,7 @@ using Xunit;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Distro.Debian;
|
||||
using StellaOps.Concelier.Connector.Distro.Debian.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Debian.Tests;
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ using StellaOps.Concelier.Connector.Distro.RedHat.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Plugin;
|
||||
using Xunit;
|
||||
|
||||
@@ -8,20 +8,20 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@@ -72,7 +72,7 @@ public sealed class SuseConnectorTests : IAsyncLifetime
|
||||
|
||||
var open = advisories.Single(a => a.AdvisoryKey == "SUSE-SU-2025:0002-1");
|
||||
var openPackage = Assert.Single(open.AffectedPackages);
|
||||
Assert.Equal(AffectedPackageStatusCatalog.UnderInvestigation, openPackage.Statuses.Single().Status);
|
||||
Assert.Equal(AffectedPackageStatusCatalog.UnderInvestigation, openPackage.Statuses.Single().Status);
|
||||
|
||||
SeedNotModifiedResponses();
|
||||
|
||||
@@ -133,10 +133,10 @@ public sealed class SuseConnectorTests : IAsyncLifetime
|
||||
_handler.AddResponse(AdvisoryOpenUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0002-1.json", "\"adv-2\""));
|
||||
}
|
||||
|
||||
private void SeedNotModifiedResponses()
|
||||
{
|
||||
_handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.NotModified, "suse-changes.csv", "\"changes-v1\""));
|
||||
}
|
||||
private void SeedNotModifiedResponses()
|
||||
{
|
||||
_handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.NotModified, "suse-changes.csv", "\"changes-v1\""));
|
||||
}
|
||||
|
||||
private HttpResponseMessage BuildResponse(HttpStatusCode statusCode, string fixture, string etag)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Suse.Tests;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Ghsa.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ghsa.Tests;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using StellaOps.Concelier.Connector.Ghsa.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ghsa.Tests;
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ using StellaOps.Concelier.Connector.Ics.Kaspersky;
|
||||
using StellaOps.Concelier.Connector.Ics.Kaspersky.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ics.Kaspersky.Tests;
|
||||
@@ -77,14 +77,14 @@ public sealed class KasperskyConnectorTests : IAsyncLifetime
|
||||
Assert.Single(advisories);
|
||||
var canonical = SnapshotSerializer.ToSnapshot(advisories.Single());
|
||||
var expected = ReadFixture("expected-advisory.json");
|
||||
var normalizedExpected = NormalizeLineEndings(expected);
|
||||
var normalizedActual = NormalizeLineEndings(canonical);
|
||||
if (!string.Equals(normalizedExpected, normalizedActual, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "Ics", "Kaspersky", "Fixtures", "expected-advisory.actual.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
File.WriteAllText(actualPath, canonical);
|
||||
}
|
||||
var normalizedExpected = NormalizeLineEndings(expected);
|
||||
var normalizedActual = NormalizeLineEndings(canonical);
|
||||
if (!string.Equals(normalizedExpected, normalizedActual, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "Ics", "Kaspersky", "Fixtures", "expected-advisory.actual.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
File.WriteAllText(actualPath, canonical);
|
||||
}
|
||||
|
||||
Assert.Equal(normalizedExpected, normalizedActual);
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ using StellaOps.Concelier.Connector.Jvn;
|
||||
using StellaOps.Concelier.Connector.Jvn.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.JpFlags;
|
||||
using Xunit.Abstractions;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
@@ -16,7 +16,7 @@ using StellaOps.Concelier.Connector.Kev;
|
||||
using StellaOps.Concelier.Connector.Kev.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ using StellaOps.Concelier.Connector.Kisa.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Nvd.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Nvd.Tests;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ using StellaOps.Concelier.Connector.Nvd;
|
||||
using StellaOps.Concelier.Connector.Nvd.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using System.Net;
|
||||
|
||||
@@ -22,8 +22,8 @@ using StellaOps.Concelier.Connector.Nvd.Configuration;
|
||||
using StellaOps.Concelier.Connector.Nvd.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.ChangeHistory;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
@@ -79,31 +79,31 @@ public sealed class NvdConnectorTests : IAsyncLifetime
|
||||
|
||||
var cve1 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0001");
|
||||
var package1 = Assert.Single(cve1.AffectedPackages);
|
||||
var range1 = Assert.Single(package1.VersionRanges);
|
||||
Assert.Equal("cpe", range1.RangeKind);
|
||||
Assert.Equal("1.0", range1.IntroducedVersion);
|
||||
Assert.Null(range1.FixedVersion);
|
||||
Assert.Equal("1.0", range1.LastAffectedVersion);
|
||||
Assert.Equal("==1.0", range1.RangeExpression);
|
||||
Assert.NotNull(range1.Primitives);
|
||||
Assert.Equal("1.0", range1.Primitives!.VendorExtensions!["version"]);
|
||||
Assert.Contains(cve1.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-79");
|
||||
var cvss1 = Assert.Single(cve1.CvssMetrics);
|
||||
Assert.Equal("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", cvss1.Provenance.Value);
|
||||
|
||||
var cve2 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0002");
|
||||
var package2 = Assert.Single(cve2.AffectedPackages);
|
||||
var range2 = Assert.Single(package2.VersionRanges);
|
||||
Assert.Equal("cpe", range2.RangeKind);
|
||||
Assert.Equal("2.0", range2.IntroducedVersion);
|
||||
Assert.Null(range2.FixedVersion);
|
||||
Assert.Equal("2.0", range2.LastAffectedVersion);
|
||||
Assert.Equal("==2.0", range2.RangeExpression);
|
||||
Assert.NotNull(range2.Primitives);
|
||||
Assert.Equal("2.0", range2.Primitives!.VendorExtensions!["version"]);
|
||||
Assert.Contains(cve2.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-89");
|
||||
var cvss2 = Assert.Single(cve2.CvssMetrics);
|
||||
Assert.Equal("CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L", cvss2.Provenance.Value);
|
||||
var range1 = Assert.Single(package1.VersionRanges);
|
||||
Assert.Equal("cpe", range1.RangeKind);
|
||||
Assert.Equal("1.0", range1.IntroducedVersion);
|
||||
Assert.Null(range1.FixedVersion);
|
||||
Assert.Equal("1.0", range1.LastAffectedVersion);
|
||||
Assert.Equal("==1.0", range1.RangeExpression);
|
||||
Assert.NotNull(range1.Primitives);
|
||||
Assert.Equal("1.0", range1.Primitives!.VendorExtensions!["version"]);
|
||||
Assert.Contains(cve1.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-79");
|
||||
var cvss1 = Assert.Single(cve1.CvssMetrics);
|
||||
Assert.Equal("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", cvss1.Provenance.Value);
|
||||
|
||||
var cve2 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0002");
|
||||
var package2 = Assert.Single(cve2.AffectedPackages);
|
||||
var range2 = Assert.Single(package2.VersionRanges);
|
||||
Assert.Equal("cpe", range2.RangeKind);
|
||||
Assert.Equal("2.0", range2.IntroducedVersion);
|
||||
Assert.Null(range2.FixedVersion);
|
||||
Assert.Equal("2.0", range2.LastAffectedVersion);
|
||||
Assert.Equal("==2.0", range2.RangeExpression);
|
||||
Assert.NotNull(range2.Primitives);
|
||||
Assert.Equal("2.0", range2.Primitives!.VendorExtensions!["version"]);
|
||||
Assert.Contains(cve2.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-89");
|
||||
var cvss2 = Assert.Single(cve2.CvssMetrics);
|
||||
Assert.Equal("CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L", cvss2.Provenance.Value);
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
|
||||
|
||||
@@ -2,8 +2,8 @@ using System.Text.Json;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Osv.Tests;
|
||||
|
||||
|
||||
@@ -5,25 +5,25 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Osv.Tests;
|
||||
|
||||
public sealed class OsvGhsaParityRegressionTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
private static readonly ICryptoHash Hash = CryptoHashFactory.CreateDefault();
|
||||
public sealed class OsvGhsaParityRegressionTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
private static readonly ICryptoHash Hash = CryptoHashFactory.CreateDefault();
|
||||
|
||||
// Curated GHSA identifiers spanning multiple ecosystems (PyPI, npm/go, Maven) for parity coverage.
|
||||
private static readonly string[] GhsaIds =
|
||||
@@ -561,7 +561,7 @@ public sealed class OsvGhsaParityRegressionTests
|
||||
|
||||
private static string ComputeSha256Hex(string payload)
|
||||
{
|
||||
var bytes = Hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payload), HashAlgorithms.Sha256);
|
||||
var bytes = Hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payload), HashAlgorithms.Sha256);
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Reflection;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Normalization.Identifiers;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using Xunit;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Reflection;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Normalization.Identifiers;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Osv.Tests;
|
||||
|
||||
public sealed class OsvMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void Map_NormalizesAliasesReferencesAndRanges()
|
||||
{
|
||||
var published = DateTimeOffset.UtcNow.AddDays(-2);
|
||||
var modified = DateTimeOffset.UtcNow.AddDays(-1);
|
||||
|
||||
public void Map_NormalizesAliasesReferencesAndRanges()
|
||||
{
|
||||
var published = DateTimeOffset.UtcNow.AddDays(-2);
|
||||
var modified = DateTimeOffset.UtcNow.AddDays(-1);
|
||||
|
||||
using var databaseSpecificJson = JsonDocument.Parse("{}");
|
||||
using var ecosystemSpecificJson = JsonDocument.Parse("{}");
|
||||
|
||||
@@ -120,121 +120,121 @@ public sealed class OsvMapperTests
|
||||
Assert.Equal("1.0.1", semver.Fixed);
|
||||
Assert.False(semver.FixedInclusive);
|
||||
|
||||
Assert.Single(advisory.CvssMetrics);
|
||||
Assert.Equal("3.1", advisory.CvssMetrics[0].Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_AssignsSeverityFallbackWhenCvssVectorUnsupported()
|
||||
{
|
||||
using var databaseSpecificJson = JsonDocument.Parse("""
|
||||
{
|
||||
"severity": "MODERATE",
|
||||
"cwe_ids": ["CWE-290"]
|
||||
}
|
||||
""");
|
||||
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = "OSV-CVSS4",
|
||||
Summary = "Severity-only advisory",
|
||||
Details = "OSV entry that lacks a parsable CVSS vector.",
|
||||
Published = DateTimeOffset.UtcNow.AddDays(-10),
|
||||
Modified = DateTimeOffset.UtcNow.AddDays(-5),
|
||||
DatabaseSpecific = databaseSpecificJson.RootElement,
|
||||
Severity = new[]
|
||||
{
|
||||
new OsvSeverityDto
|
||||
{
|
||||
Type = "CVSS_V4",
|
||||
Score = "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, "PyPI");
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, "PyPI");
|
||||
|
||||
Assert.True(advisory.CvssMetrics.IsEmpty);
|
||||
Assert.Equal("medium", advisory.Severity);
|
||||
Assert.Equal("osv:severity/medium", advisory.CanonicalMetricId);
|
||||
|
||||
var weakness = Assert.Single(advisory.Cwes);
|
||||
var provenance = Assert.Single(weakness.Provenance);
|
||||
Assert.Equal("database_specific.cwe_ids", provenance.DecisionReason);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Go", "github.com/example/project", "pkg:golang/github.com/example/project")]
|
||||
[InlineData("PyPI", "social_auth_app_django", "pkg:pypi/social-auth-app-django")]
|
||||
[InlineData("npm", "@Scope/Package", "pkg:npm/%40scope/package")]
|
||||
[InlineData("Maven", "org.example:library", "pkg:maven/org.example/library")]
|
||||
[InlineData("crates", "serde", "pkg:cargo/serde")]
|
||||
public void Map_InfersCanonicalPackageUrlWhenPurlMissing(string ecosystem, string packageName, string expectedIdentifier)
|
||||
{
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = $"OSV-{ecosystem}-PURL",
|
||||
Summary = "Test advisory",
|
||||
Details = "Details",
|
||||
Published = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Modified = DateTimeOffset.UtcNow,
|
||||
Affected = new[]
|
||||
{
|
||||
new OsvAffectedPackageDto
|
||||
{
|
||||
Package = new OsvPackageDto
|
||||
{
|
||||
Ecosystem = ecosystem,
|
||||
Name = packageName,
|
||||
Purl = null,
|
||||
},
|
||||
Ranges = null,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (string.Equals(ecosystem, "npm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Assert.True(IdentifierNormalizer.TryNormalizePackageUrl("pkg:npm/%40scope/package", out var canonical));
|
||||
Assert.Equal(expectedIdentifier, canonical);
|
||||
}
|
||||
|
||||
var method = typeof(OsvMapper).GetMethod("DetermineIdentifier", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.NotNull(method);
|
||||
var directIdentifier = method!.Invoke(null, new object?[] { dto.Affected![0].Package!, ecosystem }) as string;
|
||||
Assert.Equal(expectedIdentifier, directIdentifier);
|
||||
|
||||
var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, ecosystem);
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, ecosystem);
|
||||
|
||||
var affected = Assert.Single(advisory.AffectedPackages);
|
||||
Assert.Equal(expectedIdentifier, affected.Identifier);
|
||||
}
|
||||
|
||||
private static (DocumentRecord Document, DtoRecord DtoRecord) CreateDocumentAndDtoRecord(OsvVulnerabilityDto dto, string ecosystem)
|
||||
{
|
||||
var recordedAt = DateTimeOffset.UtcNow;
|
||||
var document = new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
OsvConnectorPlugin.SourceName,
|
||||
$"https://osv.dev/vulnerability/{dto.Id}",
|
||||
recordedAt,
|
||||
"sha256",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/json",
|
||||
null,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["osv.ecosystem"] = ecosystem,
|
||||
},
|
||||
null,
|
||||
dto.Modified,
|
||||
null,
|
||||
null);
|
||||
|
||||
var payload = new BsonDocument("id", dto.Id);
|
||||
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, recordedAt);
|
||||
return (document, dtoRecord);
|
||||
}
|
||||
}
|
||||
Assert.Single(advisory.CvssMetrics);
|
||||
Assert.Equal("3.1", advisory.CvssMetrics[0].Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_AssignsSeverityFallbackWhenCvssVectorUnsupported()
|
||||
{
|
||||
using var databaseSpecificJson = JsonDocument.Parse("""
|
||||
{
|
||||
"severity": "MODERATE",
|
||||
"cwe_ids": ["CWE-290"]
|
||||
}
|
||||
""");
|
||||
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = "OSV-CVSS4",
|
||||
Summary = "Severity-only advisory",
|
||||
Details = "OSV entry that lacks a parsable CVSS vector.",
|
||||
Published = DateTimeOffset.UtcNow.AddDays(-10),
|
||||
Modified = DateTimeOffset.UtcNow.AddDays(-5),
|
||||
DatabaseSpecific = databaseSpecificJson.RootElement,
|
||||
Severity = new[]
|
||||
{
|
||||
new OsvSeverityDto
|
||||
{
|
||||
Type = "CVSS_V4",
|
||||
Score = "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, "PyPI");
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, "PyPI");
|
||||
|
||||
Assert.True(advisory.CvssMetrics.IsEmpty);
|
||||
Assert.Equal("medium", advisory.Severity);
|
||||
Assert.Equal("osv:severity/medium", advisory.CanonicalMetricId);
|
||||
|
||||
var weakness = Assert.Single(advisory.Cwes);
|
||||
var provenance = Assert.Single(weakness.Provenance);
|
||||
Assert.Equal("database_specific.cwe_ids", provenance.DecisionReason);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Go", "github.com/example/project", "pkg:golang/github.com/example/project")]
|
||||
[InlineData("PyPI", "social_auth_app_django", "pkg:pypi/social-auth-app-django")]
|
||||
[InlineData("npm", "@Scope/Package", "pkg:npm/%40scope/package")]
|
||||
[InlineData("Maven", "org.example:library", "pkg:maven/org.example/library")]
|
||||
[InlineData("crates", "serde", "pkg:cargo/serde")]
|
||||
public void Map_InfersCanonicalPackageUrlWhenPurlMissing(string ecosystem, string packageName, string expectedIdentifier)
|
||||
{
|
||||
var dto = new OsvVulnerabilityDto
|
||||
{
|
||||
Id = $"OSV-{ecosystem}-PURL",
|
||||
Summary = "Test advisory",
|
||||
Details = "Details",
|
||||
Published = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Modified = DateTimeOffset.UtcNow,
|
||||
Affected = new[]
|
||||
{
|
||||
new OsvAffectedPackageDto
|
||||
{
|
||||
Package = new OsvPackageDto
|
||||
{
|
||||
Ecosystem = ecosystem,
|
||||
Name = packageName,
|
||||
Purl = null,
|
||||
},
|
||||
Ranges = null,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (string.Equals(ecosystem, "npm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Assert.True(IdentifierNormalizer.TryNormalizePackageUrl("pkg:npm/%40scope/package", out var canonical));
|
||||
Assert.Equal(expectedIdentifier, canonical);
|
||||
}
|
||||
|
||||
var method = typeof(OsvMapper).GetMethod("DetermineIdentifier", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.NotNull(method);
|
||||
var directIdentifier = method!.Invoke(null, new object?[] { dto.Affected![0].Package!, ecosystem }) as string;
|
||||
Assert.Equal(expectedIdentifier, directIdentifier);
|
||||
|
||||
var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, ecosystem);
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, ecosystem);
|
||||
|
||||
var affected = Assert.Single(advisory.AffectedPackages);
|
||||
Assert.Equal(expectedIdentifier, affected.Identifier);
|
||||
}
|
||||
|
||||
private static (DocumentRecord Document, DtoRecord DtoRecord) CreateDocumentAndDtoRecord(OsvVulnerabilityDto dto, string ecosystem)
|
||||
{
|
||||
var recordedAt = DateTimeOffset.UtcNow;
|
||||
var document = new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
OsvConnectorPlugin.SourceName,
|
||||
$"https://osv.dev/vulnerability/{dto.Id}",
|
||||
recordedAt,
|
||||
"sha256",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/json",
|
||||
null,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["osv.ecosystem"] = ecosystem,
|
||||
},
|
||||
null,
|
||||
dto.Modified,
|
||||
null,
|
||||
null);
|
||||
|
||||
var payload = new BsonDocument("id", dto.Id);
|
||||
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, recordedAt);
|
||||
return (document, dtoRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@@ -23,8 +23,8 @@ using StellaOps.Concelier.Connector.Ru.Bdu.Configuration;
|
||||
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
@@ -3,7 +3,7 @@ using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
|
||||
|
||||
@@ -19,13 +19,13 @@ using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ru.Nkcki.Tests;
|
||||
@@ -124,15 +124,15 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddStellaOpsCrypto();
|
||||
services.AddSourceCommon();
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddStellaOpsCrypto();
|
||||
services.AddSourceCommon();
|
||||
services.AddRuNkckiConnector(options =>
|
||||
{
|
||||
options.BaseAddress = new Uri("https://cert.gov.ru/");
|
||||
|
||||
@@ -3,7 +3,7 @@ using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using Xunit;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ using StellaOps.Concelier.Connector.StellaOpsMirror.Internal;
|
||||
using StellaOps.Concelier.Connector.StellaOpsMirror.Settings;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
|
||||
@@ -24,8 +24,8 @@ using StellaOps.Concelier.Connector.Vndr.Adobe;
|
||||
using StellaOps.Concelier.Connector.Vndr.Adobe.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.PsirtFlags;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
@@ -157,19 +157,19 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime
|
||||
Assert.NotNull(acrobatWindowsRange.Primitives);
|
||||
var windowsExtensions = acrobatWindowsRange.Primitives!.VendorExtensions;
|
||||
Assert.NotNull(windowsExtensions);
|
||||
Assert.True(windowsExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedWin));
|
||||
Assert.Equal("25.001.20672 and earlier", rawAffectedWin);
|
||||
Assert.True(windowsExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedWin));
|
||||
Assert.Equal("25.001.20680", rawUpdatedWin);
|
||||
Assert.Contains(
|
||||
AffectedPackageStatusCatalog.Fixed,
|
||||
acrobatWindowsPackage.Statuses.Select(static status => status.Status));
|
||||
var windowsNormalized = Assert.Single(acrobatWindowsPackage.NormalizedVersions.ToArray());
|
||||
Assert.Equal(NormalizedVersionSchemes.SemVer, windowsNormalized.Scheme);
|
||||
Assert.Equal(NormalizedVersionRuleTypes.LessThan, windowsNormalized.Type);
|
||||
Assert.Equal("25.1.20680", windowsNormalized.Max);
|
||||
Assert.False(windowsNormalized.MaxInclusive);
|
||||
Assert.Equal("adobe:Acrobat DC:Windows", windowsNormalized.Notes);
|
||||
Assert.True(windowsExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedWin));
|
||||
Assert.Equal("25.001.20672 and earlier", rawAffectedWin);
|
||||
Assert.True(windowsExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedWin));
|
||||
Assert.Equal("25.001.20680", rawUpdatedWin);
|
||||
Assert.Contains(
|
||||
AffectedPackageStatusCatalog.Fixed,
|
||||
acrobatWindowsPackage.Statuses.Select(static status => status.Status));
|
||||
var windowsNormalized = Assert.Single(acrobatWindowsPackage.NormalizedVersions.ToArray());
|
||||
Assert.Equal(NormalizedVersionSchemes.SemVer, windowsNormalized.Scheme);
|
||||
Assert.Equal(NormalizedVersionRuleTypes.LessThan, windowsNormalized.Type);
|
||||
Assert.Equal("25.1.20680", windowsNormalized.Max);
|
||||
Assert.False(windowsNormalized.MaxInclusive);
|
||||
Assert.Equal("adobe:Acrobat DC:Windows", windowsNormalized.Notes);
|
||||
|
||||
var acrobatMacPackage = Assert.Single(
|
||||
acrobatAdvisory.AffectedPackages,
|
||||
@@ -182,19 +182,19 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime
|
||||
Assert.NotNull(acrobatMacRange.Primitives);
|
||||
var macExtensions = acrobatMacRange.Primitives!.VendorExtensions;
|
||||
Assert.NotNull(macExtensions);
|
||||
Assert.True(macExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedMac));
|
||||
Assert.Equal("25.001.20668 and earlier", rawAffectedMac);
|
||||
Assert.True(macExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedMac));
|
||||
Assert.Equal("25.001.20678", rawUpdatedMac);
|
||||
Assert.Contains(
|
||||
AffectedPackageStatusCatalog.Fixed,
|
||||
acrobatMacPackage.Statuses.Select(static status => status.Status));
|
||||
var macNormalized = Assert.Single(acrobatMacPackage.NormalizedVersions.ToArray());
|
||||
Assert.Equal(NormalizedVersionSchemes.SemVer, macNormalized.Scheme);
|
||||
Assert.Equal(NormalizedVersionRuleTypes.LessThan, macNormalized.Type);
|
||||
Assert.Equal("25.1.20678", macNormalized.Max);
|
||||
Assert.False(macNormalized.MaxInclusive);
|
||||
Assert.Equal("adobe:Acrobat DC:macOS", macNormalized.Notes);
|
||||
Assert.True(macExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedMac));
|
||||
Assert.Equal("25.001.20668 and earlier", rawAffectedMac);
|
||||
Assert.True(macExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedMac));
|
||||
Assert.Equal("25.001.20678", rawUpdatedMac);
|
||||
Assert.Contains(
|
||||
AffectedPackageStatusCatalog.Fixed,
|
||||
acrobatMacPackage.Statuses.Select(static status => status.Status));
|
||||
var macNormalized = Assert.Single(acrobatMacPackage.NormalizedVersions.ToArray());
|
||||
Assert.Equal(NormalizedVersionSchemes.SemVer, macNormalized.Scheme);
|
||||
Assert.Equal(NormalizedVersionRuleTypes.LessThan, macNormalized.Type);
|
||||
Assert.Equal("25.1.20678", macNormalized.Max);
|
||||
Assert.False(macNormalized.MaxInclusive);
|
||||
Assert.Equal("adobe:Acrobat DC:macOS", macNormalized.Notes);
|
||||
|
||||
var premiereAdvisory = advisories.Single(a => a.AdvisoryKey == "APSB25-87");
|
||||
Assert.Contains("APSB25-87", premiereAdvisory.Aliases);
|
||||
@@ -211,17 +211,17 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime
|
||||
Assert.NotNull(premiereWindowsRange.Primitives);
|
||||
var premiereWindowsExtensions = premiereWindowsRange.Primitives!.VendorExtensions;
|
||||
Assert.NotNull(premiereWindowsExtensions);
|
||||
Assert.True(premiereWindowsExtensions!.TryGetValue("adobe.priority", out var premierePriorityWin));
|
||||
Assert.Equal("Priority 3", premierePriorityWin);
|
||||
Assert.Contains(
|
||||
AffectedPackageStatusCatalog.Fixed,
|
||||
premiereWindowsPackage.Statuses.Select(static status => status.Status));
|
||||
var premiereWinNormalized = Assert.Single(premiereWindowsPackage.NormalizedVersions.ToArray());
|
||||
Assert.Equal(NormalizedVersionSchemes.SemVer, premiereWinNormalized.Scheme);
|
||||
Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereWinNormalized.Type);
|
||||
Assert.Equal("24.6", premiereWinNormalized.Max);
|
||||
Assert.False(premiereWinNormalized.MaxInclusive);
|
||||
Assert.Equal("adobe:Premiere Pro:Windows", premiereWinNormalized.Notes);
|
||||
Assert.True(premiereWindowsExtensions!.TryGetValue("adobe.priority", out var premierePriorityWin));
|
||||
Assert.Equal("Priority 3", premierePriorityWin);
|
||||
Assert.Contains(
|
||||
AffectedPackageStatusCatalog.Fixed,
|
||||
premiereWindowsPackage.Statuses.Select(static status => status.Status));
|
||||
var premiereWinNormalized = Assert.Single(premiereWindowsPackage.NormalizedVersions.ToArray());
|
||||
Assert.Equal(NormalizedVersionSchemes.SemVer, premiereWinNormalized.Scheme);
|
||||
Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereWinNormalized.Type);
|
||||
Assert.Equal("24.6", premiereWinNormalized.Max);
|
||||
Assert.False(premiereWinNormalized.MaxInclusive);
|
||||
Assert.Equal("adobe:Premiere Pro:Windows", premiereWinNormalized.Notes);
|
||||
|
||||
var premiereMacPackage = Assert.Single(
|
||||
premiereAdvisory.AffectedPackages,
|
||||
@@ -233,29 +233,29 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime
|
||||
Assert.NotNull(premiereMacRange.Primitives);
|
||||
var premiereMacExtensions = premiereMacRange.Primitives!.VendorExtensions;
|
||||
Assert.NotNull(premiereMacExtensions);
|
||||
Assert.True(premiereMacExtensions!.TryGetValue("adobe.priority", out var premierePriorityMac));
|
||||
Assert.Equal("Priority 3", premierePriorityMac);
|
||||
Assert.Contains(
|
||||
AffectedPackageStatusCatalog.Fixed,
|
||||
premiereMacPackage.Statuses.Select(static status => status.Status));
|
||||
var premiereMacNormalized = Assert.Single(premiereMacPackage.NormalizedVersions.ToArray());
|
||||
Assert.Equal(NormalizedVersionSchemes.SemVer, premiereMacNormalized.Scheme);
|
||||
Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereMacNormalized.Type);
|
||||
Assert.Equal("24.6", premiereMacNormalized.Max);
|
||||
Assert.False(premiereMacNormalized.MaxInclusive);
|
||||
Assert.Equal("adobe:Premiere Pro:macOS", premiereMacNormalized.Notes);
|
||||
Assert.True(premiereMacExtensions!.TryGetValue("adobe.priority", out var premierePriorityMac));
|
||||
Assert.Equal("Priority 3", premierePriorityMac);
|
||||
Assert.Contains(
|
||||
AffectedPackageStatusCatalog.Fixed,
|
||||
premiereMacPackage.Statuses.Select(static status => status.Status));
|
||||
var premiereMacNormalized = Assert.Single(premiereMacPackage.NormalizedVersions.ToArray());
|
||||
Assert.Equal(NormalizedVersionSchemes.SemVer, premiereMacNormalized.Scheme);
|
||||
Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereMacNormalized.Type);
|
||||
Assert.Equal("24.6", premiereMacNormalized.Max);
|
||||
Assert.False(premiereMacNormalized.MaxInclusive);
|
||||
Assert.Equal("adobe:Premiere Pro:macOS", premiereMacNormalized.Notes);
|
||||
|
||||
var ordered = advisories.OrderBy(static a => a.AdvisoryKey, StringComparer.Ordinal).ToArray();
|
||||
var snapshot = SnapshotSerializer.ToSnapshot(ordered);
|
||||
var expected = ReadFixture("adobe-advisories.snapshot.json");
|
||||
var normalizedSnapshot = NormalizeLineEndings(snapshot);
|
||||
var normalizedExpected = NormalizeLineEndings(expected);
|
||||
if (!string.Equals(normalizedExpected, normalizedSnapshot, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Adobe", "Fixtures", "adobe-advisories.actual.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
File.WriteAllText(actualPath, snapshot);
|
||||
}
|
||||
var normalizedExpected = NormalizeLineEndings(expected);
|
||||
if (!string.Equals(normalizedExpected, normalizedSnapshot, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Adobe", "Fixtures", "adobe-advisories.actual.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
File.WriteAllText(actualPath, snapshot);
|
||||
}
|
||||
|
||||
Assert.Equal(normalizedExpected, normalizedSnapshot);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ using StellaOps.Concelier.Connector.Vndr.Chromium;
|
||||
using StellaOps.Concelier.Connector.Vndr.Chromium.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.PsirtFlags;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
@@ -86,13 +86,13 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime
|
||||
|
||||
var canonicalJson = CanonicalJsonSerializer.Serialize(advisory).Trim();
|
||||
var snapshotPath = ResolveFixturePath("chromium-advisory.snapshot.json");
|
||||
var expected = File.ReadAllText(snapshotPath).Trim();
|
||||
if (!string.Equals(expected, canonicalJson, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = ResolveFixturePath("chromium-advisory.actual.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
File.WriteAllText(actualPath, canonicalJson);
|
||||
}
|
||||
var expected = File.ReadAllText(snapshotPath).Trim();
|
||||
if (!string.Equals(expected, canonicalJson, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = ResolveFixturePath("chromium-advisory.actual.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
File.WriteAllText(actualPath, canonicalJson);
|
||||
}
|
||||
Assert.Equal(expected, canonicalJson);
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -7,8 +7,8 @@ using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Vndr.Cisco;
|
||||
using StellaOps.Concelier.Connector.Vndr.Cisco.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Cisco.Tests;
|
||||
|
||||
@@ -18,8 +18,8 @@ using StellaOps.Concelier.Connector.Vndr.Msrc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Vndr.Msrc.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
|
||||
@@ -27,8 +27,8 @@ using StellaOps.Concelier.Connector.Vndr.Oracle.Configuration;
|
||||
using StellaOps.Concelier.Connector.Vndr.Oracle.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ using StellaOps.Concelier.Connector.Vndr.Vmware.Configuration;
|
||||
using StellaOps.Concelier.Connector.Vndr.Vmware.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Vndr.Vmware;
|
||||
using StellaOps.Concelier.Connector.Vndr.Vmware.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
using StellaOps.Concelier.Storage.Mongo.Dtos;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Vmware.Tests;
|
||||
|
||||
@@ -1,492 +1,483 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Tests;
|
||||
|
||||
public sealed class JobCoordinatorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task TriggerAsync_RunCompletesSuccessfully()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SuccessfulJob>();
|
||||
services.AddLogging();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new InMemoryLeaseStore();
|
||||
var jobOptions = new JobSchedulerOptions
|
||||
{
|
||||
DefaultLeaseDuration = TimeSpan.FromSeconds(5),
|
||||
DefaultTimeout = TimeSpan.FromSeconds(10),
|
||||
};
|
||||
|
||||
var definition = new JobDefinition(
|
||||
Kind: "test:run",
|
||||
JobType: typeof(SuccessfulJob),
|
||||
Timeout: TimeSpan.FromSeconds(5),
|
||||
LeaseDuration: TimeSpan.FromSeconds(2),
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, new Dictionary<string, object?> { ["foo"] = "bar" }, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
|
||||
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
Assert.Equal(JobRunStatus.Succeeded, completed.Status);
|
||||
await leaseStore.WaitForReleaseAsync(TimeSpan.FromSeconds(1));
|
||||
Assert.True(leaseStore.ReleaseCount > 0);
|
||||
Assert.Equal("bar", completed.Parameters["foo"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_MarksRunFailed_WhenLeaseReleaseFails()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SuccessfulJob>();
|
||||
services.AddLogging();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new FailingLeaseStore
|
||||
{
|
||||
ThrowOnRelease = true,
|
||||
};
|
||||
|
||||
var jobOptions = new JobSchedulerOptions
|
||||
{
|
||||
DefaultLeaseDuration = TimeSpan.FromSeconds(5),
|
||||
DefaultTimeout = TimeSpan.FromSeconds(10),
|
||||
};
|
||||
|
||||
var definition = new JobDefinition(
|
||||
Kind: "test:run",
|
||||
JobType: typeof(SuccessfulJob),
|
||||
Timeout: TimeSpan.FromSeconds(5),
|
||||
LeaseDuration: TimeSpan.FromSeconds(2),
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
|
||||
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
Assert.Equal(JobRunStatus.Failed, completed.Status);
|
||||
Assert.NotNull(completed.Error);
|
||||
Assert.Contains("Failed to release lease", completed.Error!, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(leaseStore.ReleaseAttempts > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_MarksRunFailed_WhenLeaseHeartbeatFails()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SlowJob>();
|
||||
services.AddLogging();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new FailingLeaseStore
|
||||
{
|
||||
ThrowOnHeartbeat = true,
|
||||
};
|
||||
|
||||
var jobOptions = new JobSchedulerOptions
|
||||
{
|
||||
DefaultLeaseDuration = TimeSpan.FromSeconds(2),
|
||||
DefaultTimeout = TimeSpan.FromSeconds(10),
|
||||
};
|
||||
|
||||
var definition = new JobDefinition(
|
||||
Kind: "test:heartbeat",
|
||||
JobType: typeof(SlowJob),
|
||||
Timeout: TimeSpan.FromSeconds(5),
|
||||
LeaseDuration: TimeSpan.FromSeconds(2),
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
|
||||
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(6));
|
||||
Assert.Equal(JobRunStatus.Failed, completed.Status);
|
||||
Assert.NotNull(completed.Error);
|
||||
Assert.Contains("Failed to heartbeat lease", completed.Error!, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(leaseStore.HeartbeatCount > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_ReturnsAlreadyRunning_WhenLeaseUnavailable()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SuccessfulJob>();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new InMemoryLeaseStore
|
||||
{
|
||||
NextLease = null,
|
||||
};
|
||||
var jobOptions = new JobSchedulerOptions();
|
||||
var definition = new JobDefinition(
|
||||
"test:run",
|
||||
typeof(SuccessfulJob),
|
||||
TimeSpan.FromSeconds(5),
|
||||
TimeSpan.FromSeconds(2),
|
||||
null,
|
||||
true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.AlreadyRunning, result.Outcome);
|
||||
Assert.False(jobStore.CreatedRuns.Any());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_ReturnsInvalidParameters_ForUnsupportedPayload()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SuccessfulJob>();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new InMemoryLeaseStore();
|
||||
var jobOptions = new JobSchedulerOptions();
|
||||
var definition = new JobDefinition(
|
||||
"test:run",
|
||||
typeof(SuccessfulJob),
|
||||
TimeSpan.FromSeconds(5),
|
||||
TimeSpan.FromSeconds(2),
|
||||
null,
|
||||
true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var parameters = new Dictionary<string, object?>
|
||||
{
|
||||
["bad"] = new object(),
|
||||
};
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, parameters, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.InvalidParameters, result.Outcome);
|
||||
Assert.Contains("unsupported", result.ErrorMessage, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.False(jobStore.CreatedRuns.Any());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_CancelsJobOnTimeout()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<TimeoutJob>();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new InMemoryLeaseStore();
|
||||
var jobOptions = new JobSchedulerOptions
|
||||
{
|
||||
DefaultLeaseDuration = TimeSpan.FromSeconds(5),
|
||||
DefaultTimeout = TimeSpan.FromMilliseconds(100),
|
||||
};
|
||||
|
||||
var definition = new JobDefinition(
|
||||
Kind: "test:timeout",
|
||||
JobType: typeof(TimeoutJob),
|
||||
Timeout: TimeSpan.FromMilliseconds(100),
|
||||
LeaseDuration: TimeSpan.FromSeconds(2),
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
|
||||
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
|
||||
|
||||
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
Assert.Equal(JobRunStatus.Cancelled, completed.Status);
|
||||
await leaseStore.WaitForReleaseAsync(TimeSpan.FromSeconds(1));
|
||||
Assert.True(leaseStore.ReleaseCount > 0);
|
||||
}
|
||||
|
||||
private sealed class SuccessfulJob : IJob
|
||||
{
|
||||
public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TimeoutJob : IJob
|
||||
{
|
||||
public async Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SlowJob : IJob
|
||||
{
|
||||
public async Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InMemoryJobStore : IJobStore
|
||||
{
|
||||
private readonly Dictionary<Guid, JobRunSnapshot> _runs = new();
|
||||
public TaskCompletionSource<JobRunSnapshot> Completion { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public List<JobRunSnapshot> CreatedRuns { get; } = new();
|
||||
|
||||
public Task<JobRunSnapshot> CreateAsync(JobRunCreateRequest request, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Tests;
|
||||
|
||||
public sealed class JobCoordinatorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task TriggerAsync_RunCompletesSuccessfully()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SuccessfulJob>();
|
||||
services.AddLogging();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new InMemoryLeaseStore();
|
||||
var jobOptions = new JobSchedulerOptions
|
||||
{
|
||||
DefaultLeaseDuration = TimeSpan.FromSeconds(5),
|
||||
DefaultTimeout = TimeSpan.FromSeconds(10),
|
||||
};
|
||||
|
||||
var definition = new JobDefinition(
|
||||
Kind: "test:run",
|
||||
JobType: typeof(SuccessfulJob),
|
||||
Timeout: TimeSpan.FromSeconds(5),
|
||||
LeaseDuration: TimeSpan.FromSeconds(2),
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, new Dictionary<string, object?> { ["foo"] = "bar" }, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
|
||||
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
Assert.Equal(JobRunStatus.Succeeded, completed.Status);
|
||||
await leaseStore.WaitForReleaseAsync(TimeSpan.FromSeconds(1));
|
||||
Assert.True(leaseStore.ReleaseCount > 0);
|
||||
Assert.Equal("bar", completed.Parameters["foo"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_MarksRunFailed_WhenLeaseReleaseFails()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SuccessfulJob>();
|
||||
services.AddLogging();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new FailingLeaseStore
|
||||
{
|
||||
ThrowOnRelease = true,
|
||||
};
|
||||
|
||||
var jobOptions = new JobSchedulerOptions
|
||||
{
|
||||
DefaultLeaseDuration = TimeSpan.FromSeconds(5),
|
||||
DefaultTimeout = TimeSpan.FromSeconds(10),
|
||||
};
|
||||
|
||||
var definition = new JobDefinition(
|
||||
Kind: "test:run",
|
||||
JobType: typeof(SuccessfulJob),
|
||||
Timeout: TimeSpan.FromSeconds(5),
|
||||
LeaseDuration: TimeSpan.FromSeconds(2),
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
|
||||
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
Assert.Equal(JobRunStatus.Failed, completed.Status);
|
||||
Assert.NotNull(completed.Error);
|
||||
Assert.Contains("Failed to release lease", completed.Error!, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(leaseStore.ReleaseAttempts > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_MarksRunFailed_WhenLeaseHeartbeatFails()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SlowJob>();
|
||||
services.AddLogging();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new FailingLeaseStore
|
||||
{
|
||||
ThrowOnHeartbeat = true,
|
||||
};
|
||||
|
||||
var jobOptions = new JobSchedulerOptions
|
||||
{
|
||||
DefaultLeaseDuration = TimeSpan.FromSeconds(2),
|
||||
DefaultTimeout = TimeSpan.FromSeconds(10),
|
||||
};
|
||||
|
||||
var definition = new JobDefinition(
|
||||
Kind: "test:heartbeat",
|
||||
JobType: typeof(SlowJob),
|
||||
Timeout: TimeSpan.FromSeconds(5),
|
||||
LeaseDuration: TimeSpan.FromSeconds(2),
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
|
||||
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(6));
|
||||
Assert.Equal(JobRunStatus.Failed, completed.Status);
|
||||
Assert.NotNull(completed.Error);
|
||||
Assert.Contains("Failed to heartbeat lease", completed.Error!, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(leaseStore.HeartbeatCount > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_ReturnsAlreadyRunning_WhenLeaseUnavailable()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SuccessfulJob>();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new InMemoryLeaseStore
|
||||
{
|
||||
NextLease = null,
|
||||
};
|
||||
var jobOptions = new JobSchedulerOptions();
|
||||
var definition = new JobDefinition(
|
||||
"test:run",
|
||||
typeof(SuccessfulJob),
|
||||
TimeSpan.FromSeconds(5),
|
||||
TimeSpan.FromSeconds(2),
|
||||
null,
|
||||
true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.AlreadyRunning, result.Outcome);
|
||||
Assert.False(jobStore.CreatedRuns.Any());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_ReturnsInvalidParameters_ForUnsupportedPayload()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<SuccessfulJob>();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new InMemoryLeaseStore();
|
||||
var jobOptions = new JobSchedulerOptions();
|
||||
var definition = new JobDefinition(
|
||||
"test:run",
|
||||
typeof(SuccessfulJob),
|
||||
TimeSpan.FromSeconds(5),
|
||||
TimeSpan.FromSeconds(2),
|
||||
null,
|
||||
true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var parameters = new Dictionary<string, object?>
|
||||
{
|
||||
["bad"] = new object(),
|
||||
};
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, parameters, "unit-test", CancellationToken.None);
|
||||
|
||||
Assert.Equal(JobTriggerOutcome.InvalidParameters, result.Outcome);
|
||||
Assert.Contains("unsupported", result.ErrorMessage, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.False(jobStore.CreatedRuns.Any());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerAsync_CancelsJobOnTimeout()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddTransient<TimeoutJob>();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var jobStore = new InMemoryJobStore();
|
||||
var leaseStore = new InMemoryLeaseStore();
|
||||
var jobOptions = new JobSchedulerOptions
|
||||
{
|
||||
DefaultLeaseDuration = TimeSpan.FromSeconds(5),
|
||||
DefaultTimeout = TimeSpan.FromMilliseconds(100),
|
||||
};
|
||||
|
||||
var definition = new JobDefinition(
|
||||
Kind: "test:timeout",
|
||||
JobType: typeof(TimeoutJob),
|
||||
Timeout: TimeSpan.FromMilliseconds(100),
|
||||
LeaseDuration: TimeSpan.FromSeconds(2),
|
||||
CronExpression: null,
|
||||
Enabled: true);
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
leaseStore,
|
||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||
NullLogger<JobCoordinator>.Instance,
|
||||
NullLoggerFactory.Instance,
|
||||
new TestTimeProvider(),
|
||||
diagnostics);
|
||||
|
||||
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
|
||||
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
|
||||
|
||||
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
Assert.Equal(JobRunStatus.Cancelled, completed.Status);
|
||||
await leaseStore.WaitForReleaseAsync(TimeSpan.FromSeconds(1));
|
||||
Assert.True(leaseStore.ReleaseCount > 0);
|
||||
}
|
||||
|
||||
private sealed class SuccessfulJob : IJob
|
||||
{
|
||||
public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TimeoutJob : IJob
|
||||
{
|
||||
public async Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SlowJob : IJob
|
||||
{
|
||||
public async Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InMemoryJobStore : IJobStore
|
||||
{
|
||||
private readonly Dictionary<Guid, JobRunSnapshot> _runs = new();
|
||||
public TaskCompletionSource<JobRunSnapshot> Completion { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public List<JobRunSnapshot> CreatedRuns { get; } = new();
|
||||
|
||||
public Task<JobRunSnapshot> CreateAsync(JobRunCreateRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
var run = new JobRunSnapshot(
|
||||
Guid.NewGuid(),
|
||||
request.Kind,
|
||||
JobRunStatus.Pending,
|
||||
request.CreatedAt,
|
||||
null,
|
||||
null,
|
||||
request.Trigger,
|
||||
request.ParametersHash,
|
||||
null,
|
||||
request.Timeout,
|
||||
request.LeaseDuration,
|
||||
request.Parameters);
|
||||
_runs[run.RunId] = run;
|
||||
CreatedRuns.Add(run);
|
||||
return Task.FromResult(run);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> TryStartAsync(Guid runId, DateTimeOffset startedAt, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
request.Kind,
|
||||
JobRunStatus.Pending,
|
||||
request.CreatedAt,
|
||||
null,
|
||||
null,
|
||||
request.Trigger,
|
||||
request.ParametersHash,
|
||||
null,
|
||||
request.Timeout,
|
||||
request.LeaseDuration,
|
||||
request.Parameters);
|
||||
_runs[run.RunId] = run;
|
||||
CreatedRuns.Add(run);
|
||||
return Task.FromResult(run);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> TryStartAsync(Guid runId, DateTimeOffset startedAt, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
if (_runs.TryGetValue(runId, out var run))
|
||||
{
|
||||
var updated = run with { Status = JobRunStatus.Running, StartedAt = startedAt };
|
||||
_runs[runId] = updated;
|
||||
return Task.FromResult<JobRunSnapshot?>(updated);
|
||||
}
|
||||
|
||||
return Task.FromResult<JobRunSnapshot?>(null);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> TryCompleteAsync(Guid runId, JobRunCompletion completion, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
{
|
||||
var updated = run with { Status = JobRunStatus.Running, StartedAt = startedAt };
|
||||
_runs[runId] = updated;
|
||||
return Task.FromResult<JobRunSnapshot?>(updated);
|
||||
}
|
||||
|
||||
return Task.FromResult<JobRunSnapshot?>(null);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> TryCompleteAsync(Guid runId, JobRunCompletion completion, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
if (_runs.TryGetValue(runId, out var run))
|
||||
{
|
||||
var updated = run with { Status = completion.Status, CompletedAt = completion.CompletedAt, Error = completion.Error };
|
||||
_runs[runId] = updated;
|
||||
Completion.TrySetResult(updated);
|
||||
return Task.FromResult<JobRunSnapshot?>(updated);
|
||||
}
|
||||
|
||||
return Task.FromResult<JobRunSnapshot?>(null);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> FindAsync(Guid runId, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
{
|
||||
var updated = run with { Status = completion.Status, CompletedAt = completion.CompletedAt, Error = completion.Error };
|
||||
_runs[runId] = updated;
|
||||
Completion.TrySetResult(updated);
|
||||
return Task.FromResult<JobRunSnapshot?>(updated);
|
||||
}
|
||||
|
||||
return Task.FromResult<JobRunSnapshot?>(null);
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> FindAsync(Guid runId, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
_runs.TryGetValue(runId, out var run);
|
||||
return Task.FromResult<JobRunSnapshot?>(run);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<JobRunSnapshot>> GetRecentRunsAsync(string? kind, int limit, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
|
||||
public Task<IReadOnlyList<JobRunSnapshot>> GetRecentRunsAsync(string? kind, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
var query = _runs.Values.AsEnumerable();
|
||||
if (!string.IsNullOrWhiteSpace(kind))
|
||||
{
|
||||
query = query.Where(r => r.Kind == kind);
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<JobRunSnapshot>>(query.OrderByDescending(r => r.CreatedAt).Take(limit).ToArray());
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<JobRunSnapshot>> GetActiveRunsAsync(CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
if (!string.IsNullOrWhiteSpace(kind))
|
||||
{
|
||||
query = query.Where(r => r.Kind == kind);
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<JobRunSnapshot>>(query.OrderByDescending(r => r.CreatedAt).Take(limit).ToArray());
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<JobRunSnapshot>> GetActiveRunsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
return Task.FromResult<IReadOnlyList<JobRunSnapshot>>(_runs.Values.Where(r => r.Status is JobRunStatus.Pending or JobRunStatus.Running).ToArray());
|
||||
}
|
||||
|
||||
public Task<JobRunSnapshot?> GetLastRunAsync(string kind, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
|
||||
public Task<JobRunSnapshot?> GetLastRunAsync(string kind, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
var run = _runs.Values
|
||||
.Where(r => r.Kind == kind)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
return Task.FromResult<JobRunSnapshot?>(run);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyDictionary<string, JobRunSnapshot>> GetLastRunsAsync(IEnumerable<string> kinds, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
.FirstOrDefault();
|
||||
return Task.FromResult<JobRunSnapshot?>(run);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyDictionary<string, JobRunSnapshot>> GetLastRunsAsync(IEnumerable<string> kinds, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
var results = new Dictionary<string, JobRunSnapshot>(StringComparer.Ordinal);
|
||||
foreach (var kind in kinds.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
var run = _runs.Values
|
||||
.Where(r => r.Kind == kind)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
if (run is not null)
|
||||
{
|
||||
results[kind] = run;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyDictionary<string, JobRunSnapshot>>(results);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InMemoryLeaseStore : ILeaseStore
|
||||
{
|
||||
public JobLease? NextLease { get; set; } = new JobLease("job:test:run", "holder", DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, TimeSpan.FromSeconds(2), DateTimeOffset.UtcNow.AddSeconds(2));
|
||||
public int HeartbeatCount { get; private set; }
|
||||
public int ReleaseCount { get; private set; }
|
||||
private readonly TaskCompletionSource<bool> _released = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public Task<JobLease?> TryAcquireAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(NextLease);
|
||||
}
|
||||
|
||||
public Task<JobLease?> HeartbeatAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
|
||||
{
|
||||
HeartbeatCount++;
|
||||
NextLease = new JobLease(key, holder, now, now, leaseDuration, now.Add(leaseDuration));
|
||||
return Task.FromResult<JobLease?>(NextLease);
|
||||
}
|
||||
|
||||
public Task<bool> ReleaseAsync(string key, string holder, CancellationToken cancellationToken)
|
||||
{
|
||||
ReleaseCount++;
|
||||
_released.TrySetResult(true);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task WaitForReleaseAsync(TimeSpan timeout)
|
||||
=> _released.Task.WaitAsync(timeout);
|
||||
}
|
||||
|
||||
private sealed class FailingLeaseStore : ILeaseStore
|
||||
{
|
||||
public bool ThrowOnHeartbeat { get; set; }
|
||||
public bool ThrowOnRelease { get; set; }
|
||||
|
||||
public int HeartbeatCount { get; private set; }
|
||||
public int ReleaseAttempts { get; private set; }
|
||||
|
||||
public Task<JobLease?> TryAcquireAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
|
||||
{
|
||||
var lease = new JobLease(key, holder, now, now, leaseDuration, now.Add(leaseDuration));
|
||||
return Task.FromResult<JobLease?>(lease);
|
||||
}
|
||||
|
||||
public Task<JobLease?> HeartbeatAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
|
||||
{
|
||||
HeartbeatCount++;
|
||||
if (ThrowOnHeartbeat)
|
||||
{
|
||||
throw new InvalidOperationException("Lease heartbeat failed");
|
||||
}
|
||||
|
||||
var lease = new JobLease(key, holder, now, now, leaseDuration, now.Add(leaseDuration));
|
||||
return Task.FromResult<JobLease?>(lease);
|
||||
}
|
||||
|
||||
public Task<bool> ReleaseAsync(string key, string holder, CancellationToken cancellationToken)
|
||||
{
|
||||
ReleaseAttempts++;
|
||||
if (ThrowOnRelease)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to release lease");
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now = DateTimeOffset.Parse("2024-01-01T00:00:00Z");
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now = _now.AddMilliseconds(100);
|
||||
}
|
||||
}
|
||||
foreach (var kind in kinds.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
var run = _runs.Values
|
||||
.Where(r => r.Kind == kind)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
if (run is not null)
|
||||
{
|
||||
results[kind] = run;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyDictionary<string, JobRunSnapshot>>(results);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InMemoryLeaseStore : ILeaseStore
|
||||
{
|
||||
public JobLease? NextLease { get; set; } = new JobLease("job:test:run", "holder", DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, TimeSpan.FromSeconds(2), DateTimeOffset.UtcNow.AddSeconds(2));
|
||||
public int HeartbeatCount { get; private set; }
|
||||
public int ReleaseCount { get; private set; }
|
||||
private readonly TaskCompletionSource<bool> _released = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public Task<JobLease?> TryAcquireAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(NextLease);
|
||||
}
|
||||
|
||||
public Task<JobLease?> HeartbeatAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
|
||||
{
|
||||
HeartbeatCount++;
|
||||
NextLease = new JobLease(key, holder, now, now, leaseDuration, now.Add(leaseDuration));
|
||||
return Task.FromResult<JobLease?>(NextLease);
|
||||
}
|
||||
|
||||
public Task<bool> ReleaseAsync(string key, string holder, CancellationToken cancellationToken)
|
||||
{
|
||||
ReleaseCount++;
|
||||
_released.TrySetResult(true);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task WaitForReleaseAsync(TimeSpan timeout)
|
||||
=> _released.Task.WaitAsync(timeout);
|
||||
}
|
||||
|
||||
private sealed class FailingLeaseStore : ILeaseStore
|
||||
{
|
||||
public bool ThrowOnHeartbeat { get; set; }
|
||||
public bool ThrowOnRelease { get; set; }
|
||||
|
||||
public int HeartbeatCount { get; private set; }
|
||||
public int ReleaseAttempts { get; private set; }
|
||||
|
||||
public Task<JobLease?> TryAcquireAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
|
||||
{
|
||||
var lease = new JobLease(key, holder, now, now, leaseDuration, now.Add(leaseDuration));
|
||||
return Task.FromResult<JobLease?>(lease);
|
||||
}
|
||||
|
||||
public Task<JobLease?> HeartbeatAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
|
||||
{
|
||||
HeartbeatCount++;
|
||||
if (ThrowOnHeartbeat)
|
||||
{
|
||||
throw new InvalidOperationException("Lease heartbeat failed");
|
||||
}
|
||||
|
||||
var lease = new JobLease(key, holder, now, now, leaseDuration, now.Add(leaseDuration));
|
||||
return Task.FromResult<JobLease?>(lease);
|
||||
}
|
||||
|
||||
public Task<bool> ReleaseAsync(string key, string holder, CancellationToken cancellationToken)
|
||||
{
|
||||
ReleaseAttempts++;
|
||||
if (ThrowOnRelease)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to release lease");
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now = DateTimeOffset.Parse("2024-01-01T00:00:00Z");
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now = _now.AddMilliseconds(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,87 +227,47 @@ public sealed class AocVerifyRegressionTests
|
||||
|
||||
private static JsonDocument CreateJsonWithForbiddenField(string field, string value)
|
||||
{
|
||||
return JsonDocument.Parse($$"""
|
||||
{
|
||||
"tenant": "test",
|
||||
"{{field}}": "{{value}}",
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""");
|
||||
var payload = BuildBasePayload(field, value);
|
||||
return JsonSerializer.SerializeToDocument(payload);
|
||||
}
|
||||
|
||||
private static JsonDocument CreateJsonWithDerivedField(string field, string value)
|
||||
{
|
||||
return JsonDocument.Parse($$"""
|
||||
{
|
||||
"tenant": "test",
|
||||
"{{field}}": "{{value}}",
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""");
|
||||
var payload = BuildBasePayload(field, value);
|
||||
return JsonSerializer.SerializeToDocument(payload);
|
||||
}
|
||||
|
||||
private static JsonDocument CreateJsonWithUnknownField(string field, string value)
|
||||
{
|
||||
return JsonDocument.Parse($$"""
|
||||
{
|
||||
"tenant": "test",
|
||||
"{{field}}": "{{value}}",
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""");
|
||||
var payload = BuildBasePayload(field, value);
|
||||
return JsonSerializer.SerializeToDocument(payload);
|
||||
}
|
||||
|
||||
private static JsonDocument CreateJsonWithMergedFrom(string[] mergedFrom)
|
||||
{
|
||||
var mergedArray = string.Join(", ", mergedFrom.Select(m => $"\"{m}\""));
|
||||
return JsonDocument.Parse($$"""
|
||||
var payload = BuildBasePayload("merged_from", mergedFrom);
|
||||
return JsonSerializer.SerializeToDocument(payload);
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> BuildBasePayload(string field, object value)
|
||||
{
|
||||
return new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["tenant"] = "test",
|
||||
[field] = value,
|
||||
["source"] = new { vendor = "test", connector = "test", version = "1.0" },
|
||||
["upstream"] = new
|
||||
{
|
||||
"tenant": "test",
|
||||
"merged_from": [{{mergedArray}}],
|
||||
"source": {"vendor": "test", "connector": "test", "version": "1.0"},
|
||||
"upstream": {
|
||||
"upstream_id": "CVE-2024-0001",
|
||||
"content_hash": "sha256:abc",
|
||||
"retrieved_at": "2024-01-01T00:00:00Z",
|
||||
"signature": {"present": false},
|
||||
"provenance": {}
|
||||
},
|
||||
"content": {"format": "OSV", "raw": {}},
|
||||
"identifiers": {"aliases": [], "primary": "CVE-2024-0001"},
|
||||
"linkset": {}
|
||||
}
|
||||
""");
|
||||
upstream_id = "CVE-2024-0001",
|
||||
content_hash = "sha256:abc",
|
||||
retrieved_at = "2024-01-01T00:00:00Z",
|
||||
signature = new { present = false },
|
||||
provenance = new { }
|
||||
},
|
||||
["content"] = new { format = "OSV", raw = new { } },
|
||||
["identifiers"] = new { aliases = Array.Empty<string>(), primary = "CVE-2024-0001" },
|
||||
["linkset"] = new { }
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonDocument CreateJsonWithMultipleViolations()
|
||||
|
||||
@@ -214,6 +214,13 @@ public static class AdvisoryChunkSeedData
|
||||
|
||||
private static string CreateRawContent(string advisoryId, string source, string severity)
|
||||
{
|
||||
var cvss = severity switch
|
||||
{
|
||||
"critical" => "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"high" => "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
|
||||
_ => "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
|
||||
};
|
||||
|
||||
return $$"""
|
||||
{
|
||||
"id": "{{advisoryId}}",
|
||||
@@ -225,7 +232,7 @@ public static class AdvisoryChunkSeedData
|
||||
"severity": [
|
||||
{
|
||||
"type": "CVSS_V3",
|
||||
"score": "{{severity == "critical" ? "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" : severity == "high" ? "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"}}"
|
||||
"score": "{{cvss}}"
|
||||
}
|
||||
],
|
||||
"affected": [
|
||||
|
||||
Reference in New Issue
Block a user