Merge all changes

This commit is contained in:
StellaOps Bot
2026-01-08 08:54:27 +02:00
parent 589de352c2
commit 110591d6bf
381 changed files with 2237 additions and 1939 deletions

View File

@@ -1,5 +1,7 @@
## Release History
; Unshipped analyzer releases
### Unreleased
### New Rules
- CONCELIER0004: Flag direct `new HttpClient()` usage inside `StellaOps.Concelier.Connector*` namespaces; require sandboxed `IHttpClientFactory` to enforce allow/deny lists. Exempts test assemblies and uses symbol-based namespace matching.
Rule ID | Category | Severity | Notes
--------|----------|----------|------
CONCELIER0004 | Sandbox | Warning | Flag direct `new HttpClient()` usage inside `StellaOps.Concelier.Connector*` namespaces

View File

@@ -15,7 +15,7 @@ public sealed class ConnectorHttpClientSandboxAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor Rule = new(
id: DiagnosticId,
title: "Connector HTTP clients must use sandboxed factory",
messageFormat: "Use IHttpClientFactory or connector sandbox helpers instead of 'new HttpClient()' inside Concelier connectors.",
messageFormat: "Use IHttpClientFactory or connector sandbox helpers instead of 'new HttpClient()' inside Concelier connectors",
category: "Sandbox",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
@@ -73,7 +73,7 @@ public sealed class ConnectorHttpClientSandboxAnalyzer : DiagnosticAnalyzer
return false;
}
return assemblyName.EndsWith(".Tests", StringComparison.OrdinalIgnoreCase)
return assemblyName!.EndsWith(".Tests", StringComparison.OrdinalIgnoreCase)
|| assemblyName.EndsWith(".Test", StringComparison.OrdinalIgnoreCase)
|| assemblyName.EndsWith(".Testing", StringComparison.OrdinalIgnoreCase);
}

View File

@@ -1,9 +1,7 @@
## Release History
; Unshipped analyzer releases
### Unreleased
### New Rules
#### New Rules
Rule ID | Title | Notes
--------|-------|------
CONCELIER0002 | Legacy merge service usage detected | Flags references to `AdvisoryMergeService` and `AddMergeModule`.
Rule ID | Category | Severity | Notes
--------|----------|----------|------
CONCELIER0002 | Usage | Warning | Legacy merge service usage detected - flags references to `AdvisoryMergeService` and `AddMergeModule`

View File

@@ -185,6 +185,11 @@ public sealed class AstraConnector : IFeedConnector
/// </remarks>
private async Task<string> FetchOvalDatabaseAsync(string version, CancellationToken cancellationToken)
{
if (_fetchService is null || _rawDocumentStorage is null)
{
throw new InvalidOperationException("Fetch and raw document storage services are required for OVAL database fetch");
}
var uri = _options.BuildOvalDatabaseUri(version);
_logger.LogDebug("Fetching OVAL database for Astra Linux {Version} from {Uri}", version, uri);
@@ -197,18 +202,24 @@ public sealed class AstraConnector : IFeedConnector
var result = await _fetchService.FetchAsync(request, cancellationToken).ConfigureAwait(false);
if (!result.IsSuccess || result.Document is null)
if (result is null || !result.IsSuccess || result.Document is null)
{
throw new InvalidOperationException($"Failed to fetch OVAL database for version {version}");
}
if (!result.Document.PayloadId.HasValue)
var document = result.Document;
if (!document.PayloadId.HasValue)
{
throw new InvalidOperationException($"OVAL database document for version {version} has no payload");
}
// Download the raw XML content
var payloadBytes = await _rawDocumentStorage.DownloadAsync(result.Document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
var payloadBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
if (payloadBytes is null)
{
throw new InvalidOperationException($"OVAL database payload for version {version} not found");
}
return System.Text.Encoding.UTF8.GetString(payloadBytes);
}

View File

@@ -359,7 +359,7 @@ public sealed class CccsHtmlParser
var candidate = href.Trim();
var hasAbsolute = Uri.TryCreate(candidate, UriKind.Absolute, out var absolute);
if (!hasAbsolute || string.Equals(absolute.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
if (!hasAbsolute || absolute is null || string.Equals(absolute.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
{
if (baseUri is null || !Uri.TryCreate(baseUri, candidate, out absolute))
{

View File

@@ -177,7 +177,7 @@ public sealed class CertCcConnector : IFeedConnector
await _documentStore.UpdateStatusAsync(result.Document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
}
if (!shouldProcessNotes)
if (!shouldProcessNotes || result.Document is null)
{
continue;
}

View File

@@ -168,7 +168,7 @@ internal sealed record CertCcCursor(
}
var bytes = binary.AsByteArray;
if (bytes.Length == 16)
if (bytes is not null && bytes.Length == 16)
{
guid = new Guid(bytes);
return true;

View File

@@ -605,7 +605,7 @@ public sealed class DebianConnector : IFeedConnector
{
["advisoryId"] = dto.AdvisoryId,
["sourcePackage"] = dto.SourcePackage,
["title"] = dto.Title,
["title"] = dto.Title ?? string.Empty,
["description"] = dto.Description ?? string.Empty,
["cves"] = new DocumentArray(dto.CveIds),
["packages"] = packages,

View File

@@ -29,7 +29,12 @@ internal static class RedHatMapper
ArgumentNullException.ThrowIfNull(payload);
var csaf = JsonSerializer.Deserialize<RedHatCsafEnvelope>(payload.RootElement.GetRawText(), SerializerOptions);
var documentSection = csaf?.Document;
if (csaf is null)
{
return null;
}
var documentSection = csaf.Document;
if (documentSection is null)
{
return null;
@@ -722,7 +727,7 @@ internal sealed class RedHatProductIndex
return new RedHatProductIndex(products);
}
public bool TryGetValue(string productId, out RedHatProductNode node)
public bool TryGetValue(string productId, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out RedHatProductNode? node)
=> _products.TryGetValue(productId, out node);
private static void Traverse(RedHatProductBranch? branch, IDictionary<string, RedHatProductNode> products)

View File

@@ -464,7 +464,7 @@ public sealed class EpssConnector : IFeedConnector
["epss.file"] = GetSnapshotFileName(fetchResult.SnapshotDate)
};
if (_options.AirgapMode)
if (_options.AirgapMode && fetchResult.Content is not null)
{
TryApplyBundleManifest(fetchResult.SnapshotDate, fetchResult.Content, metadata);
}
@@ -473,6 +473,11 @@ public sealed class EpssConnector : IFeedConnector
// Use existing ID or derive deterministic ID from source + uri
var recordId = existing?.Id ?? ComputeDeterministicId(SourceName, fetchResult.SourceUri);
if (fetchResult.Content is null)
{
throw new InvalidOperationException($"EPSS fetch returned null content for {fetchResult.SourceUri}");
}
await _rawDocumentStorage.UploadAsync(
SourceName,
fetchResult.SourceUri,

View File

@@ -25,8 +25,6 @@ using StellaOps.Concelier.Connector.Ics.Cisa.Configuration;
using StellaOps.Concelier.Connector.Ics.Cisa.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Normalization.SemVer;
using StellaOps.Cryptography;
using StellaOps.Plugin;

View File

@@ -7,7 +7,6 @@ using StellaOps.Concelier.Normalization.Cvss;
using StellaOps.Concelier.Normalization.Identifiers;
using StellaOps.Concelier.Normalization.Text;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.JpFlags;
namespace StellaOps.Concelier.Connector.Jvn.Internal;
@@ -68,7 +67,7 @@ internal static class JvnAdvisoryMapper
var flag = new JpFlagRecord(
detail.VulnerabilityId,
JvnConnectorPlugin.SourceName,
detail.JvnCategory,
detail.JvnCategory ?? string.Empty,
vendorStatus,
timeProvider.GetUtcNow());

View File

@@ -13,8 +13,6 @@ using StellaOps.Concelier.Connector.Jvn.Configuration;
using StellaOps.Concelier.Connector.Jvn.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.JpFlags;
using StellaOps.Cryptography;
using StellaOps.Plugin;

View File

@@ -18,8 +18,6 @@ using StellaOps.Concelier.Connector.Kev.Configuration;
using StellaOps.Concelier.Connector.Kev.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
using StellaOps.Plugin;

View File

@@ -14,8 +14,6 @@ using StellaOps.Concelier.Connector.Kisa.Configuration;
using StellaOps.Concelier.Connector.Kisa.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
using StellaOps.Plugin;

View File

@@ -10,7 +10,6 @@ using StellaOps.Concelier.Normalization.Identifiers;
using StellaOps.Concelier.Normalization.Text;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
namespace StellaOps.Concelier.Connector.Osv.Internal;

View File

@@ -17,8 +17,6 @@ using StellaOps.Concelier.Connector.Ru.Bdu.Configuration;
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Plugin;
using StellaOps.Cryptography;

View File

@@ -18,8 +18,6 @@ using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration;
using StellaOps.Concelier.Connector.Ru.Nkcki.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Plugin;
using StellaOps.Cryptography;

View File

@@ -14,8 +14,6 @@ using StellaOps.Concelier.Connector.StellaOpsMirror.Settings;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Plugin;
using StellaOps.Cryptography;

View File

@@ -18,8 +18,6 @@ using StellaOps.Concelier.Connector.Vndr.Adobe.Configuration;
using StellaOps.Concelier.Connector.Vndr.Adobe.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.PsirtFlags;
using StellaOps.Concelier.Models;
using StellaOps.Cryptography;

View File

@@ -16,8 +16,6 @@ using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Vndr.Apple.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.PsirtFlags;
using StellaOps.Cryptography;
using StellaOps.Plugin;

View File

@@ -118,23 +118,24 @@ internal static class AppleIndexParser
return entries.Count == 0 ? Array.Empty<AppleIndexEntry>() : entries;
}
private static bool TryResolveDetailUri(AppleIndexEntryDto dto, Uri baseUri, out Uri uri)
private static bool TryResolveDetailUri(AppleIndexEntryDto dto, Uri baseUri, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Uri? uri)
{
if (!string.IsNullOrWhiteSpace(dto.DetailUrl) && Uri.TryCreate(dto.DetailUrl, UriKind.Absolute, out uri))
if (!string.IsNullOrWhiteSpace(dto.DetailUrl) && Uri.TryCreate(dto.DetailUrl, UriKind.Absolute, out var parsedUri))
{
uri = parsedUri;
return true;
}
if (string.IsNullOrWhiteSpace(dto.ArticleId))
{
uri = default!;
uri = null;
return false;
}
var article = dto.ArticleId.Trim();
if (article.Length == 0)
{
uri = default!;
uri = null;
return false;
}

View File

@@ -5,7 +5,6 @@ using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Packages;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.PsirtFlags;
namespace StellaOps.Concelier.Connector.Vndr.Apple.Internal;

View File

@@ -14,8 +14,6 @@ using StellaOps.Concelier.Connector.Vndr.Chromium.Configuration;
using StellaOps.Concelier.Connector.Vndr.Chromium.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.PsirtFlags;
using StellaOps.Cryptography;
using StellaOps.Plugin;

View File

@@ -69,7 +69,7 @@ internal sealed record ChromiumCursor(
public ChromiumCursor WithFetchCache(IDictionary<string, ChromiumFetchCacheEntry> cache)
=> this with { FetchCache = cache is null ? new Dictionary<string, ChromiumFetchCacheEntry>(StringComparer.Ordinal) : new Dictionary<string, ChromiumFetchCacheEntry>(cache, StringComparer.Ordinal) };
public bool TryGetFetchCache(string key, out ChromiumFetchCacheEntry entry)
public bool TryGetFetchCache(string key, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out ChromiumFetchCacheEntry? entry)
=> FetchCache.TryGetValue(key, out entry);
private static DateTimeOffset? ReadDateTime(DocumentValue value)

View File

@@ -12,8 +12,6 @@ using StellaOps.Concelier.Connector.Vndr.Cisco.Configuration;
using StellaOps.Concelier.Connector.Vndr.Cisco.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
using StellaOps.Plugin;

View File

@@ -4,7 +4,6 @@ using System.Linq;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common.Packages;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Normalization.SemVer;
namespace StellaOps.Concelier.Connector.Vndr.Cisco.Internal;

View File

@@ -17,8 +17,6 @@ using StellaOps.Concelier.Connector.Vndr.Msrc.Configuration;
using StellaOps.Concelier.Connector.Vndr.Msrc.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
using StellaOps.Plugin;

View File

@@ -5,7 +5,6 @@ using System.Text.RegularExpressions;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common.Packages;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.PsirtFlags;
namespace StellaOps.Concelier.Connector.Vndr.Oracle.Internal;

View File

@@ -5,7 +5,6 @@ using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Packages;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.PsirtFlags;
namespace StellaOps.Concelier.Connector.Vndr.Vmware.Internal;

View File

@@ -3,6 +3,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Npgsql;
using NpgsqlTypes;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Persistence.Postgres.Models;
using StellaOps.Infrastructure.Postgres.Repositories;
@@ -127,8 +128,14 @@ public sealed class AdvisoryLinksetCacheRepository
if (cursor is null)
{
AddParameter(cmd, "cursor_created_at", DBNull.Value);
AddParameter(cmd, "cursor_advisory_id", DBNull.Value);
cmd.Parameters.Add(new NpgsqlParameter("cursor_created_at", NpgsqlDbType.TimestampTz)
{
Value = DBNull.Value
});
cmd.Parameters.Add(new NpgsqlParameter("cursor_advisory_id", NpgsqlDbType.Text)
{
Value = DBNull.Value
});
}
else
{

View File

@@ -22,6 +22,9 @@ namespace StellaOps.Concelier.ConfigDiff.Tests;
[Trait("BlastRadius", TestCategories.BlastRadius.Advisories)]
public class ConcelierConfigDiffTests : ConfigDiffTestBase
{
private static readonly DateTimeOffset SnapshotTimestamp =
new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
/// <summary>
/// Initializes a new instance of the <see cref="ConcelierConfigDiffTests"/> class.
/// </summary>
@@ -61,7 +64,8 @@ public class ConcelierConfigDiffTests : ConfigDiffTestBase
async config => await GetDownloadBehaviorAsync(config),
async config => await GetRetryBehaviorAsync(config),
async config => await GetParseBehaviorAsync(config)
]);
],
ct: TestContext.Current.CancellationToken);
// Assert
result.IsSuccess.Should().BeTrue(
@@ -93,7 +97,8 @@ public class ConcelierConfigDiffTests : ConfigDiffTestBase
changedConfig,
getBehavior: async config => await CaptureRetryBehaviorAsync(config),
computeDelta: ComputeBehaviorSnapshotDelta,
expectedDelta: expectedDelta);
expectedDelta: expectedDelta,
ct: TestContext.Current.CancellationToken);
// Assert
result.IsSuccess.Should().BeTrue(
@@ -120,7 +125,8 @@ public class ConcelierConfigDiffTests : ConfigDiffTestBase
async config => await GetCacheBehaviorAsync(config),
async config => await GetRetryBehaviorAsync(config),
async config => await GetParseBehaviorAsync(config)
]);
],
ct: TestContext.Current.CancellationToken);
// Assert
result.IsSuccess.Should().BeTrue(
@@ -152,7 +158,8 @@ public class ConcelierConfigDiffTests : ConfigDiffTestBase
changedConfig,
getBehavior: async config => await CaptureValidationBehaviorAsync(config),
computeDelta: ComputeBehaviorSnapshotDelta,
expectedDelta: expectedDelta);
expectedDelta: expectedDelta,
ct: TestContext.Current.CancellationToken);
// Assert
result.IsSuccess.Should().BeTrue();
@@ -186,11 +193,11 @@ public class ConcelierConfigDiffTests : ConfigDiffTestBase
ConfigurationId: $"retry-{config.RetryCount}",
Behaviors:
[
new CapturedBehavior("MaxRetryAttempts", config.RetryCount.ToString(), DateTimeOffset.UtcNow),
new CapturedBehavior("MaxRetryAttempts", config.RetryCount.ToString(), SnapshotTimestamp),
new CapturedBehavior("FailureRecoveryWindow",
config.RetryCount > 3 ? "increase" : "standard", DateTimeOffset.UtcNow)
config.RetryCount > 3 ? "increase" : "standard", SnapshotTimestamp)
],
CapturedAt: DateTimeOffset.UtcNow);
CapturedAt: SnapshotTimestamp);
return Task.FromResult(snapshot);
}
@@ -202,11 +209,11 @@ public class ConcelierConfigDiffTests : ConfigDiffTestBase
Behaviors:
[
new CapturedBehavior("ValidationStrictness",
config.StrictValidation ? "strict" : "relaxed", DateTimeOffset.UtcNow),
config.StrictValidation ? "strict" : "relaxed", SnapshotTimestamp),
new CapturedBehavior("RejectionRate",
config.StrictValidation ? "increase" : "standard", DateTimeOffset.UtcNow)
config.StrictValidation ? "increase" : "standard", SnapshotTimestamp)
],
CapturedAt: DateTimeOffset.UtcNow);
CapturedAt: SnapshotTimestamp);
return Task.FromResult(snapshot);
}

