Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
This commit is contained in:
@@ -13,13 +13,15 @@ using StellaOps.Concelier.Storage.Mongo.Aliases;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
|
||||
public sealed class AdvisoryStore : IAdvisoryStore
|
||||
{
|
||||
public sealed class AdvisoryStore : IAdvisoryStore
|
||||
{
|
||||
private readonly IMongoDatabase _database;
|
||||
private readonly IMongoCollection<AdvisoryDocument> _collection;
|
||||
private readonly ILogger<AdvisoryStore> _logger;
|
||||
private readonly IAliasStore _aliasStore;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly MongoStorageOptions _options;
|
||||
private IMongoCollection<AdvisoryDocument>? _legacyCollection;
|
||||
|
||||
public AdvisoryStore(
|
||||
IMongoDatabase database,
|
||||
@@ -28,8 +30,8 @@ public sealed class AdvisoryStore : IAdvisoryStore
|
||||
IOptions<MongoStorageOptions> options,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_collection = (database ?? throw new ArgumentNullException(nameof(database)))
|
||||
.GetCollection<AdvisoryDocument>(MongoStorageDefaults.Collections.Advisory);
|
||||
_database = database ?? throw new ArgumentNullException(nameof(database));
|
||||
_collection = _database.GetCollection<AdvisoryDocument>(MongoStorageDefaults.Collections.Advisory);
|
||||
_aliasStore = aliasStore ?? throw new ArgumentNullException(nameof(aliasStore));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
@@ -69,14 +71,7 @@ public sealed class AdvisoryStore : IAdvisoryStore
|
||||
|
||||
var options = new ReplaceOptions { IsUpsert = true };
|
||||
var filter = Builders<AdvisoryDocument>.Filter.Eq(x => x.AdvisoryKey, advisory.AdvisoryKey);
|
||||
if (session is null)
|
||||
{
|
||||
await _collection.ReplaceOneAsync(filter, document, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _collection.ReplaceOneAsync(session, filter, document, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await ReplaceAsync(filter, document, options, session, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogDebug("Upserted advisory {AdvisoryKey}", advisory.AdvisoryKey);
|
||||
|
||||
var aliasEntries = BuildAliasEntries(advisory);
|
||||
@@ -129,6 +124,71 @@ public sealed class AdvisoryStore : IAdvisoryStore
|
||||
return cursor.Select(static doc => Deserialize(doc.Payload)).ToArray();
|
||||
}
|
||||
|
||||
private async Task ReplaceAsync(
|
||||
FilterDefinition<AdvisoryDocument> filter,
|
||||
AdvisoryDocument document,
|
||||
ReplaceOptions options,
|
||||
IClientSessionHandle? session,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (session is null)
|
||||
{
|
||||
await _collection.ReplaceOneAsync(filter, document, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _collection.ReplaceOneAsync(session, filter, document, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (MongoWriteException ex) when (IsNamespaceViewError(ex))
|
||||
{
|
||||
var legacyCollection = await GetLegacyAdvisoryCollectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (session is null)
|
||||
{
|
||||
await legacyCollection.ReplaceOneAsync(filter, document, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await legacyCollection.ReplaceOneAsync(session, filter, document, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNamespaceViewError(MongoWriteException ex)
|
||||
=> ex?.WriteError?.Code == 166 ||
|
||||
(ex?.WriteError?.Message?.Contains("is a view", StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
|
||||
private async ValueTask<IMongoCollection<AdvisoryDocument>> GetLegacyAdvisoryCollectionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_legacyCollection is not null)
|
||||
{
|
||||
return _legacyCollection;
|
||||
}
|
||||
|
||||
var filter = new BsonDocument("name", MongoStorageDefaults.Collections.Advisory);
|
||||
using var cursor = await _database
|
||||
.ListCollectionsAsync(new ListCollectionsOptions { Filter = filter }, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var info = await cursor.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException("Advisory collection metadata not found.");
|
||||
|
||||
if (!info.TryGetValue("options", out var optionsValue) || optionsValue is not BsonDocument optionsDocument)
|
||||
{
|
||||
throw new InvalidOperationException("Advisory view options missing.");
|
||||
}
|
||||
|
||||
if (!optionsDocument.TryGetValue("viewOn", out var viewOnValue) || viewOnValue.BsonType != BsonType.String)
|
||||
{
|
||||
throw new InvalidOperationException("Advisory view target not specified.");
|
||||
}
|
||||
|
||||
var targetName = viewOnValue.AsString;
|
||||
_legacyCollection = _database.GetCollection<AdvisoryDocument>(targetName);
|
||||
return _legacyCollection;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<Advisory> StreamAsync([EnumeratorCancellation] CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
{
|
||||
var options = new FindOptions<AdvisoryDocument>
|
||||
|
||||
@@ -46,14 +46,42 @@ public sealed class AliasStore : IAliasStore
|
||||
});
|
||||
}
|
||||
|
||||
if (documents.Count > 0)
|
||||
{
|
||||
await _collection.InsertManyAsync(
|
||||
documents,
|
||||
new InsertManyOptions { IsOrdered = false },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
if (documents.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _collection.InsertManyAsync(
|
||||
documents,
|
||||
new InsertManyOptions { IsOrdered = false },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (MongoBulkWriteException<AliasDocument> ex) when (ex.WriteErrors.Any(error => error.Category == ServerErrorCategory.DuplicateKey))
|
||||
{
|
||||
foreach (var writeError in ex.WriteErrors.Where(error => error.Category == ServerErrorCategory.DuplicateKey))
|
||||
{
|
||||
var duplicateDocument = documents.ElementAtOrDefault(writeError.Index);
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Alias duplicate detected while inserting {Scheme}:{Value} for advisory {AdvisoryKey}. Existing aliases: {Existing}",
|
||||
duplicateDocument?.Scheme,
|
||||
duplicateDocument?.Value,
|
||||
duplicateDocument?.AdvisoryKey,
|
||||
string.Join(", ", aliasList.Select(a => $"{a.Scheme}:{a.Value}")));
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Alias duplicate detected while inserting aliases for advisory {AdvisoryKey}. Aliases: {Aliases}",
|
||||
advisoryKey,
|
||||
string.Join(", ", aliasList.Select(a => $"{a.Scheme}:{a.Value}")));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aliasList.Length == 0)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user