View File

@@ -51,9 +51,10 @@ public sealed class AcscConnectorFetchTests
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString);
var stateValue = state!;
Assert.Equal("Direct", stateValue.Cursor.GetValue("preferredEndpoint").AsString);
var feeds = state.Cursor.GetValue("feeds").AsDocumentObject;
var feeds = stateValue.Cursor.GetValue("feeds").AsDocumentObject;
Assert.True(feeds.TryGetValue("alerts", out var published));
Assert.Equal(DateTime.Parse("2025-10-11T05:30:00Z").ToUniversalTime(), published.ToUniversalTime());
@@ -90,9 +91,10 @@ public sealed class AcscConnectorFetchTests
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal("Relay", state!.Cursor.GetValue("preferredEndpoint").AsString);
var stateValue = state!;
Assert.Equal("Relay", stateValue.Cursor.GetValue("preferredEndpoint").AsString);
var feeds = state.Cursor.GetValue("feeds").AsDocumentObject;
var feeds = stateValue.Cursor.GetValue("feeds").AsDocumentObject;
Assert.True(feeds.TryGetValue("alerts", out var published));
Assert.Equal(DateTime.Parse("2025-10-11T00:00:00Z").ToUniversalTime(), published.ToUniversalTime());
@@ -142,7 +144,8 @@ public sealed class AcscConnectorFetchTests
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString);
var stateValue = state!;
Assert.Equal("Direct", stateValue.Cursor.GetValue("preferredEndpoint").AsString);
Assert.Collection(harness.Handler.Requests,
request =>
@@ -175,7 +178,8 @@ public sealed class AcscConnectorFetchTests
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString);
var stateValue = state!;
Assert.Equal("Direct", stateValue.Cursor.GetValue("preferredEndpoint").AsString);
Assert.Empty(harness.Handler.Requests);
}

View File

@@ -80,8 +80,9 @@ public sealed class AcscConnectorParseTests
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.DoesNotContain(document.Id.ToString(), state!.Cursor.GetValue("pendingDocuments").AsDocumentArray.Select(v => v.AsString));
Assert.Contains(document.Id.ToString(), state.Cursor.GetValue("pendingMappings").AsDocumentArray.Select(v => v.AsString));
var stateValue = state!;
Assert.DoesNotContain(document.Id.ToString(), stateValue.Cursor.GetValue("pendingDocuments").AsDocumentArray.Select(v => v.AsString));
Assert.Contains(document.Id.ToString(), stateValue.Cursor.GetValue("pendingMappings").AsDocumentArray.Select(v => v.AsString));
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
@@ -103,7 +104,8 @@ public sealed class AcscConnectorParseTests
state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.GetValue("pendingMappings").AsDocumentArray.Count == 0);
stateValue = state!;
Assert.True(stateValue.Cursor.GetValue("pendingMappings").AsDocumentArray.Count == 0);
}
[Fact]

View File

@@ -18,7 +18,6 @@ using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common.Cursors;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Testing;
using Xunit;
@@ -110,9 +109,10 @@ public sealed class CertCcConnectorFetchTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var stateValue = state!;
DocumentValue summaryValue;
Assert.True(state!.Cursor.TryGetValue("summary", out summaryValue));
Assert.True(stateValue.Cursor.TryGetValue("summary", out summaryValue));
var summaryDocument = Assert.IsType<DocumentObject>(summaryValue);
Assert.True(summaryDocument.TryGetValue("start", out _));
Assert.True(summaryDocument.TryGetValue("end", out _));

View File

@@ -23,7 +23,6 @@ using StellaOps.Concelier.Connector.Common.Cursors;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Testing;
using Xunit;

View File

@@ -22,7 +22,6 @@ using StellaOps.Concelier.Connector.Common.Cursors;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Testing;
using Xunit;
@@ -135,8 +134,9 @@ public sealed class CertCcConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
state.Should().NotBeNull();
state!.Cursor.Should().NotBeNull();
var pendingNotesCount = state.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue)
var stateValue = state!;
stateValue.Cursor.Should().NotBeNull();
var pendingNotesCount = stateValue.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue)
? pendingNotesValue!.AsDocumentArray.Count
: 0;
pendingNotesCount.Should().Be(0);
@@ -216,10 +216,11 @@ public sealed class CertCcConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
state.Should().NotBeNull();
state!.FailCount.Should().BeGreaterThan(0);
state.BackoffUntil.Should().NotBeNull();
state.BackoffUntil.Should().BeAfter(_timeProvider.GetUtcNow());
state.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue).Should().BeTrue();
var stateValue = state!;
stateValue.FailCount.Should().BeGreaterThan(0);
stateValue.BackoffUntil.Should().NotBeNull();
stateValue.BackoffUntil.Should().BeAfter(_timeProvider.GetUtcNow());
stateValue.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue).Should().BeTrue();
pendingNotesValue!.AsDocumentArray.Should().Contain(value => value.AsString == "294418");
var pendingSummaries = state.Cursor.TryGetValue("pendingSummaries", out var pendingSummariesValue)
? pendingSummariesValue!.AsDocumentArray.Count
@@ -259,11 +260,12 @@ public sealed class CertCcConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
state.Should().NotBeNull();
state!.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue).Should().BeTrue();
var stateValue = state!;
stateValue.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue).Should().BeTrue();
pendingNotesValue!.AsDocumentArray.Should().BeEmpty();
state.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue).Should().BeTrue();
stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue).Should().BeTrue();
pendingDocsValue!.AsDocumentArray.Should().BeEmpty();
state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue).Should().BeTrue();
stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue).Should().BeTrue();
pendingMappingsValue!.AsDocumentArray.Should().BeEmpty();
}
@@ -298,11 +300,12 @@ public sealed class CertCcConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
state.Should().NotBeNull();
var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
var stateValue = state!;
var pendingDocuments = stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
? pendingDocsValue!.AsDocumentArray.Count
: 0;
pendingDocuments.Should().BeGreaterThan(0);
var pendingMappings = state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue)
var pendingMappings = stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue)
? pendingMappingsValue!.AsDocumentArray.Count
: 0;
pendingMappings.Should().Be(0);

View File

@@ -4,7 +4,6 @@ using StellaOps.Concelier.Documents;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.CertCc.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using Xunit;
namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal;

View File

@@ -95,11 +95,12 @@ public sealed class CertFrConnectorTests
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal(1, state!.FailCount);
Assert.NotNull(state.LastFailureReason);
Assert.Contains("500", state.LastFailureReason, StringComparison.Ordinal);
Assert.NotNull(state.BackoffUntil);
Assert.True(state.BackoffUntil > harness.TimeProvider.GetUtcNow());
var stateValue = state!;
Assert.Equal(1, stateValue.FailCount);
Assert.NotNull(stateValue.LastFailureReason);
Assert.Contains("500", stateValue.LastFailureReason, StringComparison.Ordinal);
Assert.NotNull(stateValue.BackoffUntil);
Assert.True(stateValue.BackoffUntil > harness.TimeProvider.GetUtcNow());
}
[Fact]
@@ -139,8 +140,9 @@ public sealed class CertFrConnectorTests
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsDocumentArray.Count == 0);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsDocumentArray.Count == 0);
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsDocumentArray.Count == 0);
Assert.True(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsDocumentArray.Count == 0);
}
[Fact]
@@ -182,8 +184,9 @@ public sealed class CertFrConnectorTests
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsDocumentArray.Count == 0);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsDocumentArray.Count == 0);
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsDocumentArray.Count == 0);
Assert.True(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsDocumentArray.Count == 0);
}
private async Task<ConnectorTestHarness> BuildHarnessAsync()

View File

@@ -25,8 +25,6 @@ using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Testing;
namespace StellaOps.Concelier.Connector.CertIn.Tests;
@@ -100,7 +98,8 @@ public sealed class CertInConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertInConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pending));
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pending));
Assert.Empty(pending.AsDocumentArray);
}
@@ -131,11 +130,12 @@ public sealed class CertInConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertInConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal(1, state!.FailCount);
Assert.NotNull(state.LastFailureReason);
Assert.Contains("500", state.LastFailureReason, StringComparison.Ordinal);
Assert.True(state.BackoffUntil.HasValue);
Assert.True(state.BackoffUntil!.Value > _timeProvider.GetUtcNow());
var stateValue = state!;
Assert.Equal(1, stateValue.FailCount);
Assert.NotNull(stateValue.LastFailureReason);
Assert.Contains("500", stateValue.LastFailureReason, StringComparison.Ordinal);
Assert.True(stateValue.BackoffUntil.HasValue);
Assert.True(stateValue.BackoffUntil!.Value > _timeProvider.GetUtcNow());
}
[Fact]
@@ -209,9 +209,9 @@ public sealed class CertInConnectorTests : IAsyncLifetime
var state = await stateRepository.TryGetAsync(CertInConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs));
Assert.Equal(0, pendingDocs.AsDocumentArray.Count);
Assert.Empty(pendingDocs.AsDocumentArray);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings));
Assert.Equal(0, pendingMappings.AsDocumentArray.Count);
Assert.Empty(pendingMappings.AsDocumentArray);
}
[Fact]
@@ -263,10 +263,11 @@ public sealed class CertInConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertInConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs));
Assert.Equal(0, pendingDocs.AsDocumentArray.Count);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings));
Assert.Equal(0, pendingMappings.AsDocumentArray.Count);
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs));
Assert.Empty(pendingDocs.AsDocumentArray);
Assert.True(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMappings));
Assert.Empty(pendingMappings.AsDocumentArray);
}
private async Task EnsureServiceProviderAsync(CertInOptions template)

View File

@@ -14,7 +14,6 @@ using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.Connector.Common.Tests;

View File

@@ -10,7 +10,6 @@ using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.State;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.Connector.Common.Tests;
@@ -105,9 +104,10 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
var state = await _stateRepository.TryGetAsync("vndr.test", CancellationToken.None);
Assert.NotNull(state);
Assert.Equal(_timeProvider.GetUtcNow().UtcDateTime, state!.LastSuccess);
var stateValue = state!;
Assert.Equal(_timeProvider.GetUtcNow().UtcDateTime, stateValue.LastSuccess);
var cursor = state.Cursor;
var cursor = stateValue.Cursor;
var pendingDocs = cursor["pendingDocuments"].AsDocumentArray.Select(v => Guid.Parse(v.AsString)).ToList();
Assert.Contains(documentId, pendingDocs);

View File

@@ -6,6 +6,8 @@
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<!-- This is a shared test library, not a standalone test project - disable ConcelierTestInfra to avoid duplicate types (CS0436) when referenced by actual test projects -->
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />

View File

@@ -17,8 +17,6 @@ using StellaOps.Concelier.Connector.Cve.Internal;
using StellaOps.Concelier.Testing;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
namespace StellaOps.Concelier.Connector.Cve.Tests;

View File

@@ -56,9 +56,10 @@ public sealed class AlpineConnectorTests
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(AlpineConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
&& pendingDocs.AsDocumentArray.Count == 0);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings)
Assert.True(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMappings)
&& pendingMappings.AsDocumentArray.Count == 0);
harness.Handler.AssertNoPendingResponses();

View File

@@ -22,8 +22,6 @@ using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Distro.Debian.Configuration;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Testing;
using Xunit;

View File

@@ -107,8 +107,9 @@ public sealed class RedHatConnectorHarnessTests : IAsyncLifetime
var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsDocumentArray.Count == 0);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings) && pendingMappings.AsDocumentArray.Count == 0);
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsDocumentArray.Count == 0);
Assert.True(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMappings) && pendingMappings.AsDocumentArray.Count == 0);
}
public ValueTask InitializeAsync() => ValueTask.CompletedTask;

View File

@@ -25,8 +25,6 @@ using StellaOps.Concelier.Connector.Distro.RedHat.Internal;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Testing;
using StellaOps.Plugin;
@@ -170,8 +168,9 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs2) && pendingDocs2.AsDocumentArray.Count == 0);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings2) && pendingMappings2.AsDocumentArray.Count == 0);
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs2) && pendingDocs2.AsDocumentArray.Count == 0);
Assert.True(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMappings2) && pendingMappings2.AsDocumentArray.Count == 0);
const string fetchKind = "source:redhat:fetch";
const string parseKind = "source:redhat:parse";
@@ -241,8 +240,9 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs3) && pendingDocs3.AsDocumentArray.Count == 0);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings3) && pendingMappings3.AsDocumentArray.Count == 0);
stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs3) && pendingDocs3.AsDocumentArray.Count == 0);
Assert.True(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMappings3) && pendingMappings3.AsDocumentArray.Count == 0);
}
[Fact]
@@ -338,7 +338,8 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pendingDocs = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
var stateValue = state!;
var pendingDocs = stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
? pendingDocsValue.AsDocumentArray
: new DocumentArray();
Assert.NotEmpty(pendingDocs);
@@ -368,9 +369,10 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
var stateRepository = resumeProvider.GetRequiredService<ISourceStateRepository>();
var finalState = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(finalState);
var finalPendingDocs = finalState!.Cursor.TryGetValue("pendingDocuments", out var docsValue) ? docsValue.AsDocumentArray : new DocumentArray();
var finalStateValue = finalState!;
var finalPendingDocs = finalStateValue.Cursor.TryGetValue("pendingDocuments", out var docsValue) ? docsValue.AsDocumentArray : new DocumentArray();
Assert.Empty(finalPendingDocs);
var finalPendingMappings = finalState.Cursor.TryGetValue("pendingMappings", out var mappingsValue) ? mappingsValue.AsDocumentArray : new DocumentArray();
var finalPendingMappings = finalStateValue.Cursor.TryGetValue("pendingMappings", out var mappingsValue) ? mappingsValue.AsDocumentArray : new DocumentArray();
Assert.Empty(finalPendingMappings);
}
}

View File

@@ -142,10 +142,11 @@ public sealed class GhsaConnectorTests : IAsyncLifetime
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(GhsaConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var stateValue = state!;
Assert.True(state!.Cursor.TryGetValue("currentWindowStart", out var startValue));
Assert.True(state.Cursor.TryGetValue("currentWindowEnd", out var endValue));
Assert.True(state.Cursor.TryGetValue("nextPage", out var nextPageValue));
Assert.True(stateValue.Cursor.TryGetValue("currentWindowStart", out var startValue));
Assert.True(stateValue.Cursor.TryGetValue("currentWindowEnd", out var endValue));
Assert.True(stateValue.Cursor.TryGetValue("nextPage", out var nextPageValue));
Assert.Equal(since.UtcDateTime, startValue.ToUniversalTime());
Assert.Equal(until.UtcDateTime, endValue.ToUniversalTime());

View File

@@ -70,7 +70,7 @@ public sealed class IcsCisaConnectorTests
var icsma = Assert.Single(advisories, advisory => advisory.AdvisoryKey == "ICSMA-25-045-01");
Assert.Contains("CVE-2025-11111", icsma.Aliases);
var icsmaMitigation = Assert.Single(icsma.References.Where(reference => reference.Kind == "mitigation"));
var icsmaMitigation = Assert.Single(icsma.References, reference => reference.Kind == "mitigation");
Assert.Contains("Contact HealthTech support", icsmaMitigation.Summary, StringComparison.Ordinal);
Assert.Contains(icsma.References, reference => reference.Url == "https://www.cisa.gov/sites/default/files/2025-10/ICSMA-25-045-01_Supplement.pdf");
var infusionPackage = Assert.Single(icsma.AffectedPackages, package => string.Equals(package.Identifier, "InfusionManager", StringComparison.OrdinalIgnoreCase));

View File

@@ -21,8 +21,6 @@ using StellaOps.Concelier.Connector.Ics.Kaspersky;
using StellaOps.Concelier.Connector.Ics.Kaspersky.Configuration;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Testing;
@@ -97,7 +95,8 @@ public sealed class KasperskyConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(KasperskyConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pending)
var stateValue = state!;
var pendingDocuments = stateValue.Cursor.TryGetValue("pendingDocuments", out var pending)
? pending.AsDocumentArray
: new DocumentArray();
Assert.Empty(pendingDocuments);
@@ -130,11 +129,12 @@ public sealed class KasperskyConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(KasperskyConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal(1, state!.FailCount);
Assert.NotNull(state.LastFailureReason);
Assert.Contains("500", state.LastFailureReason, StringComparison.Ordinal);
Assert.True(state.BackoffUntil.HasValue);
Assert.True(state.BackoffUntil!.Value > _timeProvider.GetUtcNow());
var stateValue = state!;
Assert.Equal(1, stateValue.FailCount);
Assert.NotNull(stateValue.LastFailureReason);
Assert.Contains("500", stateValue.LastFailureReason, StringComparison.Ordinal);
Assert.True(stateValue.BackoffUntil.HasValue);
Assert.True(stateValue.BackoffUntil!.Value > _timeProvider.GetUtcNow());
}
[Fact]
@@ -202,10 +202,11 @@ public sealed class KasperskyConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(KasperskyConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs));
Assert.Equal(0, pendingDocs.AsDocumentArray.Count);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings));
Assert.Equal(0, pendingMappings.AsDocumentArray.Count);
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs));
Assert.Empty(pendingDocs.AsDocumentArray);
Assert.True(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMappings));
Assert.Empty(pendingMappings.AsDocumentArray);
}
[Fact]

View File

@@ -20,8 +20,6 @@ using StellaOps.Concelier.Connector.Jvn;
using StellaOps.Concelier.Connector.Jvn.Configuration;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.JpFlags;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Testing;
@@ -123,7 +121,8 @@ public sealed class JvnConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(JvnConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs));
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs));
Assert.Empty(pendingDocs.AsDocumentArray);
}

View File

@@ -71,10 +71,11 @@ public sealed class KevConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(KevConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal("2025.10.09", state!.Cursor.TryGetValue("catalogVersion", out var versionValue) ? versionValue.AsString : null);
Assert.True(state.Cursor.TryGetValue("catalogReleased", out var releasedValue) && releasedValue.DocumentType is DocumentType.DateTime);
Assert.True(IsEmptyArray(state.Cursor, "pendingDocuments"));
Assert.True(IsEmptyArray(state.Cursor, "pendingMappings"));
var stateValue = state!;
Assert.Equal("2025.10.09", stateValue.Cursor.TryGetValue("catalogVersion", out var versionValue) ? versionValue.AsString : null);
Assert.True(stateValue.Cursor.TryGetValue("catalogReleased", out var releasedValue) && releasedValue.DocumentType is DocumentType.DateTime);
Assert.True(IsEmptyArray(stateValue.Cursor, "pendingDocuments"));
Assert.True(IsEmptyArray(stateValue.Cursor, "pendingMappings"));
}
private async Task<ServiceProvider> BuildServiceProviderAsync()

View File

@@ -23,8 +23,6 @@ using StellaOps.Concelier.Connector.Kisa.Internal;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Testing;
using Xunit;

View File

@@ -10,7 +10,6 @@ using StellaOps.Concelier.Connector.Nvd;
using StellaOps.Concelier.Connector.Nvd.Configuration;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Testing;
using StellaOps.Concelier.Testing;
using System.Net;
@@ -74,15 +73,18 @@ public sealed class NvdConnectorHarnessTests : IAsyncLifetime
var firstDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, firstUri.ToString(), CancellationToken.None);
Assert.NotNull(firstDocument);
Assert.Equal("0", firstDocument!.Metadata["startIndex"]);
Assert.NotNull(firstDocument!.Metadata);
Assert.Equal("0", firstDocument.Metadata["startIndex"]);
var secondDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, secondUri.ToString(), CancellationToken.None);
Assert.NotNull(secondDocument);
Assert.Equal("2", secondDocument!.Metadata["startIndex"]);
Assert.NotNull(secondDocument!.Metadata);
Assert.Equal("2", secondDocument.Metadata["startIndex"]);
var thirdDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, thirdUri.ToString(), CancellationToken.None);
Assert.NotNull(thirdDocument);
Assert.Equal("4", thirdDocument!.Metadata["startIndex"]);
Assert.NotNull(thirdDocument!.Metadata);
Assert.Equal("4", thirdDocument.Metadata["startIndex"]);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);

View File

@@ -22,8 +22,6 @@ using StellaOps.Concelier.Connector.Nvd.Configuration;
using StellaOps.Concelier.Connector.Nvd.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.ChangeHistory;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Testing;
@@ -146,7 +144,8 @@ public sealed class NvdConnectorTests : IAsyncLifetime
var documentStore = provider.GetRequiredService<IDocumentStore>();
var finalState = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(finalState);
var pendingDocuments = finalState!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
var finalStateValue = finalState!;
var pendingDocuments = finalStateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
? pendingDocs.AsDocumentArray
: new DocumentArray();
Assert.Empty(pendingDocuments);
@@ -188,7 +187,8 @@ public sealed class NvdConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
var stateValue = state!;
var pendingDocuments = stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
? pendingDocs.AsDocumentArray.Select(v => Guid.Parse(v.AsString)).ToArray()
: Array.Empty<Guid>();
Assert.Equal(3, pendingDocuments.Length);
@@ -280,8 +280,9 @@ public sealed class NvdConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var stateValue = state!;
var cursorDocument = state!.Cursor;
var cursorDocument = stateValue.Cursor;
var lastWindowEnd = cursorDocument.TryGetValue("windowEnd", out var endValue) ? ReadDateTime(endValue) : (DateTimeOffset?)null;
var startCandidate = (lastWindowEnd ?? windowEnd) - options.WindowOverlap;
var backfillLimit = now - options.InitialBackfill;
@@ -350,9 +351,10 @@ public sealed class NvdConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pendingDocs = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue) ? pendingDocsValue.AsDocumentArray : new DocumentArray();
var stateValue = state!;
var pendingDocs = stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue) ? pendingDocsValue.AsDocumentArray : new DocumentArray();
Assert.Empty(pendingDocs);
var pendingMappings = state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue) ? pendingMappingsValue.AsDocumentArray : new DocumentArray();
var pendingMappings = stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue) ? pendingMappingsValue.AsDocumentArray : new DocumentArray();
Assert.Empty(pendingMappings);
Assert.Equal(1, collector.GetValue("nvd.fetch.documents"));
@@ -462,7 +464,8 @@ public sealed class NvdConnectorTests : IAsyncLifetime
var stateRepository = fetchProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pending = state!.Cursor.TryGetValue("pendingDocuments", out var value)
var stateValue = state!;
var pending = stateValue.Cursor.TryGetValue("pendingDocuments", out var value)
? value.AsDocumentArray
: new DocumentArray();
Assert.NotEmpty(pending);
@@ -492,7 +495,8 @@ public sealed class NvdConnectorTests : IAsyncLifetime
var stateRepository = resumeProvider.GetRequiredService<ISourceStateRepository>();
var finalState = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(finalState);
var cursor = finalState!.Cursor;
var finalStateValue = finalState!;
var cursor = finalStateValue.Cursor;
var finalPendingDocs = cursor.TryGetValue("pendingDocuments", out var pendingDocs) ? pendingDocs.AsDocumentArray : new DocumentArray();
Assert.Empty(finalPendingDocs);
var finalPendingMappings = cursor.TryGetValue("pendingMappings", out var pendingMappings) ? pendingMappings.AsDocumentArray : new DocumentArray();

View File

@@ -3,7 +3,6 @@ using StellaOps.Concelier.Documents;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Osv.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
namespace StellaOps.Concelier.Connector.Osv.Tests;

View File

@@ -14,7 +14,6 @@ using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Osv;
using StellaOps.Concelier.Connector.Osv.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
using Xunit;

View File

@@ -10,7 +10,6 @@ using StellaOps.Concelier.Connector.Osv;
using StellaOps.Concelier.Connector.Osv.Internal;
using StellaOps.Concelier.Normalization.Identifiers;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using Xunit;
namespace StellaOps.Concelier.Connector.Osv.Tests;

View File

@@ -7,7 +7,6 @@ using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Osv;
using StellaOps.Concelier.Connector.Osv.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Connector.Common;
using Xunit;

View File

@@ -21,8 +21,6 @@ using StellaOps.Concelier.Connector.Ru.Bdu.Configuration;
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Testing;
using StellaOps.Cryptography;
using Xunit;

View File

@@ -21,7 +21,6 @@ using StellaOps.Concelier.Connector.Ru.Nkcki;
using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Testing;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Persistence.Postgres;

View File

@@ -20,8 +20,6 @@ using StellaOps.Concelier.Connector.StellaOpsMirror.Internal;
using StellaOps.Concelier.Connector.StellaOpsMirror.Settings;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Testing;
using StellaOps.Cryptography;

View File

@@ -23,8 +23,6 @@ using StellaOps.Concelier.Connector.Vndr.Adobe;
using StellaOps.Concelier.Connector.Vndr.Adobe.Configuration;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.PsirtFlags;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Testing;
@@ -132,7 +130,8 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime
var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor;
var stateValue = state!;
var cursor = stateValue.Cursor;
Assert.True(!cursor.TryGetValue("pendingDocuments", out _)
|| cursor.GetValue("pendingDocuments").AsDocumentArray.Count == 0);
Assert.True(!cursor.TryGetValue("pendingMappings", out _)
@@ -314,8 +313,9 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsDocumentArray.Count == 0);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMap) && pendingMap.AsDocumentArray.Count == 0);
var stateValue = state!;
Assert.True(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsDocumentArray.Count == 0);
Assert.True(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMap) && pendingMap.AsDocumentArray.Count == 0);
}
private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler)

View File

@@ -19,7 +19,6 @@ using StellaOps.Concelier.Connector.Vndr.Chromium;
using StellaOps.Concelier.Connector.Vndr.Chromium.Configuration;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.PsirtFlags;
using StellaOps.Concelier.Testing;
@@ -137,7 +136,8 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(VndrChromiumConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
var stateValue = state!;
var pendingDocuments = stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
? pendingDocsValue.AsDocumentArray
: new DocumentArray();
Assert.Empty(pendingDocuments);
@@ -167,7 +167,8 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime
var stateRepository = fetchProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(VndrChromiumConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
var stateValue = state!;
var pendingDocuments = stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
? pendingDocsValue.AsDocumentArray
: new DocumentArray();
Assert.NotEmpty(pendingDocuments);
@@ -180,7 +181,8 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime
var stateRepositoryBefore = resumeProvider.GetRequiredService<ISourceStateRepository>();
var resumeState = await stateRepositoryBefore.TryGetAsync(VndrChromiumConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(resumeState);
var resumePendingDocs = resumeState!.Cursor.TryGetValue("pendingDocuments", out var resumePendingValue)
var resumeStateValue = resumeState!;
var resumePendingDocs = resumeStateValue.Cursor.TryGetValue("pendingDocuments", out var resumePendingValue)
? resumePendingValue.AsDocumentArray
: new DocumentArray();
Assert.Equal(pendingDocumentIds.Length, resumePendingDocs.Count);
@@ -202,12 +204,13 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime
var stateRepositoryAfter = resumeProvider.GetRequiredService<ISourceStateRepository>();
var finalState = await stateRepositoryAfter.TryGetAsync(VndrChromiumConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(finalState);
var finalPending = finalState!.Cursor.TryGetValue("pendingDocuments", out var finalPendingDocs)
var finalStateValue = finalState!;
var finalPending = finalStateValue.Cursor.TryGetValue("pendingDocuments", out var finalPendingDocs)
? finalPendingDocs.AsDocumentArray
: new DocumentArray();
Assert.Empty(finalPending);
var finalPendingMappings = finalState.Cursor.TryGetValue("pendingMappings", out var finalPendingMappingsValue)
var finalPendingMappings = finalStateValue.Cursor.TryGetValue("pendingMappings", out var finalPendingMappingsValue)
? finalPendingMappingsValue.AsDocumentArray
: new DocumentArray();
Assert.Empty(finalPendingMappings);
@@ -246,7 +249,8 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(VndrChromiumConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor;
var stateValue = state!;
var cursor = stateValue.Cursor;
var pendingDocuments = cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
? pendingDocsValue.AsDocumentArray
: new DocumentArray();

View File

@@ -8,7 +8,6 @@ using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Vndr.Cisco;
using StellaOps.Concelier.Connector.Vndr.Cisco.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using Xunit;
using StellaOps.TestKit;

View File

@@ -18,8 +18,6 @@ using StellaOps.Concelier.Connector.Vndr.Msrc.Configuration;
using StellaOps.Concelier.Connector.Vndr.Msrc.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Testing;
using Xunit;
@@ -74,7 +72,8 @@ public sealed class MsrcConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(MsrcConnectorPlugin.SourceName, CancellationToken.None);
state.Should().NotBeNull();
state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
var stateValue = state!;
stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
pendingDocs!.AsDocumentArray.Should().BeEmpty();
var documentStore = provider.GetRequiredService<IDocumentStore>();

View File

@@ -24,8 +24,6 @@ using StellaOps.Concelier.Connector.Vndr.Oracle.Configuration;
using StellaOps.Concelier.Connector.Vndr.Oracle.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Storage.PsirtFlags;
using StellaOps.Concelier.Testing;

View File

@@ -23,8 +23,6 @@ using StellaOps.Concelier.Connector.Vndr.Vmware.Configuration;
using StellaOps.Concelier.Connector.Vndr.Vmware.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Storage.PsirtFlags;
using StellaOps.Concelier.Testing;
@@ -98,9 +96,10 @@ public sealed class VmwareConnectorTests : IAsyncLifetime
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(VmwareConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.Empty(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) ? pendingDocs.AsDocumentArray : new DocumentArray());
Assert.Empty(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) ? pendingMaps.AsDocumentArray : new DocumentArray());
var cursorSnapshot = VmwareCursor.FromDocument(state.Cursor);
var stateValue = state!;
Assert.Empty(stateValue.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) ? pendingDocs.AsDocumentArray : new DocumentArray());
Assert.Empty(stateValue.Cursor.TryGetValue("pendingMappings", out var pendingMaps) ? pendingMaps.AsDocumentArray : new DocumentArray());
var cursorSnapshot = VmwareCursor.FromDocument(stateValue.Cursor);
_output.WriteLine($"Initial fetch cache entries: {cursorSnapshot.FetchCache.Count}");
foreach (var entry in cursorSnapshot.FetchCache)
{

View File

@@ -7,7 +7,6 @@ using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Vndr.Vmware;
using StellaOps.Concelier.Connector.Vndr.Vmware.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage;
using Xunit;
namespace StellaOps.Concelier.Connector.Vndr.Vmware.Tests;

View File

@@ -24,7 +24,7 @@ public sealed class EvidenceBundleAttestationBuilderTests
manifestPath,
transparencyPath,
"git:test-sha"),
CancellationToken.None);
TestContext.Current.CancellationToken);
Assert.Equal("evidence-bundle-m0", claims.SubjectName);
Assert.StartsWith("sha256:", claims.SubjectDigest);
@@ -62,7 +62,7 @@ public sealed class EvidenceBundleAttestationBuilderTests
var builder = new EvidenceBundleAttestationBuilder();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
builder.BuildAsync(new EvidenceBundleAttestationRequest(tempTar, tempManifest, null, "git:test"), CancellationToken.None));
builder.BuildAsync(new EvidenceBundleAttestationRequest(tempTar, tempManifest, null, "git:test"), TestContext.Current.CancellationToken));
Assert.Contains("Tenant must be lowercase", ex.Message);
}
@@ -88,7 +88,7 @@ public sealed class EvidenceBundleAttestationBuilderTests
var builder = new EvidenceBundleAttestationBuilder();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
builder.BuildAsync(new EvidenceBundleAttestationRequest(tempTar, tempManifest, null, "git:test"), CancellationToken.None));
builder.BuildAsync(new EvidenceBundleAttestationRequest(tempTar, tempManifest, null, "git:test"), TestContext.Current.CancellationToken));
Assert.Contains("Tenant must be present", ex.Message);
}

View File

@@ -230,11 +230,11 @@ public sealed class CanonicalMergerTests
var reference = Assert.Single(result.Advisory.References);
Assert.Equal("https://example.com/path/resource?a=1&b=2", reference.Url);
var unionDecision = Assert.Single(result.Decisions.Where(decision => decision.Field == "references"));
var unionDecision = Assert.Single(result.Decisions, decision => decision.Field == "references");
Assert.Null(unionDecision.SelectedSource);
Assert.Equal("union", unionDecision.DecisionReason);
var itemDecision = Assert.Single(result.Decisions.Where(decision => decision.Field.StartsWith("references[", StringComparison.OrdinalIgnoreCase)));
var itemDecision = Assert.Single(result.Decisions, decision => decision.Field.StartsWith("references[", StringComparison.OrdinalIgnoreCase));
Assert.Equal("osv", itemDecision.SelectedSource);
Assert.Equal("freshness_override", itemDecision.DecisionReason);
Assert.Contains("https://example.com/path/resource?a=1&b=2", itemDecision.Field, StringComparison.OrdinalIgnoreCase);

View File

@@ -40,7 +40,7 @@ public sealed class VulnExplorerTelemetryTests
var service = new AdvisoryObservationQueryService(new TestObservationLookup(observations));
await service.QueryAsync(new AdvisoryObservationQueryOptions("tenant-a"), CancellationToken.None);
await service.QueryAsync(new AdvisoryObservationQueryOptions("tenant-a"), TestContext.Current.CancellationToken);
listener.Dispose();

View File

@@ -42,7 +42,7 @@ public sealed class AdvisoryEventLogTests
AsOf: DateTimeOffset.Parse("2025-10-03T00:00:00Z"),
InputDocumentIds: new[] { Guid.Parse("11111111-1111-1111-1111-111111111111") });
await log.AppendAsync(new AdvisoryEventAppendRequest(new[] { statementInput }), CancellationToken.None);
await log.AppendAsync(new AdvisoryEventAppendRequest(new[] { statementInput }), TestContext.Current.CancellationToken);
Assert.Single(repository.InsertedStatements);
var entry = repository.InsertedStatements.Single();
@@ -87,7 +87,7 @@ public sealed class AdvisoryEventLogTests
AsOf: DateTimeOffset.Parse("2025-11-10T12:00:00Z"),
InputDocumentIds: Array.Empty<Guid>());
await log.AppendAsync(new AdvisoryEventAppendRequest(new[] { statementInput }), CancellationToken.None);
await log.AppendAsync(new AdvisoryEventAppendRequest(new[] { statementInput }), TestContext.Current.CancellationToken);
var entry = Assert.Single(repository.InsertedStatements);
Assert.NotNull(entry.Provenance);
@@ -112,7 +112,7 @@ public sealed class AdvisoryEventLogTests
AsOf: DateTimeOffset.Parse("2025-10-04T00:00:00Z"),
StatementIds: new[] { Guid.Parse("22222222-2222-2222-2222-222222222222") });
await log.AppendAsync(new AdvisoryEventAppendRequest(Array.Empty<AdvisoryStatementInput>(), new[] { conflictInput }), CancellationToken.None);
await log.AppendAsync(new AdvisoryEventAppendRequest(Array.Empty<AdvisoryStatementInput>(), new[] { conflictInput }), TestContext.Current.CancellationToken);
Assert.Single(repository.InsertedConflicts);
var entry = repository.InsertedConflicts.Single();
@@ -147,7 +147,7 @@ public sealed class AdvisoryEventLogTests
await log.AppendAsync(
new AdvisoryEventAppendRequest(Array.Empty<AdvisoryStatementInput>(), new[] { conflictInput }),
CancellationToken.None);
TestContext.Current.CancellationToken);
var entry = Assert.Single(repository.InsertedConflicts);
Assert.Equal(
@@ -225,7 +225,7 @@ public sealed class AdvisoryEventLogTests
RecordedAt: DateTimeOffset.Parse("2025-10-04T03:00:00Z"),
StatementIds: ImmutableArray<Guid>.Empty));
var replay = await log.ReplayAsync("CVE-2025-0001", asOf: null, CancellationToken.None);
var replay = await log.ReplayAsync("CVE-2025-0001", asOf: null, TestContext.Current.CancellationToken);
Assert.Equal("cve-2025-0001", replay.VulnerabilityKey);
Assert.Collection(

View File

@@ -49,7 +49,7 @@ public sealed class JobCoordinatorTests
new TestTimeProvider(),
diagnostics);
var result = await coordinator.TriggerAsync(definition.Kind, new Dictionary<string, object?> { ["foo"] = "bar" }, "unit-test", CancellationToken.None);
var result = await coordinator.TriggerAsync(definition.Kind, new Dictionary<string, object?> { ["foo"] = "bar" }, "unit-test", TestContext.Current.CancellationToken);
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
@@ -100,7 +100,7 @@ public sealed class JobCoordinatorTests
new TestTimeProvider(),
diagnostics);
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", TestContext.Current.CancellationToken);
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
@@ -151,7 +151,7 @@ public sealed class JobCoordinatorTests
new TestTimeProvider(),
diagnostics);
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", TestContext.Current.CancellationToken);
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(6));
@@ -195,7 +195,7 @@ public sealed class JobCoordinatorTests
new TestTimeProvider(),
diagnostics);
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", TestContext.Current.CancellationToken);
Assert.Equal(JobTriggerOutcome.AlreadyRunning, result.Outcome);
Assert.False(jobStore.CreatedRuns.Any());
@@ -237,7 +237,7 @@ public sealed class JobCoordinatorTests
["bad"] = new object(),
};
var result = await coordinator.TriggerAsync(definition.Kind, parameters, "unit-test", CancellationToken.None);
var result = await coordinator.TriggerAsync(definition.Kind, parameters, "unit-test", TestContext.Current.CancellationToken);
Assert.Equal(JobTriggerOutcome.InvalidParameters, result.Outcome);
Assert.Contains("unsupported", result.ErrorMessage, StringComparison.OrdinalIgnoreCase);
@@ -280,7 +280,7 @@ public sealed class JobCoordinatorTests
new TestTimeProvider(),
diagnostics);
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", TestContext.Current.CancellationToken);
Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));

View File

@@ -32,7 +32,7 @@ public sealed class AdvisoryLinksetQueryServiceTests
var lookup = new FakeLinksetLookup(linksets);
var service = new AdvisoryLinksetQueryService(lookup);
var firstPage = await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", Limit: 2), CancellationToken.None);
var firstPage = await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", Limit: 2), TestContext.Current.CancellationToken);
Assert.Equal(2, firstPage.Linksets.Length);
Assert.True(firstPage.HasMore);
@@ -40,7 +40,7 @@ public sealed class AdvisoryLinksetQueryServiceTests
Assert.Equal("adv-003", firstPage.Linksets[0].AdvisoryId);
Assert.Equal("pkg:npm/a", firstPage.Linksets[0].Normalized?.Purls?.First());
var secondPage = await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", Limit: 2, Cursor: firstPage.NextCursor), CancellationToken.None);
var secondPage = await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", Limit: 2, Cursor: firstPage.NextCursor), TestContext.Current.CancellationToken);
Assert.Single(secondPage.Linksets);
Assert.False(secondPage.HasMore);
@@ -56,7 +56,7 @@ public sealed class AdvisoryLinksetQueryServiceTests
await Assert.ThrowsAsync<FormatException>(async () =>
{
await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", Limit: 1, Cursor: "not-base64"), CancellationToken.None);
await service.QueryAsync(new AdvisoryLinksetQueryOptions("tenant", Limit: 1, Cursor: "not-base64"), TestContext.Current.CancellationToken);
});
}

View File

@@ -69,7 +69,7 @@ public sealed class NoisePriorServiceTests
var result = await service.RecomputeAsync(
new NoisePriorComputationRequest("CVE-9999-0001"),
CancellationToken.None);
TestContext.Current.CancellationToken);
Assert.Equal("cve-9999-0001", result.VulnerabilityKey);
Assert.Single(result.Summaries);
@@ -135,7 +135,7 @@ public sealed class NoisePriorServiceTests
var result = await service.RecomputeAsync(
new NoisePriorComputationRequest("cve-2025-1111"),
CancellationToken.None);
TestContext.Current.CancellationToken);
var summary = Assert.Single(result.Summaries);
Assert.Equal(1.0, summary.Probability);
@@ -170,13 +170,13 @@ public sealed class NoisePriorServiceTests
await service.RecomputeAsync(
new NoisePriorComputationRequest("CVE-2025-2000"),
CancellationToken.None);
TestContext.Current.CancellationToken);
var summaries = await service.GetByPackageAsync(
" SemVer ",
"pkg:npm/example",
" linux ",
CancellationToken.None);
TestContext.Current.CancellationToken);
Assert.Single(summaries);
Assert.Equal("semver", summaries[0].PackageType);

View File

@@ -57,7 +57,7 @@ public sealed class AdvisoryObservationQueryServiceTests
var lookup = new InMemoryLookup(observations);
var service = new AdvisoryObservationQueryService(lookup);
var result = await service.QueryAsync(new AdvisoryObservationQueryOptions("tenant-a"), CancellationToken.None);
var result = await service.QueryAsync(new AdvisoryObservationQueryOptions("tenant-a"), TestContext.Current.CancellationToken);
Assert.Equal(2, result.Observations.Length);
Assert.Equal("tenant-a:osv:beta:1", result.Observations[0].ObservationId);
@@ -118,7 +118,7 @@ public sealed class AdvisoryObservationQueryServiceTests
var result = await service.QueryAsync(
new AdvisoryObservationQueryOptions("TEnant-A", aliases: new[] { " CVE-2025-0001 ", "CVE-2025-9999" }),
CancellationToken.None);
TestContext.Current.CancellationToken);
Assert.Equal(2, result.Observations.Length);
Assert.All(result.Observations, observation =>
@@ -164,7 +164,7 @@ public sealed class AdvisoryObservationQueryServiceTests
purls: new[] { "pkg:pypi/package-b@2.0.0" },
cpes: new[] { "cpe:/a:vendor:product:2.0" });
var result = await service.QueryAsync(options, CancellationToken.None);
var result = await service.QueryAsync(options, TestContext.Current.CancellationToken);
Assert.Single(result.Observations);
Assert.Equal("tenant-a:ghsa:beta:1", result.Observations[0].ObservationId);
@@ -212,7 +212,7 @@ public sealed class AdvisoryObservationQueryServiceTests
var firstPage = await service.QueryAsync(
new AdvisoryObservationQueryOptions("tenant-a", limit: 2),
CancellationToken.None);
TestContext.Current.CancellationToken);
Assert.Equal(2, firstPage.Observations.Length);
Assert.True(firstPage.HasMore);
@@ -220,7 +220,7 @@ public sealed class AdvisoryObservationQueryServiceTests
var secondPage = await service.QueryAsync(
new AdvisoryObservationQueryOptions("tenant-a", limit: 2, cursor: firstPage.NextCursor),
CancellationToken.None);
TestContext.Current.CancellationToken);
Assert.Single(secondPage.Observations);
Assert.False(secondPage.HasMore);

View File

@@ -10,9 +10,9 @@ public sealed class OrchestratorRegistryStoreTests
var store = new InMemoryOrchestratorRegistryStore();
var record = CreateRegistryRecord("tenant-1", "connector-1");
await store.UpsertAsync(record, CancellationToken.None);
await store.UpsertAsync(record, TestContext.Current.CancellationToken);
var retrieved = await store.GetAsync("tenant-1", "connector-1", CancellationToken.None);
var retrieved = await store.GetAsync("tenant-1", "connector-1", TestContext.Current.CancellationToken);
Assert.NotNull(retrieved);
Assert.Equal("tenant-1", retrieved.Tenant);
Assert.Equal("connector-1", retrieved.ConnectorId);
@@ -25,10 +25,10 @@ public sealed class OrchestratorRegistryStoreTests
var record1 = CreateRegistryRecord("tenant-1", "connector-1", source: "nvd");
var record2 = CreateRegistryRecord("tenant-1", "connector-1", source: "osv");
await store.UpsertAsync(record1, CancellationToken.None);
await store.UpsertAsync(record2, CancellationToken.None);
await store.UpsertAsync(record1, TestContext.Current.CancellationToken);
await store.UpsertAsync(record2, TestContext.Current.CancellationToken);
var retrieved = await store.GetAsync("tenant-1", "connector-1", CancellationToken.None);
var retrieved = await store.GetAsync("tenant-1", "connector-1", TestContext.Current.CancellationToken);
Assert.NotNull(retrieved);
Assert.Equal("osv", retrieved.Source);
}
@@ -38,7 +38,7 @@ public sealed class OrchestratorRegistryStoreTests
{
var store = new InMemoryOrchestratorRegistryStore();
var retrieved = await store.GetAsync("tenant-1", "nonexistent", CancellationToken.None);
var retrieved = await store.GetAsync("tenant-1", "nonexistent", TestContext.Current.CancellationToken);
Assert.Null(retrieved);
}
@@ -47,11 +47,11 @@ public sealed class OrchestratorRegistryStoreTests
public async Task ListAsync_ReturnsRecordsForTenant()
{
var store = new InMemoryOrchestratorRegistryStore();
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "connector-a"), CancellationToken.None);
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "connector-b"), CancellationToken.None);
await store.UpsertAsync(CreateRegistryRecord("tenant-2", "connector-c"), CancellationToken.None);
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "connector-a"), TestContext.Current.CancellationToken);
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "connector-b"), TestContext.Current.CancellationToken);
await store.UpsertAsync(CreateRegistryRecord("tenant-2", "connector-c"), TestContext.Current.CancellationToken);
var records = await store.ListAsync("tenant-1", CancellationToken.None);
var records = await store.ListAsync("tenant-1", TestContext.Current.CancellationToken);
Assert.Equal(2, records.Count);
Assert.All(records, r => Assert.Equal("tenant-1", r.Tenant));
@@ -61,10 +61,10 @@ public sealed class OrchestratorRegistryStoreTests
public async Task ListAsync_ReturnsOrderedByConnectorId()
{
var store = new InMemoryOrchestratorRegistryStore();
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "zzz-connector"), CancellationToken.None);
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "aaa-connector"), CancellationToken.None);
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "zzz-connector"), TestContext.Current.CancellationToken);
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "aaa-connector"), TestContext.Current.CancellationToken);
var records = await store.ListAsync("tenant-1", CancellationToken.None);
var records = await store.ListAsync("tenant-1", TestContext.Current.CancellationToken);
Assert.Equal("aaa-connector", records[0].ConnectorId);
Assert.Equal("zzz-connector", records[1].ConnectorId);
@@ -80,9 +80,9 @@ public sealed class OrchestratorRegistryStoreTests
OrchestratorHeartbeatStatus.Running, 50, 10,
null, null, null, null, DateTimeOffset.UtcNow);
await store.AppendHeartbeatAsync(heartbeat, CancellationToken.None);
await store.AppendHeartbeatAsync(heartbeat, TestContext.Current.CancellationToken);
var latest = await store.GetLatestHeartbeatAsync("tenant-1", "connector-1", runId, CancellationToken.None);
var latest = await store.GetLatestHeartbeatAsync("tenant-1", "connector-1", runId, TestContext.Current.CancellationToken);
Assert.NotNull(latest);
Assert.Equal(1, latest.Sequence);
Assert.Equal(OrchestratorHeartbeatStatus.Running, latest.Status);
@@ -95,11 +95,11 @@ public sealed class OrchestratorRegistryStoreTests
var runId = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;
await store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 1, OrchestratorHeartbeatStatus.Starting, now), CancellationToken.None);
await store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 3, OrchestratorHeartbeatStatus.Succeeded, now.AddMinutes(2)), CancellationToken.None);
await store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 2, OrchestratorHeartbeatStatus.Running, now.AddMinutes(1)), CancellationToken.None);
await store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 1, OrchestratorHeartbeatStatus.Starting, now), TestContext.Current.CancellationToken);
await store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 3, OrchestratorHeartbeatStatus.Succeeded, now.AddMinutes(2)), TestContext.Current.CancellationToken);
await store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 2, OrchestratorHeartbeatStatus.Running, now.AddMinutes(1)), TestContext.Current.CancellationToken);
var latest = await store.GetLatestHeartbeatAsync("tenant-1", "connector-1", runId, CancellationToken.None);
var latest = await store.GetLatestHeartbeatAsync("tenant-1", "connector-1", runId, TestContext.Current.CancellationToken);
Assert.NotNull(latest);
Assert.Equal(3, latest.Sequence);
@@ -116,9 +116,9 @@ public sealed class OrchestratorRegistryStoreTests
OrchestratorCommandKind.Pause, null, null,
DateTimeOffset.UtcNow, null);
await store.EnqueueCommandAsync(command, CancellationToken.None);
await store.EnqueueCommandAsync(command, TestContext.Current.CancellationToken);
var commands = await store.GetPendingCommandsAsync("tenant-1", "connector-1", runId, null, CancellationToken.None);
var commands = await store.GetPendingCommandsAsync("tenant-1", "connector-1", runId, null, TestContext.Current.CancellationToken);
Assert.Single(commands);
Assert.Equal(OrchestratorCommandKind.Pause, commands[0].Command);
}
@@ -130,11 +130,11 @@ public sealed class OrchestratorRegistryStoreTests
var runId = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 1, OrchestratorCommandKind.Pause, now), CancellationToken.None);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 2, OrchestratorCommandKind.Resume, now), CancellationToken.None);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 3, OrchestratorCommandKind.Throttle, now), CancellationToken.None);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 1, OrchestratorCommandKind.Pause, now), TestContext.Current.CancellationToken);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 2, OrchestratorCommandKind.Resume, now), TestContext.Current.CancellationToken);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 3, OrchestratorCommandKind.Throttle, now), TestContext.Current.CancellationToken);
var commands = await store.GetPendingCommandsAsync("tenant-1", "connector-1", runId, 1, CancellationToken.None);
var commands = await store.GetPendingCommandsAsync("tenant-1", "connector-1", runId, 1, TestContext.Current.CancellationToken);
Assert.Equal(2, commands.Count);
Assert.Equal(2, commands[0].Sequence);
@@ -150,10 +150,10 @@ public sealed class OrchestratorRegistryStoreTests
var expired = now.AddMinutes(-5);
var future = now.AddMinutes(5);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 1, OrchestratorCommandKind.Pause, now, expired), CancellationToken.None);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 2, OrchestratorCommandKind.Resume, now, future), CancellationToken.None);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 1, OrchestratorCommandKind.Pause, now, expired), TestContext.Current.CancellationToken);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 2, OrchestratorCommandKind.Resume, now, future), TestContext.Current.CancellationToken);
var commands = await store.GetPendingCommandsAsync("tenant-1", "connector-1", runId, null, CancellationToken.None);
var commands = await store.GetPendingCommandsAsync("tenant-1", "connector-1", runId, null, TestContext.Current.CancellationToken);
Assert.Single(commands);
Assert.Equal(2, commands[0].Sequence);
@@ -171,9 +171,9 @@ public sealed class OrchestratorRegistryStoreTests
"dsse-hash",
DateTimeOffset.UtcNow);
await store.StoreManifestAsync(manifest, CancellationToken.None);
await store.StoreManifestAsync(manifest, TestContext.Current.CancellationToken);
var retrieved = await store.GetManifestAsync("tenant-1", "connector-1", runId, CancellationToken.None);
var retrieved = await store.GetManifestAsync("tenant-1", "connector-1", runId, TestContext.Current.CancellationToken);
Assert.NotNull(retrieved);
Assert.Equal(runId, retrieved.RunId);
Assert.Equal(2, retrieved.ArtifactHashes.Count);
@@ -185,7 +185,7 @@ public sealed class OrchestratorRegistryStoreTests
{
var store = new InMemoryOrchestratorRegistryStore();
var manifest = await store.GetManifestAsync("tenant-1", "connector-1", Guid.NewGuid(), CancellationToken.None);
var manifest = await store.GetManifestAsync("tenant-1", "connector-1", Guid.NewGuid(), TestContext.Current.CancellationToken);
Assert.Null(manifest);
}
@@ -196,13 +196,13 @@ public sealed class OrchestratorRegistryStoreTests
var store = new InMemoryOrchestratorRegistryStore();
var runId = Guid.NewGuid();
store.UpsertAsync(CreateRegistryRecord("tenant-1", "connector-1"), CancellationToken.None).Wait();
store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 1, OrchestratorHeartbeatStatus.Running, DateTimeOffset.UtcNow), CancellationToken.None).Wait();
store.UpsertAsync(CreateRegistryRecord("tenant-1", "connector-1"), TestContext.Current.CancellationToken).Wait();
store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 1, OrchestratorHeartbeatStatus.Running, DateTimeOffset.UtcNow), TestContext.Current.CancellationToken).Wait();
store.Clear();
Assert.Null(store.GetAsync("tenant-1", "connector-1", CancellationToken.None).Result);
Assert.Null(store.GetLatestHeartbeatAsync("tenant-1", "connector-1", runId, CancellationToken.None).Result);
Assert.Null(store.GetAsync("tenant-1", "connector-1", TestContext.Current.CancellationToken).Result);
Assert.Null(store.GetLatestHeartbeatAsync("tenant-1", "connector-1", runId, TestContext.Current.CancellationToken).Result);
}
private static OrchestratorRegistryRecord CreateRegistryRecord(string tenant, string connectorId, string source = "nvd")

View File

@@ -33,7 +33,7 @@ public sealed class AdvisoryRawServiceTests
var expectedResult = new AdvisoryRawUpsertResult(true, CreateRecord(storedDocument));
repository.NextResult = expectedResult;
var result = await service.IngestAsync(document, CancellationToken.None);
var result = await service.IngestAsync(document, TestContext.Current.CancellationToken);
Assert.NotNull(repository.CapturedDocument);
Assert.Null(repository.CapturedDocument!.Supersedes);
@@ -55,7 +55,7 @@ public sealed class AdvisoryRawServiceTests
var expectedResult = new AdvisoryRawUpsertResult(false, CreateRecord(existingDocument));
repository.NextResult = expectedResult;
var result = await service.IngestAsync(CreateDocument(), CancellationToken.None);
var result = await service.IngestAsync(CreateDocument(), TestContext.Current.CancellationToken);
Assert.False(result.Inserted);
Assert.Same(expectedResult.Record, result.Record);
@@ -70,7 +70,7 @@ public sealed class AdvisoryRawServiceTests
var measurements = await CollectCounterMeasurementsAsync(
"ingestion_write_total",
() => service.IngestAsync(CreateDocument(), CancellationToken.None));
() => service.IngestAsync(CreateDocument(), TestContext.Current.CancellationToken));
Assert.Contains(
measurements,
@@ -89,7 +89,7 @@ public sealed class AdvisoryRawServiceTests
async () =>
{
await Assert.ThrowsAsync<ConcelierAocGuardException>(
() => service.IngestAsync(CreateDocument(), CancellationToken.None));
() => service.IngestAsync(CreateDocument(), TestContext.Current.CancellationToken));
});
Assert.Contains(
@@ -102,7 +102,7 @@ public sealed class AdvisoryRawServiceTests
async () =>
{
await Assert.ThrowsAsync<ConcelierAocGuardException>(
() => service.IngestAsync(CreateDocument(), CancellationToken.None));
() => service.IngestAsync(CreateDocument(), TestContext.Current.CancellationToken));
});
Assert.Contains(
@@ -125,7 +125,7 @@ public sealed class AdvisoryRawServiceTests
repository.NextResult = new AdvisoryRawUpsertResult(true, CreateRecord(document));
await service.IngestAsync(document, CancellationToken.None);
await service.IngestAsync(document, TestContext.Current.CancellationToken);
Assert.NotNull(repository.CapturedDocument);
Assert.True(aliasSeries.SequenceEqual(repository.CapturedDocument!.Identifiers.Aliases));
@@ -148,7 +148,7 @@ public sealed class AdvisoryRawServiceTests
"Tenant-Example",
"ghsa-aaaa-bbbb-cccc",
new[] { "Vendor-X", " " },
CancellationToken.None);
TestContext.Current.CancellationToken);
Assert.Single(results);
Assert.Equal("tenant-example", repository.CapturedTenant);

View File

@@ -18,7 +18,7 @@ public sealed class AffectedSymbolProviderTests
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var result = await provider.GetByAdvisoryAsync("tenant-1", "CVE-2024-0001", CancellationToken.None);
var result = await provider.GetByAdvisoryAsync("tenant-1", "CVE-2024-0001", TestContext.Current.CancellationToken);
Assert.Equal("tenant-1", result.TenantId);
Assert.Equal("CVE-2024-0001", result.AdvisoryId);
@@ -53,9 +53,9 @@ public sealed class AffectedSymbolProviderTests
module: "lodash",
versionRange: "<4.17.21");
await store.StoreAsync([symbol], CancellationToken.None);
await store.StoreAsync([symbol], TestContext.Current.CancellationToken);
var result = await provider.GetByAdvisoryAsync("tenant-1", "CVE-2024-0001", CancellationToken.None);
var result = await provider.GetByAdvisoryAsync("tenant-1", "CVE-2024-0001", TestContext.Current.CancellationToken);
Assert.Single(result.Symbols);
Assert.Equal("lodash.template", result.Symbols[0].Symbol);
@@ -84,9 +84,9 @@ public sealed class AffectedSymbolProviderTests
AffectedSymbol.Method("tenant-1", "CVE-2024-0001", "obs-3", "method1", "ClassName", nvdProvenance, _timeProvider.GetUtcNow())
};
await store.StoreAsync(symbols, CancellationToken.None);
await store.StoreAsync(symbols, TestContext.Current.CancellationToken);
var result = await provider.GetByAdvisoryAsync("tenant-1", "CVE-2024-0001", CancellationToken.None);
var result = await provider.GetByAdvisoryAsync("tenant-1", "CVE-2024-0001", TestContext.Current.CancellationToken);
Assert.Equal(3, result.Symbols.Length);
Assert.Equal(2, result.SourceSummaries.Length);
@@ -121,9 +121,9 @@ public sealed class AffectedSymbolProviderTests
extractedAt: _timeProvider.GetUtcNow(),
purl: "pkg:npm/express@4.18.0");
await store.StoreAsync([symbol], CancellationToken.None);
await store.StoreAsync([symbol], TestContext.Current.CancellationToken);
var result = await provider.GetByPackageAsync("tenant-1", "pkg:npm/express@4.18.0", CancellationToken.None);
var result = await provider.GetByPackageAsync("tenant-1", "pkg:npm/express@4.18.0", TestContext.Current.CancellationToken);
Assert.Single(result.Symbols);
Assert.Equal("express.render", result.Symbols[0].Symbol);
@@ -146,10 +146,10 @@ public sealed class AffectedSymbolProviderTests
AffectedSymbol.Function("tenant-1", "CVE-2024-0002", "obs-2", "func2", provenance, _timeProvider.GetUtcNow())
};
await store.StoreAsync(symbols, CancellationToken.None);
await store.StoreAsync(symbols, TestContext.Current.CancellationToken);
var options = AffectedSymbolQueryOptions.ForAdvisory("tenant-1", "CVE-2024-0001");
var result = await provider.QueryAsync(options, CancellationToken.None);
var result = await provider.QueryAsync(options, TestContext.Current.CancellationToken);
Assert.Equal(1, result.TotalCount);
Assert.Single(result.Symbols);
@@ -173,12 +173,12 @@ public sealed class AffectedSymbolProviderTests
AffectedSymbol.Method("tenant-1", "CVE-2024-0001", "obs-2", "method1", "Class1", provenance, _timeProvider.GetUtcNow())
};
await store.StoreAsync(symbols, CancellationToken.None);
await store.StoreAsync(symbols, TestContext.Current.CancellationToken);
var options = new AffectedSymbolQueryOptions(
TenantId: "tenant-1",
SymbolTypes: [AffectedSymbolType.Method]);
var result = await provider.QueryAsync(options, CancellationToken.None);
var result = await provider.QueryAsync(options, TestContext.Current.CancellationToken);
Assert.Equal(1, result.TotalCount);
Assert.Equal(AffectedSymbolType.Method, result.Symbols[0].SymbolType);
@@ -200,13 +200,13 @@ public sealed class AffectedSymbolProviderTests
"tenant-1", "CVE-2024-0001", $"obs-{i}", $"func{i}", provenance, _timeProvider.GetUtcNow()))
.ToList();
await store.StoreAsync(symbols, CancellationToken.None);
await store.StoreAsync(symbols, TestContext.Current.CancellationToken);
var options = new AffectedSymbolQueryOptions(
TenantId: "tenant-1",
Limit: 3,
Offset: 2);
var result = await provider.QueryAsync(options, CancellationToken.None);
var result = await provider.QueryAsync(options, TestContext.Current.CancellationToken);
Assert.Equal(10, result.TotalCount);
Assert.Equal(3, result.Symbols.Length);
@@ -230,12 +230,12 @@ public sealed class AffectedSymbolProviderTests
AffectedSymbol.Function("tenant-1", "CVE-2024-0002", "obs-2", "func2", provenance, _timeProvider.GetUtcNow())
};
await store.StoreAsync(symbols, CancellationToken.None);
await store.StoreAsync(symbols, TestContext.Current.CancellationToken);
var result = await provider.GetByAdvisoriesBatchAsync(
"tenant-1",
["CVE-2024-0001", "CVE-2024-0002", "CVE-2024-0003"],
CancellationToken.None);
TestContext.Current.CancellationToken);
Assert.Equal(3, result.Count);
Assert.Single(result["CVE-2024-0001"].Symbols);
@@ -256,10 +256,10 @@ public sealed class AffectedSymbolProviderTests
var symbol = AffectedSymbol.Function(
"tenant-1", "CVE-2024-0001", "obs-1", "func1", provenance, _timeProvider.GetUtcNow());
await store.StoreAsync([symbol], CancellationToken.None);
await store.StoreAsync([symbol], TestContext.Current.CancellationToken);
var exists = await provider.HasSymbolsAsync("tenant-1", "CVE-2024-0001", CancellationToken.None);
var notExists = await provider.HasSymbolsAsync("tenant-1", "CVE-2024-9999", CancellationToken.None);
var exists = await provider.HasSymbolsAsync("tenant-1", "CVE-2024-0001", TestContext.Current.CancellationToken);
var notExists = await provider.HasSymbolsAsync("tenant-1", "CVE-2024-9999", TestContext.Current.CancellationToken);
Assert.True(exists);
Assert.False(notExists);

View File

@@ -41,7 +41,7 @@ public sealed class UnknownStateLedgerTests
});
var request = new UnknownStateLedgerRequest("CVE-2025-1111", advisory, observedAt);
var result = await ledger.RecordAsync(request, CancellationToken.None);
var result = await ledger.RecordAsync(request, TestContext.Current.CancellationToken);
Assert.True(advisory.Provenance.IsDefaultOrEmpty);
Assert.Equal("cve-2025-1111", result.VulnerabilityKey);
@@ -62,7 +62,7 @@ public sealed class UnknownStateLedgerTests
Assert.Equal("medium", fixMarker.ConfidenceBand);
Assert.Contains("explicit fixed version", fixMarker.Evidence, StringComparison.OrdinalIgnoreCase);
var repositoryMarkers = await repository.GetByVulnerabilityAsync("cve-2025-1111", CancellationToken.None);
var repositoryMarkers = await repository.GetByVulnerabilityAsync("cve-2025-1111", TestContext.Current.CancellationToken);
Assert.Equal(3, repositoryMarkers.Count);
}
@@ -89,7 +89,7 @@ public sealed class UnknownStateLedgerTests
}),
});
var result = await ledger.RecordAsync(new UnknownStateLedgerRequest("GHSA-1234", advisory, observedAt), CancellationToken.None);
var result = await ledger.RecordAsync(new UnknownStateLedgerRequest("GHSA-1234", advisory, observedAt), TestContext.Current.CancellationToken);
Assert.Equal("ghsa-1234", result.VulnerabilityKey);
Assert.Empty(result.Markers);
@@ -113,7 +113,7 @@ public sealed class UnknownStateLedgerTests
repository.Stored["cve-2025-0001"] = new List<UnknownStateSnapshot> { snapshot };
var ledger = new UnknownStateLedger(repository);
var markers = await ledger.GetByVulnerabilityAsync("CVE-2025-0001", CancellationToken.None);
var markers = await ledger.GetByVulnerabilityAsync("CVE-2025-0001", TestContext.Current.CancellationToken);
Assert.Single(markers);
Assert.Equal(snapshot, markers[0]);

View File

@@ -302,7 +302,8 @@ public sealed class MergePropertyTests
// Assert - merge provenance trace should contain all original sources
var mergeProvenance = result.Provenance.FirstOrDefault(p => p.Source == "merge");
mergeProvenance.Should().NotBeNull();
mergeProvenance!.Value.ToLowerInvariant().Should().Contain("redhat");
Assert.NotNull(mergeProvenance);
mergeProvenance.Value.ToLowerInvariant().Should().Contain("redhat");
mergeProvenance.Value.ToLowerInvariant().Should().Contain("ghsa");
mergeProvenance.Value.ToLowerInvariant().Should().Contain("osv");
}

View File

@@ -32,14 +32,14 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new AdvisoryCanonicalRepository(_dataSource, NullLogger<AdvisoryCanonicalRepository>.Instance);
_sourceRepository = new SourceRepository(_dataSource, NullLogger<SourceRepository>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
#region GetByIdAsync Tests

View File

@@ -43,14 +43,14 @@ public sealed class AdvisoryIdempotencyTests : IAsyncLifetime
{
await _fixture.TruncateAllTablesAsync();
var options = _fixture.Fixture.CreateOptions();
var options = _fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_advisoryRepository = new AdvisoryRepository(_dataSource, NullLogger<AdvisoryRepository>.Instance);
_sourceRepository = new SourceRepository(_dataSource, NullLogger<SourceRepository>.Instance);
_sourceStateRepository = new SourceStateRepository(_dataSource, NullLogger<SourceStateRepository>.Instance);
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Fact]
public async Task UpsertAsync_SameAdvisoryKey_Twice_NosDuplicates()

View File

@@ -26,7 +26,7 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new AdvisoryRepository(_dataSource, NullLogger<AdvisoryRepository>.Instance);
@@ -36,7 +36,7 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Trait("Category", TestCategories.Unit)]
[Fact]

View File

@@ -5,14 +5,16 @@
// Description: Model S1 migration tests for Concelier.Storage
// -----------------------------------------------------------------------------
using System.Reflection;
using Dapper;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Infrastructure.Postgres.Migrations;
using Npgsql;
using StellaOps.TestKit;
using Testcontainers.PostgreSql;
using Xunit;
using Xunit.Sdk;
namespace StellaOps.Concelier.Persistence.Tests;
@@ -31,19 +33,34 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
public async ValueTask InitializeAsync()
{
_container = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.WithDatabase("concelier_migration_test")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
try
{
_container = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.WithDatabase("concelier_migration_test")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
await _container.StartAsync();
await _container.StartAsync();
}
catch (ArgumentException ex) when (ShouldSkipForMissingDocker(ex))
{
await SafeDisposeAsync();
throw SkipException.ForSkip(
$"Postgres migration tests require Docker/Testcontainers. Skipping because the container failed to start: {ex.Message}");
}
catch (TimeoutException ex)
{
await SafeDisposeAsync();
throw SkipException.ForSkip(
$"Postgres migration tests require Docker/Testcontainers. Skipping because the container startup timed out: {ex.Message}");
}
}
public async ValueTask DisposeAsync()
{
await _container.DisposeAsync();
await SafeDisposeAsync();
}
[Fact]
@@ -61,7 +78,7 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
var tables = await connection.QueryAsync<string>(
@"SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
WHERE table_schema = 'vuln'
ORDER BY table_name");
var tableList = tables.ToList();
@@ -69,7 +86,7 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
// Verify critical Concelier tables exist
tableList.Should().Contain("advisories", "advisories table should exist");
tableList.Should().Contain("sources", "sources table should exist");
tableList.Should().Contain("__migrations", "Migration tracking table should exist");
tableList.Should().Contain("schema_migrations", "Migration tracking table should exist");
}
[Fact]
@@ -84,7 +101,7 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
await connection.OpenAsync();
var migrationsApplied = await connection.QueryAsync<string>(
"SELECT migration_id FROM __migrations ORDER BY applied_at");
"SELECT migration_name FROM vuln.schema_migrations ORDER BY applied_at");
var migrationList = migrationsApplied.ToList();
migrationList.Should().NotBeEmpty("migrations should be tracked");
@@ -109,11 +126,11 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
await connection.OpenAsync();
var migrationCount = await connection.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM __migrations");
"SELECT COUNT(*) FROM vuln.schema_migrations");
// Count unique migrations
var uniqueMigrations = await connection.ExecuteScalarAsync<int>(
"SELECT COUNT(DISTINCT migration_id) FROM __migrations");
"SELECT COUNT(DISTINCT migration_name) FROM vuln.schema_migrations");
migrationCount.Should().Be(uniqueMigrations,
"each migration should only be recorded once");
@@ -132,7 +149,7 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
var indexes = await connection.QueryAsync<string>(
@"SELECT indexname FROM pg_indexes
WHERE schemaname = 'public'
WHERE schemaname = 'vuln'
ORDER BY indexname");
var indexList = indexes.ToList();
@@ -152,7 +169,7 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
var advisoryColumns = await connection.QueryAsync<string>(
@"SELECT column_name FROM information_schema.columns
WHERE table_name = 'advisories' AND table_schema = 'public'
WHERE table_name = 'advisories' AND table_schema = 'vuln'
ORDER BY ordinal_position");
var columnList = advisoryColumns.ToList();
@@ -177,7 +194,7 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
var sourceColumns = await connection.QueryAsync<string>(
@"SELECT column_name FROM information_schema.columns
WHERE table_name = 'sources' AND table_schema = 'public'
WHERE table_name = 'sources' AND table_schema = 'vuln'
ORDER BY ordinal_position");
var columnList = sourceColumns.ToList();
@@ -195,49 +212,15 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
// Arrange
var connectionString = _container.GetConnectionString();
// Act - Apply migrations in sequence
var migrationFiles = GetMigrationFiles();
// Act - Apply migrations from scratch
await ApplyAllMigrationsAsync(connectionString);
// Assert - at least some migrations should be applied (if any exist)
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
// Create migration tracking table first
await connection.ExecuteAsync(@"
CREATE TABLE IF NOT EXISTS __migrations (
id SERIAL PRIMARY KEY,
migration_id TEXT NOT NULL UNIQUE,
applied_at TIMESTAMPTZ DEFAULT NOW()
)");
// Apply each migration in order
int appliedCount = 0;
foreach (var migrationFile in migrationFiles.OrderBy(f => f))
{
var migrationId = Path.GetFileName(migrationFile);
// Check if already applied
var alreadyApplied = await connection.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM __migrations WHERE migration_id = @Id",
new { Id = migrationId });
if (alreadyApplied > 0)
continue;
// Apply migration
var sql = GetMigrationContent(migrationFile);
if (!string.IsNullOrWhiteSpace(sql))
{
await connection.ExecuteAsync(sql);
await connection.ExecuteAsync(
"INSERT INTO __migrations (migration_id) VALUES (@Id)",
new { Id = migrationId });
appliedCount++;
}
}
// Assert - at least some migrations should be applied (if any exist)
var totalMigrations = await connection.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM __migrations");
"SELECT COUNT(*) FROM vuln.schema_migrations");
// This test passes even if no migrations exist yet
totalMigrations.Should().BeGreaterThanOrEqualTo(0);
@@ -258,7 +241,7 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
@"SELECT tc.constraint_name
FROM information_schema.table_constraints tc
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND tc.table_schema = 'vuln'
ORDER BY tc.constraint_name");
var fkList = foreignKeys.ToList();
@@ -268,63 +251,31 @@ public sealed class ConcelierMigrationTests : IAsyncLifetime
private async Task ApplyAllMigrationsAsync(string connectionString)
{
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
var runner = new MigrationRunner(
connectionString,
ConcelierDataSource.DefaultSchemaName,
"Concelier",
NullLogger.Instance);
// Create migration tracking table
await connection.ExecuteAsync(@"
CREATE TABLE IF NOT EXISTS __migrations (
id SERIAL PRIMARY KEY,
migration_id TEXT NOT NULL UNIQUE,
applied_at TIMESTAMPTZ DEFAULT NOW()
)");
await runner.RunFromAssemblyAsync(
typeof(ConcelierDataSource).Assembly,
resourcePrefix: null,
options: null,
cancellationToken: default);
}
// Get and apply all migrations
var migrationFiles = GetMigrationFiles();
foreach (var migrationFile in migrationFiles.OrderBy(f => f))
private async ValueTask SafeDisposeAsync()
{
if (_container != null)
{
var migrationId = Path.GetFileName(migrationFile);
// Skip if already applied
var alreadyApplied = await connection.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM __migrations WHERE migration_id = @Id",
new { Id = migrationId });
if (alreadyApplied > 0)
continue;
// Apply migration
var sql = GetMigrationContent(migrationFile);
if (!string.IsNullOrWhiteSpace(sql))
{
await connection.ExecuteAsync(sql);
await connection.ExecuteAsync(
"INSERT INTO __migrations (migration_id) VALUES (@Id)",
new { Id = migrationId });
}
await _container.DisposeAsync();
}
}
private static IEnumerable<string> GetMigrationFiles()
private static bool ShouldSkipForMissingDocker(ArgumentException exception)
{
var assembly = typeof(ConcelierDataSource).Assembly;
var resourceNames = assembly.GetManifestResourceNames()
.Where(n => n.Contains("Migrations") && n.EndsWith(".sql"))
.OrderBy(n => n);
return resourceNames;
}
private static string GetMigrationContent(string resourceName)
{
var assembly = typeof(ConcelierDataSource).Assembly;
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
return string.Empty;
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
return string.Equals(exception.ParamName, "DockerEndpointAuthConfig", StringComparison.Ordinal)
|| exception.Message.Contains("Docker is either not running", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -1,6 +1,7 @@
using System.Reflection;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Infrastructure.Postgres.Testing;
using StellaOps.Infrastructure.Postgres.Options;
using Xunit;
namespace StellaOps.Concelier.Persistence.Tests;
@@ -15,13 +16,21 @@ public sealed class ConcelierPostgresFixture : PostgresIntegrationFixture, IColl
=> typeof(ConcelierDataSource).Assembly;
protected override string GetModuleName() => "Concelier";
public PostgresOptions CreateOptions()
{
var options = Fixture.CreateOptions();
options.MaxPoolSize = 10;
options.MinPoolSize = 0;
return options;
}
}
/// <summary>
/// Collection definition for Concelier PostgreSQL integration tests.
/// Tests in this collection share a single PostgreSQL container instance.
/// </summary>
[CollectionDefinition(Name)]
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class ConcelierPostgresCollection : ICollectionFixture<ConcelierPostgresFixture>
{
public const string Name = "ConcelierPostgres";

View File

@@ -44,7 +44,7 @@ public sealed class ConcelierQueryDeterminismTests : IAsyncLifetime
{
await _fixture.TruncateAllTablesAsync();
var options = _fixture.Fixture.CreateOptions();
var options = _fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_advisoryRepository = new AdvisoryRepository(_dataSource, NullLogger<AdvisoryRepository>.Instance);
_sourceRepository = new SourceRepository(_dataSource, NullLogger<SourceRepository>.Instance);
@@ -52,7 +52,7 @@ public sealed class ConcelierQueryDeterminismTests : IAsyncLifetime
_affectedRepository = new AdvisoryAffectedRepository(_dataSource, NullLogger<AdvisoryAffectedRepository>.Instance);
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Fact]
public async Task GetModifiedSinceAsync_MultipleQueries_ReturnsDeterministicOrder()

View File

@@ -9,8 +9,10 @@ using FluentAssertions;
using StellaOps.Concelier.Persistence.Postgres;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.Core.Canonical;
using StellaOps.Concelier.Interest;
using StellaOps.Concelier.Interest.Models;
using StellaOps.Concelier.Persistence.Postgres.Models;
using StellaOps.Concelier.Persistence.Postgres.Repositories;
using Xunit;
@@ -27,18 +29,20 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
private readonly ConcelierPostgresFixture _fixture;
private readonly ConcelierDataSource _dataSource;
private readonly InterestScoreRepository _repository;
private readonly AdvisoryCanonicalRepository _canonicalRepository;
public InterestScoreRepositoryTests(ConcelierPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new InterestScoreRepository(_dataSource, NullLogger<InterestScoreRepository>.Instance);
_canonicalRepository = new AdvisoryCanonicalRepository(_dataSource, NullLogger<AdvisoryCanonicalRepository>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
#region GetByCanonicalIdAsync Tests
@@ -48,7 +52,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
{
// Arrange
var score = CreateTestScore();
await _repository.SaveAsync(score);
await SaveScoreAsync(score);
// Act
var result = await _repository.GetByCanonicalIdAsync(score.CanonicalId);
@@ -84,9 +88,9 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var score1 = CreateTestScore();
var score2 = CreateTestScore();
var score3 = CreateTestScore();
await _repository.SaveAsync(score1);
await _repository.SaveAsync(score2);
await _repository.SaveAsync(score3);
await SaveScoreAsync(score1);
await SaveScoreAsync(score2);
await SaveScoreAsync(score3);
// Act
var result = await _repository.GetByCanonicalIdsAsync([score1.CanonicalId, score3.CanonicalId]);
@@ -132,7 +136,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var score = CreateTestScore(score: 0.75, reasons: ["in_sbom", "reachable", "deployed"]);
// Act
await _repository.SaveAsync(score);
await SaveScoreAsync(score);
// Assert
var result = await _repository.GetByCanonicalIdAsync(score.CanonicalId);
@@ -148,7 +152,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
// Arrange
var canonicalId = Guid.NewGuid();
var original = CreateTestScore(canonicalId: canonicalId, score: 0.5, reasons: ["in_sbom"]);
await _repository.SaveAsync(original);
await SaveScoreAsync(original);
var updated = CreateTestScore(
canonicalId: canonicalId,
@@ -156,7 +160,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
reasons: ["in_sbom", "reachable", "deployed", "no_vex_na"]);
// Act
await _repository.SaveAsync(updated);
await SaveScoreAsync(updated);
// Assert
var result = await _repository.GetByCanonicalIdAsync(canonicalId);
@@ -174,7 +178,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var score = CreateTestScore(lastSeenInBuild: buildId);
// Act
await _repository.SaveAsync(score);
await SaveScoreAsync(score);
// Assert
var result = await _repository.GetByCanonicalIdAsync(score.CanonicalId);
@@ -190,7 +194,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var score = CreateTestScore(lastSeenInBuild: null);
// Act
await _repository.SaveAsync(score);
await SaveScoreAsync(score);
// Assert
var result = await _repository.GetByCanonicalIdAsync(score.CanonicalId);
@@ -206,7 +210,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var score = CreateTestScore(reasons: []);
// Act
await _repository.SaveAsync(score);
await SaveScoreAsync(score);
// Assert
var result = await _repository.GetByCanonicalIdAsync(score.CanonicalId);
@@ -231,7 +235,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
};
// Act
await _repository.SaveManyAsync(scores);
await SaveScoresAsync(scores);
// Assert
var count = await _repository.CountAsync();
@@ -245,7 +249,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
// Arrange
var canonicalId = Guid.NewGuid();
var original = CreateTestScore(canonicalId: canonicalId, score: 0.3);
await _repository.SaveAsync(original);
await SaveScoreAsync(original);
var scores = new[]
{
@@ -254,7 +258,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
};
// Act
await _repository.SaveManyAsync(scores);
await SaveScoresAsync(scores);
// Assert
var count = await _repository.CountAsync();
@@ -269,7 +273,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
public async Task SaveManyAsync_ShouldHandleEmptyInput()
{
// Act - should not throw
await _repository.SaveManyAsync([]);
await SaveScoresAsync([]);
// Assert
var count = await _repository.CountAsync();
@@ -286,7 +290,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
{
// Arrange
var score = CreateTestScore();
await _repository.SaveAsync(score);
await SaveScoreAsync(score);
// Verify exists
var exists = await _repository.GetByCanonicalIdAsync(score.CanonicalId);
@@ -324,9 +328,9 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var lowScore2 = CreateTestScore(score: 0.15, computedAt: oldDate);
var highScore = CreateTestScore(score: 0.8, computedAt: oldDate);
await _repository.SaveAsync(lowScore1);
await _repository.SaveAsync(lowScore2);
await _repository.SaveAsync(highScore);
await SaveScoreAsync(lowScore1);
await SaveScoreAsync(lowScore2);
await SaveScoreAsync(highScore);
// Act
var result = await _repository.GetLowScoreCanonicalIdsAsync(
@@ -349,8 +353,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var oldScore = CreateTestScore(score: 0.1, computedAt: DateTimeOffset.UtcNow.AddDays(-10));
var recentScore = CreateTestScore(score: 0.1, computedAt: DateTimeOffset.UtcNow);
await _repository.SaveAsync(oldScore);
await _repository.SaveAsync(recentScore);
await SaveScoreAsync(oldScore);
await SaveScoreAsync(recentScore);
// Act
var result = await _repository.GetLowScoreCanonicalIdsAsync(
@@ -371,7 +375,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var oldDate = DateTimeOffset.UtcNow.AddDays(-10);
for (int i = 0; i < 10; i++)
{
await _repository.SaveAsync(CreateTestScore(score: 0.1, computedAt: oldDate));
await SaveScoreAsync(CreateTestScore(score: 0.1, computedAt: oldDate));
}
// Act
@@ -397,9 +401,9 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var highScore2 = CreateTestScore(score: 0.75);
var lowScore = CreateTestScore(score: 0.3);
await _repository.SaveAsync(highScore1);
await _repository.SaveAsync(highScore2);
await _repository.SaveAsync(lowScore);
await SaveScoreAsync(highScore1);
await SaveScoreAsync(highScore2);
await SaveScoreAsync(lowScore);
// Act
var result = await _repository.GetHighScoreCanonicalIdsAsync(
@@ -420,7 +424,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
// Arrange
for (int i = 0; i < 10; i++)
{
await _repository.SaveAsync(CreateTestScore(score: 0.8));
await SaveScoreAsync(CreateTestScore(score: 0.8));
}
// Act
@@ -445,9 +449,9 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var medium = CreateTestScore(score: 0.5);
var high = CreateTestScore(score: 0.9);
await _repository.SaveAsync(low);
await _repository.SaveAsync(medium);
await _repository.SaveAsync(high);
await SaveScoreAsync(low);
await SaveScoreAsync(medium);
await SaveScoreAsync(high);
// Act
var result = await _repository.GetTopScoresAsync(limit: 10);
@@ -466,7 +470,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
// Arrange
for (int i = 0; i < 10; i++)
{
await _repository.SaveAsync(CreateTestScore(score: 0.1 * (i + 1)));
await SaveScoreAsync(CreateTestScore(score: 0.1 * (i + 1)));
}
// Act
@@ -487,7 +491,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
// Arrange
for (int i = 0; i < 10; i++)
{
await _repository.SaveAsync(CreateTestScore(score: 0.1 * (i + 1)));
await SaveScoreAsync(CreateTestScore(score: 0.1 * (i + 1)));
}
// Act
@@ -516,8 +520,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var stale = CreateTestScore(computedAt: DateTimeOffset.UtcNow.AddDays(-30));
var fresh = CreateTestScore(computedAt: DateTimeOffset.UtcNow);
await _repository.SaveAsync(stale);
await _repository.SaveAsync(fresh);
await SaveScoreAsync(stale);
await SaveScoreAsync(fresh);
// Act
var result = await _repository.GetStaleCanonicalIdsAsync(
@@ -537,7 +541,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var oldDate = DateTimeOffset.UtcNow.AddDays(-30);
for (int i = 0; i < 10; i++)
{
await _repository.SaveAsync(CreateTestScore(computedAt: oldDate));
await SaveScoreAsync(CreateTestScore(computedAt: oldDate));
}
// Act
@@ -558,9 +562,9 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
public async Task CountAsync_ShouldReturnTotalCount()
{
// Arrange
await _repository.SaveAsync(CreateTestScore());
await _repository.SaveAsync(CreateTestScore());
await _repository.SaveAsync(CreateTestScore());
await SaveScoreAsync(CreateTestScore());
await SaveScoreAsync(CreateTestScore());
await SaveScoreAsync(CreateTestScore());
// Act
var count = await _repository.CountAsync();
@@ -590,15 +594,15 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
{
// Arrange - create scores in different tiers
// High tier (>= 0.7)
await _repository.SaveAsync(CreateTestScore(score: 0.9));
await _repository.SaveAsync(CreateTestScore(score: 0.8));
await SaveScoreAsync(CreateTestScore(score: 0.9));
await SaveScoreAsync(CreateTestScore(score: 0.8));
// Medium tier (0.4 - 0.7)
await _repository.SaveAsync(CreateTestScore(score: 0.5));
await SaveScoreAsync(CreateTestScore(score: 0.5));
// Low tier (0.2 - 0.4)
await _repository.SaveAsync(CreateTestScore(score: 0.3));
await SaveScoreAsync(CreateTestScore(score: 0.3));
// None tier (< 0.2)
await _repository.SaveAsync(CreateTestScore(score: 0.1));
await _repository.SaveAsync(CreateTestScore(score: 0.05));
await SaveScoreAsync(CreateTestScore(score: 0.1));
await SaveScoreAsync(CreateTestScore(score: 0.05));
// Act
var distribution = await _repository.GetDistributionAsync();
@@ -635,8 +639,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
public async Task GetScoreDistributionAsync_ShouldBeAliasForGetDistributionAsync()
{
// Arrange
await _repository.SaveAsync(CreateTestScore(score: 0.9));
await _repository.SaveAsync(CreateTestScore(score: 0.5));
await SaveScoreAsync(CreateTestScore(score: 0.9));
await SaveScoreAsync(CreateTestScore(score: 0.5));
// Act
var distribution1 = await _repository.GetDistributionAsync();
@@ -660,7 +664,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var score = CreateTestScore(score: 1.0);
// Act
await _repository.SaveAsync(score);
await SaveScoreAsync(score);
// Assert
var result = await _repository.GetByCanonicalIdAsync(score.CanonicalId);
@@ -675,7 +679,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var score = CreateTestScore(score: 0.0);
// Act
await _repository.SaveAsync(score);
await SaveScoreAsync(score);
// Assert
var result = await _repository.GetByCanonicalIdAsync(score.CanonicalId);
@@ -691,7 +695,7 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var score = CreateTestScore(reasons: reasons);
// Act
await _repository.SaveAsync(score);
await SaveScoreAsync(score);
// Assert
var result = await _repository.GetByCanonicalIdAsync(score.CanonicalId);
@@ -706,8 +710,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
var older = CreateTestScore(score: 0.8, computedAt: DateTimeOffset.UtcNow.AddHours(-1));
var newer = CreateTestScore(score: 0.8, computedAt: DateTimeOffset.UtcNow);
await _repository.SaveAsync(older);
await _repository.SaveAsync(newer);
await SaveScoreAsync(older);
await SaveScoreAsync(newer);
// Act
var result = await _repository.GetTopScoresAsync(limit: 10);
@@ -723,6 +727,49 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
#region Test Helpers
private static readonly DateTimeOffset CanonicalSeedTimestamp =
new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
private async Task SaveScoreAsync(InterestScore score)
{
await SeedCanonicalAsync(score.CanonicalId);
await _repository.SaveAsync(score);
}
private async Task SaveScoresAsync(IReadOnlyCollection<InterestScore> scores)
{
foreach (var score in scores)
{
await SeedCanonicalAsync(score.CanonicalId);
}
await _repository.SaveManyAsync(scores);
}
private async Task SeedCanonicalAsync(Guid canonicalId, CanonicalStatus status = CanonicalStatus.Active)
{
var entity = new AdvisoryCanonicalEntity
{
Id = canonicalId,
Cve = $"CVE-2024-{canonicalId.ToString("N")[..5]}",
AffectsKey = "pkg:npm/test@1.0.0",
MergeHash = $"sha256:{canonicalId:N}",
Status = CanonicalStatusToString(status),
CreatedAt = CanonicalSeedTimestamp,
UpdatedAt = CanonicalSeedTimestamp,
};
await _canonicalRepository.UpsertAsync(entity);
}
private static string CanonicalStatusToString(CanonicalStatus status) => status switch
{
CanonicalStatus.Active => "active",
CanonicalStatus.Stub => "stub",
CanonicalStatus.Withdrawn => "withdrawn",
_ => "active"
};
private static InterestScore CreateTestScore(
Guid? canonicalId = null,
double score = 0.5,

View File

@@ -14,6 +14,7 @@ using StellaOps.Concelier.Cache.Valkey;
using StellaOps.Concelier.Core.Canonical;
using StellaOps.Concelier.Interest;
using StellaOps.Concelier.Interest.Models;
using StellaOps.Concelier.Persistence.Postgres.Models;
using StellaOps.Concelier.Persistence.Postgres.Repositories;
using Xunit;
@@ -30,6 +31,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
private readonly ConcelierPostgresFixture _fixture;
private readonly ConcelierDataSource _dataSource;
private readonly InterestScoreRepository _repository;
private readonly AdvisoryCanonicalRepository _canonicalRepository;
private readonly Mock<IAdvisoryCacheService> _cacheServiceMock;
private readonly Mock<ICanonicalAdvisoryStore> _advisoryStoreMock;
private readonly InterestScoreCalculator _calculator;
@@ -40,9 +42,10 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new InterestScoreRepository(_dataSource, NullLogger<InterestScoreRepository>.Instance);
_canonicalRepository = new AdvisoryCanonicalRepository(_dataSource, NullLogger<AdvisoryCanonicalRepository>.Instance);
_cacheServiceMock = new Mock<IAdvisoryCacheService>();
_advisoryStoreMock = new Mock<ICanonicalAdvisoryStore>();
@@ -83,7 +86,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
return new ValueTask(_fixture.TruncateAllTablesAsync());
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
#region ComputeScoreAsync Tests
@@ -120,9 +123,10 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
var score = await _service.ComputeScoreAsync(canonicalId);
// Assert
score.Score.Should().Be(0.45); // in_sbom (0.30) + no_vex_na (0.15)
score.Score.Should().Be(0.55); // in_sbom (0.30) + no_vex_na (0.15) + recent (0.10)
score.Reasons.Should().Contain("in_sbom");
score.Reasons.Should().Contain("no_vex_na");
score.Reasons.Should().Contain("recent");
}
[Trait("Category", TestCategories.Unit)]
@@ -142,11 +146,12 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
var score = await _service.ComputeScoreAsync(canonicalId);
// Assert
score.Score.Should().Be(0.90); // in_sbom (0.30) + reachable (0.25) + deployed (0.20) + no_vex_na (0.15)
score.Score.Should().Be(1.00); // in_sbom + reachable + deployed + no_vex_na + recent
score.Reasons.Should().Contain("in_sbom");
score.Reasons.Should().Contain("reachable");
score.Reasons.Should().Contain("deployed");
score.Reasons.Should().Contain("no_vex_na");
score.Reasons.Should().Contain("recent");
}
[Trait("Category", TestCategories.Unit)]
@@ -173,9 +178,10 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
var score = await _service.ComputeScoreAsync(canonicalId);
// Assert
score.Score.Should().Be(0.30); // Only in_sbom, no no_vex_na
score.Score.Should().Be(0.40); // in_sbom + recent, no no_vex_na
score.Reasons.Should().Contain("in_sbom");
score.Reasons.Should().NotContain("no_vex_na");
score.Reasons.Should().Contain("recent");
}
#endregion
@@ -194,6 +200,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
Reasons = ["in_sbom", "reachable", "deployed"],
ComputedAt = DateTimeOffset.UtcNow
};
await SeedCanonicalAsync(score.CanonicalId);
// Act
await _service.UpdateScoreAsync(score);
@@ -217,6 +224,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
Reasons = ["in_sbom"],
ComputedAt = DateTimeOffset.UtcNow
};
await SeedCanonicalAsync(score.CanonicalId);
// Act
await _service.UpdateScoreAsync(score);
@@ -236,6 +244,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
{
// Arrange
var canonicalId = Guid.NewGuid();
await SeedCanonicalAsync(canonicalId);
var initialScore = new InterestScore
{
CanonicalId = canonicalId,
@@ -278,6 +287,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
Reasons = ["in_sbom", "deployed"],
ComputedAt = DateTimeOffset.UtcNow
};
await SeedCanonicalAsync(score.CanonicalId);
await _repository.SaveAsync(score);
// Act
@@ -311,6 +321,9 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var id3 = Guid.NewGuid();
await SeedCanonicalAsync(id1);
await SeedCanonicalAsync(id2);
await SeedCanonicalAsync(id3);
// Setup signals for different scores
await _service.RecordSbomMatchAsync(id1, "sha256:a", "pkg:npm/a@1.0.0");
@@ -327,8 +340,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
var score2 = await _repository.GetByCanonicalIdAsync(id2);
var score3 = await _repository.GetByCanonicalIdAsync(id3);
score1!.Score.Should().Be(0.45); // in_sbom + no_vex_na
score2!.Score.Should().Be(0.70); // in_sbom + reachable + no_vex_na
score1!.Score.Should().Be(0.55); // in_sbom + no_vex_na + recent
score2!.Score.Should().Be(0.80); // in_sbom + reachable + no_vex_na + recent
score3!.Score.Should().Be(0.15); // only no_vex_na
}
@@ -339,6 +352,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
// Arrange
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
await SeedCanonicalAsync(id1);
await SeedCanonicalAsync(id2);
await _service.RecordSbomMatchAsync(id1, "sha256:a", "pkg:npm/a@1.0.0");
await _service.RecordSbomMatchAsync(id2, "sha256:b", "pkg:npm/b@1.0.0");
@@ -374,6 +389,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
foreach (var score in scores)
{
await SeedCanonicalAsync(score.CanonicalId);
await _repository.SaveAsync(score);
}
@@ -398,14 +414,24 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
{
// Arrange
// High tier
await _repository.SaveAsync(CreateScore(0.9));
await _repository.SaveAsync(CreateScore(0.8));
var highScore1 = CreateScore(0.9);
var highScore2 = CreateScore(0.8);
await SeedCanonicalAsync(highScore1.CanonicalId);
await SeedCanonicalAsync(highScore2.CanonicalId);
await _repository.SaveAsync(highScore1);
await _repository.SaveAsync(highScore2);
// Medium tier
await _repository.SaveAsync(CreateScore(0.5));
var mediumScore = CreateScore(0.5);
await SeedCanonicalAsync(mediumScore.CanonicalId);
await _repository.SaveAsync(mediumScore);
// Low tier
await _repository.SaveAsync(CreateScore(0.3));
var lowScore = CreateScore(0.3);
await SeedCanonicalAsync(lowScore.CanonicalId);
await _repository.SaveAsync(lowScore);
// None tier
await _repository.SaveAsync(CreateScore(0.1));
var noneScore = CreateScore(0.1);
await SeedCanonicalAsync(noneScore.CanonicalId);
await _repository.SaveAsync(noneScore);
// Act
var distribution = await _service.GetDistributionAsync();
@@ -431,6 +457,9 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
var lowScore1 = CreateScore(0.1, oldDate);
var lowScore2 = CreateScore(0.15, oldDate);
var highScore = CreateScore(0.8, oldDate);
await SeedCanonicalAsync(lowScore1.CanonicalId);
await SeedCanonicalAsync(lowScore2.CanonicalId);
await SeedCanonicalAsync(highScore.CanonicalId);
await _repository.SaveAsync(lowScore1);
await _repository.SaveAsync(lowScore2);
@@ -460,6 +489,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
// Arrange - one old, one recent
var lowOld = CreateScore(0.1, DateTimeOffset.UtcNow.AddDays(-60));
var lowRecent = CreateScore(0.1, DateTimeOffset.UtcNow.AddDays(-5));
await SeedCanonicalAsync(lowOld.CanonicalId);
await SeedCanonicalAsync(lowRecent.CanonicalId);
await _repository.SaveAsync(lowOld);
await _repository.SaveAsync(lowRecent);
@@ -491,6 +522,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
{
// Arrange
var highScore = CreateScore(0.8);
await SeedCanonicalAsync(highScore.CanonicalId);
await _repository.SaveAsync(highScore);
var stubAdvisory = CreateMockCanonicalAdvisory(highScore.CanonicalId, CanonicalStatus.Stub);
@@ -517,6 +549,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
{
// Arrange
var highScore = CreateScore(0.8);
await SeedCanonicalAsync(highScore.CanonicalId);
await _repository.SaveAsync(highScore);
var activeAdvisory = CreateMockCanonicalAdvisory(highScore.CanonicalId, CanonicalStatus.Active);
@@ -544,6 +577,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
{
// Arrange
var canonicalId = Guid.NewGuid();
await SeedCanonicalAsync(canonicalId);
// Act 1: Record SBOM match
await _service.RecordSbomMatchAsync(
@@ -562,21 +596,22 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
// Assert: Verify in database
var dbScore = await _repository.GetByCanonicalIdAsync(canonicalId);
dbScore.Should().NotBeNull();
dbScore!.Score.Should().Be(0.90);
dbScore!.Score.Should().Be(1.00);
dbScore.Reasons.Should().Contain("in_sbom");
dbScore.Reasons.Should().Contain("reachable");
dbScore.Reasons.Should().Contain("deployed");
dbScore.Reasons.Should().Contain("no_vex_na");
dbScore.Reasons.Should().Contain("recent");
// Assert: Verify cache was updated
_cacheServiceMock.Verify(
x => x.UpdateScoreAsync(canonicalId.ToString(), 0.90, It.IsAny<CancellationToken>()),
x => x.UpdateScoreAsync(canonicalId.ToString(), 1.00, It.IsAny<CancellationToken>()),
Times.Once);
// Act 4: Retrieve via service
var retrievedScore = await _service.GetScoreAsync(canonicalId);
retrievedScore.Should().NotBeNull();
retrievedScore!.Score.Should().Be(0.90);
retrievedScore!.Score.Should().Be(1.00);
}
[Trait("Category", TestCategories.Unit)]
@@ -596,7 +631,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
// Compute initial score
var initialScore = await _service.ComputeScoreAsync(canonicalId);
initialScore.Score.Should().Be(0.90);
initialScore.Score.Should().Be(1.00);
// Act: Add VEX not_affected statement
await _service.RecordVexStatementAsync(
@@ -612,7 +647,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
var reducedScore = await _service.ComputeScoreAsync(canonicalId);
// Assert: Score should be reduced (no no_vex_na factor)
reducedScore.Score.Should().Be(0.75);
reducedScore.Score.Should().Be(0.85);
reducedScore.Reasons.Should().NotContain("no_vex_na");
}
@@ -641,6 +676,7 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
Reasons = ["in_sbom"],
ComputedAt = DateTimeOffset.UtcNow
};
await SeedCanonicalAsync(score.CanonicalId);
// Act
await serviceWithCacheDisabled.UpdateScoreAsync(score);
@@ -659,6 +695,33 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
#region Test Helpers
private static readonly DateTimeOffset CanonicalSeedTimestamp =
new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
private async Task SeedCanonicalAsync(Guid canonicalId, CanonicalStatus status = CanonicalStatus.Active)
{
var entity = new AdvisoryCanonicalEntity
{
Id = canonicalId,
Cve = $"CVE-2024-{canonicalId.ToString("N")[..5]}",
AffectsKey = "pkg:npm/test@1.0.0",
MergeHash = $"sha256:{canonicalId:N}",
Status = CanonicalStatusToString(status),
CreatedAt = CanonicalSeedTimestamp,
UpdatedAt = CanonicalSeedTimestamp,
};
await _canonicalRepository.UpsertAsync(entity);
}
private static string CanonicalStatusToString(CanonicalStatus status) => status switch
{
CanonicalStatus.Active => "active",
CanonicalStatus.Stub => "stub",
CanonicalStatus.Withdrawn => "withdrawn",
_ => "active"
};
private static InterestScore CreateScore(double score, DateTimeOffset? computedAt = null)
{
return new InterestScore

View File

@@ -24,14 +24,14 @@ public sealed class KevFlagRepositoryTests : IAsyncLifetime
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_advisoryRepository = new AdvisoryRepository(_dataSource, NullLogger<AdvisoryRepository>.Instance);
_repository = new KevFlagRepository(_dataSource, NullLogger<KevFlagRepository>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Trait("Category", TestCategories.Unit)]
[Fact]

View File

@@ -14,20 +14,21 @@ namespace StellaOps.Concelier.Persistence.Tests.Linksets;
public sealed class AdvisoryLinksetCacheRepositoryTests : IAsyncLifetime
{
private readonly ConcelierPostgresFixture _fixture;
private readonly ConcelierDataSource _dataSource;
private readonly AdvisoryLinksetCacheRepository _repository;
public AdvisoryLinksetCacheRepositoryTests(ConcelierPostgresFixture fixture)
{
_fixture = fixture;
PostgresOptions options = fixture.Fixture.CreateOptions();
PostgresOptions options = fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new AdvisoryLinksetCacheRepository(dataSource, NullLogger<AdvisoryLinksetCacheRepository>.Instance);
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new AdvisoryLinksetCacheRepository(_dataSource, NullLogger<AdvisoryLinksetCacheRepository>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Fact]
public async Task Upsert_NormalizesTenantAndReplaces()
@@ -36,10 +37,16 @@ public sealed class AdvisoryLinksetCacheRepositoryTests : IAsyncLifetime
var initial = BuildLinkset("Tenant-A", "ghsa", "GHSA-1", new[] { "obs-1" }, createdAt, confidence: 0.5);
var replacement = BuildLinkset("tenant-a", "ghsa", "GHSA-1", new[] { "obs-2" }, createdAt.AddMinutes(5), confidence: 0.9);
await _repository.UpsertAsync(initial, CancellationToken.None);
await _repository.UpsertAsync(replacement, CancellationToken.None);
await _repository.UpsertAsync(initial, TestContext.Current.CancellationToken);
await _repository.UpsertAsync(replacement, TestContext.Current.CancellationToken);
var results = await _repository.FindByTenantAsync("TENANT-A", null, null, cursor: null, limit: 10, CancellationToken.None);
var results = await _repository.FindByTenantAsync(
"TENANT-A",
Array.Empty<string>(),
Array.Empty<string>(),
cursor: null,
limit: 10,
TestContext.Current.CancellationToken);
results.Should().ContainSingle();
results[0].TenantId.Should().Be("tenant-a");
@@ -61,14 +68,26 @@ public sealed class AdvisoryLinksetCacheRepositoryTests : IAsyncLifetime
foreach (var linkset in linksets)
{
await _repository.UpsertAsync(linkset, CancellationToken.None);
await _repository.UpsertAsync(linkset, TestContext.Current.CancellationToken);
}
var firstPage = await _repository.FindByTenantAsync("tenant", null, null, cursor: null, limit: 10, CancellationToken.None);
var firstPage = await _repository.FindByTenantAsync(
"tenant",
Array.Empty<string>(),
Array.Empty<string>(),
cursor: null,
limit: 10,
TestContext.Current.CancellationToken);
firstPage.Select(ls => ls.AdvisoryId).Should().ContainInOrder("ADV-001", "ADV-002", "ADV-003");
var cursor = new AdvisoryLinksetCursor(firstPage[1].CreatedAt, firstPage[1].AdvisoryId);
var secondPage = await _repository.FindByTenantAsync("tenant", null, null, cursor, limit: 10, CancellationToken.None);
var secondPage = await _repository.FindByTenantAsync(
"tenant",
Array.Empty<string>(),
Array.Empty<string>(),
cursor,
limit: 10,
TestContext.Current.CancellationToken);
secondPage.Should().ContainSingle();
secondPage[0].AdvisoryId.Should().Be("ADV-003");
@@ -112,9 +131,15 @@ public sealed class AdvisoryLinksetCacheRepositoryTests : IAsyncLifetime
CreatedAt: DateTimeOffset.Parse("2025-11-20T00:00:00Z"),
BuiltByJobId: "job-42");
await _repository.UpsertAsync(linkset, CancellationToken.None);
await _repository.UpsertAsync(linkset, TestContext.Current.CancellationToken);
var results = await _repository.FindByTenantAsync("tenant-x", new[] { "GHSA-9999" }, null, cursor: null, limit: 1, CancellationToken.None);
var results = await _repository.FindByTenantAsync(
"tenant-x",
new[] { "GHSA-9999" },
Array.Empty<string>(),
cursor: null,
limit: 1,
TestContext.Current.CancellationToken);
results.Should().ContainSingle();
var cached = results[0];

View File

@@ -25,7 +25,7 @@ public sealed class MergeEventRepositoryTests : IAsyncLifetime
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_advisoryRepository = new AdvisoryRepository(_dataSource, NullLogger<AdvisoryRepository>.Instance);
_sourceRepository = new SourceRepository(_dataSource, NullLogger<SourceRepository>.Instance);
@@ -33,7 +33,7 @@ public sealed class MergeEventRepositoryTests : IAsyncLifetime
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Trait("Category", TestCategories.Unit)]
[Fact]

View File

@@ -31,14 +31,14 @@ public sealed class AdvisoryPerformanceTests : IAsyncLifetime
_fixture = fixture;
_output = output;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new AdvisoryRepository(_dataSource, NullLogger<AdvisoryRepository>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
/// <summary>
/// Benchmark bulk advisory insertion performance.
@@ -375,7 +375,7 @@ public sealed class AdvisoryPerformanceTests : IAsyncLifetime
{
Id = Guid.NewGuid(),
AdvisoryKey = key,
PrimaryVulnId = $"CVE-2025-{key.GetHashCode():X8}"[..20],
PrimaryVulnId = BuildPrimaryVulnId(key),
Title = title ?? $"Test Advisory {key}",
Severity = "medium",
Summary = $"Summary for {key}",
@@ -385,6 +385,12 @@ public sealed class AdvisoryPerformanceTests : IAsyncLifetime
Provenance = $$$"""{"source": "performance-test", "key": "{{{key}}}"}"""
};
private static string BuildPrimaryVulnId(string key)
{
var baseId = $"CVE-2025-{key}";
return baseId.Length >= 20 ? baseId[..20] : baseId.PadRight(20, '0');
}
private static List<AdvisoryAliasEntity> CreateTestAliases(Guid advisoryId, string cve) =>
[
new AdvisoryAliasEntity

View File

@@ -34,13 +34,13 @@ public sealed class ProvenanceScopeRepositoryTests : IAsyncLifetime
public ProvenanceScopeRepositoryTests(ConcelierPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new ProvenanceScopeRepository(_dataSource, NullLogger<ProvenanceScopeRepository>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
#region Migration Validation

View File

@@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Persistence.Postgres.Models;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Persistence.Postgres.Repositories;
using Xunit;
@@ -33,7 +32,7 @@ public sealed class RepositoryIntegrationTests : IAsyncLifetime
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
@@ -53,7 +52,7 @@ public sealed class RepositoryIntegrationTests : IAsyncLifetime
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Trait("Category", TestCategories.Unit)]
[Fact]

View File

@@ -23,13 +23,13 @@ public sealed class SourceRepositoryTests : IAsyncLifetime
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new SourceRepository(_dataSource, NullLogger<SourceRepository>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Trait("Category", TestCategories.Unit)]
[Fact]

View File

@@ -24,14 +24,14 @@ public sealed class SourceStateRepositoryTests : IAsyncLifetime
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_sourceRepository = new SourceRepository(_dataSource, NullLogger<SourceRepository>.Instance);
_repository = new SourceStateRepository(_dataSource, NullLogger<SourceStateRepository>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Trait("Category", TestCategories.Unit)]
[Fact]

View File

@@ -36,14 +36,14 @@ public sealed class SyncLedgerRepositoryTests : IAsyncLifetime
public SyncLedgerRepositoryTests(ConcelierPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
var options = fixture.CreateOptions();
_dataSource = new ConcelierDataSource(Options.Create(options), NullLogger<ConcelierDataSource>.Instance);
_repository = new SyncLedgerRepository(_dataSource, NullLogger<SyncLedgerRepository>.Instance);
_policyService = new SitePolicyEnforcementService(_repository, NullLogger<SitePolicyEnforcementService>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
#region Task 3: Migration Validation

Some files were not shown because too many files have changed in this diff Show More